feat(poller): add SSH relay server with WebSocket-to-PTY bridge
Implements the SSH relay server (Task 2.1) that validates single-use Redis tokens via GETDEL, dials SSH to the target device with PTY, and bridges WebSocket binary/text frames to SSH stdin/stdout/stderr with idle timeout and per-user/per-device session limits. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
75
poller/internal/sshrelay/bridge.go
Normal file
75
poller/internal/sshrelay/bridge.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package sshrelay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
type ControlMsg struct {
|
||||
Type string `json:"type"`
|
||||
Cols int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
func bridge(ctx context.Context, cancel context.CancelFunc, ws *websocket.Conn,
|
||||
sshSess *ssh.Session, stdin io.WriteCloser, stdout, stderr io.Reader, lastActive *int64) {
|
||||
|
||||
// WebSocket → SSH stdin
|
||||
go func() {
|
||||
defer cancel()
|
||||
for {
|
||||
typ, data, err := ws.Read(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
atomic.StoreInt64(lastActive, time.Now().UnixNano())
|
||||
|
||||
if typ == websocket.MessageText {
|
||||
var ctrl ControlMsg
|
||||
if json.Unmarshal(data, &ctrl) != nil {
|
||||
continue
|
||||
}
|
||||
if ctrl.Type == "resize" && ctrl.Cols > 0 && ctrl.Cols <= 500 && ctrl.Rows > 0 && ctrl.Rows <= 200 {
|
||||
sshSess.WindowChange(ctrl.Rows, ctrl.Cols)
|
||||
}
|
||||
continue
|
||||
}
|
||||
stdin.Write(data)
|
||||
}
|
||||
}()
|
||||
|
||||
// SSH stdout → WebSocket
|
||||
go func() {
|
||||
defer cancel()
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := stdout.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
atomic.StoreInt64(lastActive, time.Now().UnixNano())
|
||||
ws.Write(ctx, websocket.MessageBinary, buf[:n])
|
||||
}
|
||||
}()
|
||||
|
||||
// SSH stderr → WebSocket
|
||||
go func() {
|
||||
defer cancel()
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := stderr.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ws.Write(ctx, websocket.MessageBinary, buf[:n])
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
Reference in New Issue
Block a user