Wire-compatible with C mosh. AES-128-OCB3, SSP transport, fragmentation, protobuf state sync, latch extension fields. Includes ServeRW bridge mode for embedding.
654 lines
15 KiB
Go
654 lines
15 KiB
Go
package mosh
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/bits"
|
|
)
|
|
|
|
// Hand-rolled protobuf encoding for the three mosh .proto schemas.
|
|
// Field numbers match upstream mobile-shell/mosh exactly.
|
|
|
|
// TransportInstruction is the outer transport wrapper (TransportBuffers.Instruction).
|
|
//
|
|
// Field numbers:
|
|
//
|
|
// 1: protocol_version (uint32)
|
|
// 2: old_num (uint64)
|
|
// 3: new_num (uint64)
|
|
// 4: ack_num (uint64)
|
|
// 5: throwaway_num (uint64)
|
|
// 6: diff (bytes)
|
|
// 7: chaff (bytes)
|
|
type TransportInstruction struct {
|
|
ProtocolVersion uint32
|
|
OldNum uint64
|
|
NewNum uint64
|
|
AckNum uint64
|
|
ThrowawayNum uint64
|
|
Diff []byte
|
|
Chaff []byte
|
|
LatchCaps []byte
|
|
}
|
|
|
|
// LatchControl is a latch extension control message.
|
|
type LatchControl struct {
|
|
Type uint32
|
|
Payload []byte
|
|
}
|
|
|
|
const (
|
|
CtrlSessionListReq uint32 = 1
|
|
CtrlSessionListResp uint32 = 2
|
|
CtrlSessionSwitch uint32 = 3
|
|
CtrlSessionSwitched uint32 = 4
|
|
CtrlSessionCreate uint32 = 5
|
|
CtrlSessionCreated uint32 = 6
|
|
)
|
|
|
|
const (
|
|
CapSessionControl byte = 1 << 0
|
|
)
|
|
|
|
// HostInstruction is one instruction within a HostMessage.
|
|
// Represents the extension fields of HostBuffers.Instruction:
|
|
//
|
|
// field 2 → HostBytes { field 4: hoststring }
|
|
// field 3 → ResizeMessage { field 5: width, field 6: height }
|
|
// field 7 → EchoAck { field 8: echo_ack_num }
|
|
type HostInstruction struct {
|
|
Hoststring []byte
|
|
Width int32 // 0 = not present
|
|
Height int32 // 0 = not present
|
|
EchoAckNum int64 // -1 = not present
|
|
Control *LatchControl
|
|
}
|
|
|
|
// UserInstruction is one instruction within a UserMessage.
|
|
// Represents the extension fields of ClientBuffers.Instruction:
|
|
//
|
|
// field 2 → Keystroke { field 4: keys }
|
|
// field 3 → ResizeMessage { field 5: width, field 6: height }
|
|
type UserInstruction struct {
|
|
Keys []byte
|
|
Width int32 // 0 = not present
|
|
Height int32 // 0 = not present
|
|
Control *LatchControl
|
|
}
|
|
|
|
// Wire type constants.
|
|
const (
|
|
wireVarint = 0
|
|
wireBytes = 2
|
|
)
|
|
|
|
var errTruncated = errors.New("mosh/pb: truncated message")
|
|
|
|
// --- Encoding helpers ---
|
|
|
|
func appendTag(b []byte, field int, wtype int) []byte {
|
|
return appendVarint(b, uint64(field<<3|wtype))
|
|
}
|
|
|
|
func appendVarint(b []byte, v uint64) []byte {
|
|
for v >= 0x80 {
|
|
b = append(b, byte(v)|0x80)
|
|
v >>= 7
|
|
}
|
|
return append(b, byte(v))
|
|
}
|
|
|
|
func appendTagVarint(b []byte, field int, v uint64) []byte {
|
|
b = appendTag(b, field, wireVarint)
|
|
return appendVarint(b, v)
|
|
}
|
|
|
|
func appendTagBytes(b []byte, field int, data []byte) []byte {
|
|
b = appendTag(b, field, wireBytes)
|
|
b = appendVarint(b, uint64(len(data)))
|
|
return append(b, data...)
|
|
}
|
|
|
|
// --- Decoding helpers ---
|
|
|
|
func decodeVarint(b []byte) (uint64, int) {
|
|
var v uint64
|
|
for i, c := range b {
|
|
if i >= binary.MaxVarintLen64 {
|
|
return 0, 0
|
|
}
|
|
v |= uint64(c&0x7f) << (7 * i)
|
|
if c < 0x80 {
|
|
return v, i + 1
|
|
}
|
|
}
|
|
return 0, 0
|
|
}
|
|
|
|
func decodeTag(b []byte) (field, wtype int, n int) {
|
|
v, n := decodeVarint(b)
|
|
if n == 0 {
|
|
return 0, 0, 0
|
|
}
|
|
return int(v >> 3), int(v & 7), n
|
|
}
|
|
|
|
// skipField advances past an unknown field.
|
|
func skipField(b []byte, wtype int) int {
|
|
switch wtype {
|
|
case wireVarint:
|
|
_, n := decodeVarint(b)
|
|
return n
|
|
case wireBytes:
|
|
length, n := decodeVarint(b)
|
|
if n == 0 {
|
|
return 0
|
|
}
|
|
return n + int(length)
|
|
case 5: // 32-bit
|
|
return 4
|
|
case 1: // 64-bit
|
|
return 8
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// --- TransportInstruction ---
|
|
|
|
func (ti *TransportInstruction) Marshal() []byte {
|
|
var b []byte
|
|
if ti.ProtocolVersion != 0 {
|
|
b = appendTagVarint(b, 1, uint64(ti.ProtocolVersion))
|
|
}
|
|
b = appendTagVarint(b, 2, ti.OldNum)
|
|
b = appendTagVarint(b, 3, ti.NewNum)
|
|
b = appendTagVarint(b, 4, ti.AckNum)
|
|
b = appendTagVarint(b, 5, ti.ThrowawayNum)
|
|
if len(ti.Diff) > 0 {
|
|
b = appendTagBytes(b, 6, ti.Diff)
|
|
}
|
|
if len(ti.Chaff) > 0 {
|
|
b = appendTagBytes(b, 7, ti.Chaff)
|
|
}
|
|
if len(ti.LatchCaps) > 0 {
|
|
b = appendTagBytes(b, 8, ti.LatchCaps)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (ti *TransportInstruction) Unmarshal(data []byte) error {
|
|
for len(data) > 0 {
|
|
field, wtype, n := decodeTag(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[n:]
|
|
|
|
switch field {
|
|
case 1:
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
ti.ProtocolVersion = uint32(v)
|
|
data = data[n:]
|
|
case 2:
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
ti.OldNum = v
|
|
data = data[n:]
|
|
case 3:
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
ti.NewNum = v
|
|
data = data[n:]
|
|
case 4:
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
ti.AckNum = v
|
|
data = data[n:]
|
|
case 5:
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
ti.ThrowawayNum = v
|
|
data = data[n:]
|
|
case 6:
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
ti.Diff = append([]byte(nil), data[n:n+int(length)]...)
|
|
data = data[n+int(length):]
|
|
case 7:
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
ti.Chaff = append([]byte(nil), data[n:n+int(length)]...)
|
|
data = data[n+int(length):]
|
|
case 8:
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
ti.LatchCaps = append([]byte(nil), data[n:n+int(length)]...)
|
|
data = data[n+int(length):]
|
|
default:
|
|
skip := skipField(data, wtype)
|
|
if skip == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[skip:]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- HostMessage ---
|
|
|
|
// MarshalHostMessage encodes a list of HostInstructions as a HostMessage protobuf.
|
|
func MarshalHostMessage(instrs []HostInstruction) []byte {
|
|
return marshalHostMessage(instrs)
|
|
}
|
|
|
|
func marshalHostMessage(instrs []HostInstruction) []byte {
|
|
var b []byte
|
|
for i := range instrs {
|
|
sub := instrs[i].marshal()
|
|
b = appendTagBytes(b, 1, sub)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// UnmarshalHostMessage decodes a HostMessage protobuf into a list of HostInstructions.
|
|
func UnmarshalHostMessage(data []byte) ([]HostInstruction, error) {
|
|
return unmarshalHostMessage(data)
|
|
}
|
|
|
|
func unmarshalHostMessage(data []byte) ([]HostInstruction, error) {
|
|
var instrs []HostInstruction
|
|
for len(data) > 0 {
|
|
field, _, n := decodeTag(data)
|
|
if n == 0 {
|
|
return nil, errTruncated
|
|
}
|
|
data = data[n:]
|
|
if field != 1 {
|
|
return nil, errors.New("mosh/pb: unexpected field in HostMessage")
|
|
}
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return nil, errTruncated
|
|
}
|
|
var hi HostInstruction
|
|
hi.EchoAckNum = -1
|
|
if err := hi.unmarshal(data[n : n+int(length)]); err != nil {
|
|
return nil, err
|
|
}
|
|
instrs = append(instrs, hi)
|
|
data = data[n+int(length):]
|
|
}
|
|
return instrs, nil
|
|
}
|
|
|
|
func (hi *HostInstruction) marshal() []byte {
|
|
var b []byte
|
|
if len(hi.Hoststring) > 0 {
|
|
// field 2: HostBytes submessage containing field 4: hoststring
|
|
sub := appendTagBytes(nil, 4, hi.Hoststring)
|
|
b = appendTagBytes(b, 2, sub)
|
|
}
|
|
if hi.Width > 0 || hi.Height > 0 {
|
|
// field 3: ResizeMessage submessage containing field 5: width, field 6: height
|
|
sub := appendTagVarint(nil, 5, uint64(hi.Width))
|
|
sub = appendTagVarint(sub, 6, uint64(hi.Height))
|
|
b = appendTagBytes(b, 3, sub)
|
|
}
|
|
if hi.EchoAckNum >= 0 {
|
|
// field 7: EchoAck submessage containing field 8: echo_ack_num
|
|
sub := appendTagVarint(nil, 8, uint64(hi.EchoAckNum))
|
|
b = appendTagBytes(b, 7, sub)
|
|
}
|
|
if hi.Control != nil {
|
|
sub := appendTagVarint(nil, 10, uint64(hi.Control.Type))
|
|
if len(hi.Control.Payload) > 0 {
|
|
sub = appendTagBytes(sub, 11, hi.Control.Payload)
|
|
}
|
|
b = appendTagBytes(b, 9, sub)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (hi *HostInstruction) unmarshal(data []byte) error {
|
|
for len(data) > 0 {
|
|
field, wtype, n := decodeTag(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[n:]
|
|
|
|
switch field {
|
|
case 2: // HostBytes
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
if err := hi.unmarshalHostBytes(data[n : n+int(length)]); err != nil {
|
|
return err
|
|
}
|
|
data = data[n+int(length):]
|
|
case 3: // ResizeMessage
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
if err := hi.unmarshalResize(data[n : n+int(length)]); err != nil {
|
|
return err
|
|
}
|
|
data = data[n+int(length):]
|
|
case 7: // EchoAck
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
if err := hi.unmarshalEchoAck(data[n : n+int(length)]); err != nil {
|
|
return err
|
|
}
|
|
data = data[n+int(length):]
|
|
case 9: // LatchControl
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
ctrl := &LatchControl{}
|
|
if err := ctrl.unmarshal(data[n : n+int(length)]); err != nil {
|
|
return err
|
|
}
|
|
hi.Control = ctrl
|
|
data = data[n+int(length):]
|
|
default:
|
|
skip := skipField(data, wtype)
|
|
if skip == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[skip:]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (hi *HostInstruction) unmarshalHostBytes(data []byte) error {
|
|
for len(data) > 0 {
|
|
field, wtype, n := decodeTag(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[n:]
|
|
if field == 4 {
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
hi.Hoststring = append([]byte(nil), data[n:n+int(length)]...)
|
|
data = data[n+int(length):]
|
|
} else {
|
|
skip := skipField(data, wtype)
|
|
if skip == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[skip:]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (hi *HostInstruction) unmarshalResize(data []byte) error {
|
|
return unmarshalResize(data, &hi.Width, &hi.Height)
|
|
}
|
|
|
|
func (hi *HostInstruction) unmarshalEchoAck(data []byte) error {
|
|
for len(data) > 0 {
|
|
field, wtype, n := decodeTag(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[n:]
|
|
if field == 8 {
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
hi.EchoAckNum = int64(v)
|
|
data = data[n:]
|
|
} else {
|
|
skip := skipField(data, wtype)
|
|
if skip == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[skip:]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- UserMessage ---
|
|
|
|
// MarshalUserMessage encodes a list of UserInstructions as a UserMessage protobuf.
|
|
func MarshalUserMessage(instrs []UserInstruction) []byte {
|
|
return marshalUserMessage(instrs)
|
|
}
|
|
|
|
func marshalUserMessage(instrs []UserInstruction) []byte {
|
|
var b []byte
|
|
for i := range instrs {
|
|
sub := instrs[i].marshal()
|
|
b = appendTagBytes(b, 1, sub)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func unmarshalUserMessage(data []byte) ([]UserInstruction, error) {
|
|
var instrs []UserInstruction
|
|
for len(data) > 0 {
|
|
field, _, n := decodeTag(data)
|
|
if n == 0 {
|
|
return nil, errTruncated
|
|
}
|
|
data = data[n:]
|
|
if field != 1 {
|
|
return nil, errors.New("mosh/pb: unexpected field in UserMessage")
|
|
}
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return nil, errTruncated
|
|
}
|
|
var ui UserInstruction
|
|
if err := ui.unmarshal(data[n : n+int(length)]); err != nil {
|
|
return nil, err
|
|
}
|
|
instrs = append(instrs, ui)
|
|
data = data[n+int(length):]
|
|
}
|
|
return instrs, nil
|
|
}
|
|
|
|
func (ui *UserInstruction) marshal() []byte {
|
|
var b []byte
|
|
if len(ui.Keys) > 0 {
|
|
// field 2: Keystroke submessage containing field 4: keys
|
|
sub := appendTagBytes(nil, 4, ui.Keys)
|
|
b = appendTagBytes(b, 2, sub)
|
|
}
|
|
if ui.Width > 0 || ui.Height > 0 {
|
|
// field 3: ResizeMessage submessage
|
|
sub := appendTagVarint(nil, 5, uint64(ui.Width))
|
|
sub = appendTagVarint(sub, 6, uint64(ui.Height))
|
|
b = appendTagBytes(b, 3, sub)
|
|
}
|
|
if ui.Control != nil {
|
|
sub := appendTagVarint(nil, 10, uint64(ui.Control.Type))
|
|
if len(ui.Control.Payload) > 0 {
|
|
sub = appendTagBytes(sub, 11, ui.Control.Payload)
|
|
}
|
|
b = appendTagBytes(b, 9, sub)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (ui *UserInstruction) unmarshal(data []byte) error {
|
|
for len(data) > 0 {
|
|
field, wtype, n := decodeTag(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[n:]
|
|
|
|
switch field {
|
|
case 2: // Keystroke
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
if err := ui.unmarshalKeystroke(data[n : n+int(length)]); err != nil {
|
|
return err
|
|
}
|
|
data = data[n+int(length):]
|
|
case 3: // ResizeMessage
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
if err := unmarshalResize(data[n:n+int(length)], &ui.Width, &ui.Height); err != nil {
|
|
return err
|
|
}
|
|
data = data[n+int(length):]
|
|
case 9: // LatchControl
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
ctrl := &LatchControl{}
|
|
if err := ctrl.unmarshal(data[n : n+int(length)]); err != nil {
|
|
return err
|
|
}
|
|
ui.Control = ctrl
|
|
data = data[n+int(length):]
|
|
default:
|
|
skip := skipField(data, wtype)
|
|
if skip == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[skip:]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ui *UserInstruction) unmarshalKeystroke(data []byte) error {
|
|
for len(data) > 0 {
|
|
field, wtype, n := decodeTag(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[n:]
|
|
if field == 4 {
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
ui.Keys = append(ui.Keys, data[n:n+int(length)]...)
|
|
data = data[n+int(length):]
|
|
} else {
|
|
skip := skipField(data, wtype)
|
|
if skip == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[skip:]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Shared ResizeMessage decoder (used by both Host and User).
|
|
func unmarshalResize(data []byte, width, height *int32) error {
|
|
for len(data) > 0 {
|
|
field, wtype, n := decodeTag(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[n:]
|
|
switch field {
|
|
case 5:
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
*width = int32(v)
|
|
data = data[n:]
|
|
case 6:
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
*height = int32(v)
|
|
data = data[n:]
|
|
default:
|
|
skip := skipField(data, wtype)
|
|
if skip == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[skip:]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *LatchControl) unmarshal(data []byte) error {
|
|
for len(data) > 0 {
|
|
field, wtype, n := decodeTag(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[n:]
|
|
switch field {
|
|
case 10:
|
|
v, n := decodeVarint(data)
|
|
if n == 0 {
|
|
return errTruncated
|
|
}
|
|
c.Type = uint32(v)
|
|
data = data[n:]
|
|
case 11:
|
|
length, n := decodeVarint(data)
|
|
if n == 0 || int(length) > len(data[n:]) {
|
|
return errTruncated
|
|
}
|
|
c.Payload = append([]byte(nil), data[n:n+int(length)]...)
|
|
data = data[n+int(length):]
|
|
default:
|
|
skip := skipField(data, wtype)
|
|
if skip == 0 {
|
|
return errTruncated
|
|
}
|
|
data = data[skip:]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// varintSize returns the encoded size of a varint.
|
|
func varintSize(v uint64) int {
|
|
if v == 0 {
|
|
return 1
|
|
}
|
|
return (bits.Len64(v) + 6) / 7
|
|
}
|