107 lines
3.2 KiB
Go
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
|
|
}
|