Files
sherlock/internal/wsdatagram/wsdatagram.go
T
amacocian 22ca6b3dc7
CI / build (push) Failing after 0s
Setup gssh-mcp
2026-05-30 14:11:01 +02:00

107 lines
3.2 KiB
Go

// Package wsdatagram implements the fixed-size 2048-byte binary wire
// protocol spoken by Gssh's headless exec WebSocket endpoint
// (/api/v1/exec/{hostName}). The encoding mirrors the C# server's
// Models/ShellDatagram.cs exactly:
//
// [0] : Operation (byte)
// [1..2] : PayloadLength (uint16, host byte order — little-endian
// in practice, matching the x64 Linux server)
// [3..2047] : Payload (only the first PayloadLength bytes are
// meaningful)
//
// Op codes (Stdin is reserved for future use; the server currently
// ignores it):
//
// Start 0x01 client → server UTF-8 command bytes
// Stdout 0x02 server → client raw stream bytes
// Stderr 0x03 server → client raw stream bytes
// Exit 0x04 server → client ASCII text of integer exit code
// Timeout 0x05 server → client empty
//
// A single dialect is shared between client and server; peers ignore
// op codes they don't implement.
package wsdatagram
import (
"encoding/binary"
"fmt"
)
// FrameSize is the on-the-wire length of every datagram.
const FrameSize = 2048
// HeaderSize is the op-code byte plus the length uint16.
const HeaderSize = 3
// MaxPayloadSize is FrameSize - HeaderSize.
const MaxPayloadSize = FrameSize - HeaderSize
// Op is the one-byte operation code at offset 0.
type Op byte
const (
OpStart Op = 0x01
OpStdout Op = 0x02
OpStderr Op = 0x03
OpExit Op = 0x04
OpTimeout Op = 0x05
)
func (o Op) String() string {
switch o {
case OpStart:
return "Start"
case OpStdout:
return "Stdout"
case OpStderr:
return "Stderr"
case OpExit:
return "Exit"
case OpTimeout:
return "Timeout"
default:
return fmt.Sprintf("Op(0x%02x)", byte(o))
}
}
// Datagram is the decoded view of a single frame. Payload aliases
// into the source buffer when produced by Decode; callers that need
// to retain it past the next Decode must copy.
type Datagram struct {
Op Op
Payload []byte
}
// Encode writes d into a fresh FrameSize-byte buffer. Trailing bytes
// after the payload are left zero. Returns an error when the payload
// exceeds MaxPayloadSize.
func Encode(d Datagram) ([]byte, error) {
if len(d.Payload) > MaxPayloadSize {
return nil, fmt.Errorf("wsdatagram: payload of %d bytes exceeds %d", len(d.Payload), MaxPayloadSize)
}
buf := make([]byte, FrameSize)
buf[0] = byte(d.Op)
// Host byte order on the server (x64 little-endian); we match
// explicitly so we behave the same on big-endian Go targets.
binary.LittleEndian.PutUint16(buf[1:3], uint16(len(d.Payload)))
copy(buf[HeaderSize:], d.Payload)
return buf, nil
}
// Decode parses a frame. buf must be exactly FrameSize bytes. The
// returned Payload aliases into buf — copy it if you need to retain
// it past the next Decode call on the same buffer.
func Decode(buf []byte) (Datagram, error) {
if len(buf) != FrameSize {
return Datagram{}, fmt.Errorf("wsdatagram: frame is %d bytes, expected %d", len(buf), FrameSize)
}
length := binary.LittleEndian.Uint16(buf[1:3])
if int(length) > MaxPayloadSize {
return Datagram{}, fmt.Errorf("wsdatagram: payload length %d exceeds %d", length, MaxPayloadSize)
}
return Datagram{
Op: Op(buf[0]),
Payload: buf[HeaderSize : HeaderSize+int(length)],
}, nil
}