Files
mosh-go/client.go
T
realrasengan bb08f71e30 Initial release: pure Go mosh client and server
Wire-compatible with C mosh. AES-128-OCB3, SSP transport, fragmentation,
protobuf state sync, latch extension fields.

Includes ServeRW bridge mode for embedding.
2026-03-12 18:33:41 -07:00

209 lines
3.6 KiB
Go

package mosh
import (
"encoding/base64"
"fmt"
"net"
"os"
"sync"
"time"
)
// Client is a mosh client. It connects to a mosh server over UDP,
// handles the SSP transport, and provides send/recv for terminal I/O.
type Client struct {
conn *net.UDPConn
transport *Transport
ocb *OCB
mu sync.Mutex
output []byte // accumulated terminal output
outputC chan struct{}
done chan struct{}
wg sync.WaitGroup
}
// Dial connects to a mosh server. The key is the base64-encoded mosh key
// (with or without padding).
func Dial(host string, port int, key string) (*Client, error) {
// Pad key for base64 if needed.
for len(key)%4 != 0 {
key += "="
}
rawKey, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return nil, fmt.Errorf("mosh: bad key: %w", err)
}
ocb, err := NewOCB(rawKey)
if err != nil {
return nil, err
}
conn, err := net.DialUDP("udp4", nil, &net.UDPAddr{
IP: net.ParseIP(host),
Port: port,
})
if err != nil {
return nil, err
}
c := &Client{
conn: conn,
transport: NewTransport(ocb, false),
ocb: ocb,
outputC: make(chan struct{}, 1),
done: make(chan struct{}),
}
c.wg.Add(2)
go c.recvLoop()
go c.sendLoop()
// Send initial empty datagram to associate with the server.
c.tick()
return c, nil
}
// Send sends keystrokes to the server.
func (c *Client) Send(keys []byte) {
diff := MarshalUserMessage([]UserInstruction{{Keys: keys}})
c.transport.SetPending(diff)
c.tick()
}
// Resize sends a resize to the server.
func (c *Client) Resize(cols, rows uint16) {
diff := MarshalUserMessage([]UserInstruction{{
Width: int32(cols),
Height: int32(rows),
}})
c.transport.SetPending(diff)
c.tick()
}
// Recv reads accumulated terminal output, blocking until output is available
// or the timeout expires. Returns nil on timeout.
func (c *Client) Recv(timeout time.Duration) []byte {
// Check if output is already available.
c.mu.Lock()
if len(c.output) > 0 {
out := c.output
c.output = nil
c.mu.Unlock()
return out
}
c.mu.Unlock()
// Wait for output or timeout.
select {
case <-c.outputC:
case <-time.After(timeout):
return nil
case <-c.done:
return nil
}
c.mu.Lock()
out := c.output
c.output = nil
c.mu.Unlock()
return out
}
// Close shuts down the client.
func (c *Client) Close() {
select {
case <-c.done:
return
default:
close(c.done)
}
c.conn.Close()
c.wg.Wait()
}
// Transport returns the underlying SSP transport for advanced use
// (e.g., capability negotiation).
func (c *Client) Transport() *Transport {
return c.transport
}
func (c *Client) tick() {
for _, dg := range c.transport.Tick() {
c.conn.Write(dg)
}
}
func (c *Client) recvLoop() {
defer c.wg.Done()
buf := make([]byte, maxPayload+64)
for {
select {
case <-c.done:
return
default:
}
c.conn.SetReadDeadline(time.Now().Add(1 * time.Second))
n, err := c.conn.Read(buf)
if err != nil {
if os.IsTimeout(err) {
continue
}
return
}
if n < minDatagram {
continue
}
data := make([]byte, n)
copy(data, buf[:n])
diff := c.transport.Recv(data)
if diff == nil {
continue
}
instrs, err := UnmarshalHostMessage(diff)
if err != nil || len(instrs) == 0 {
continue
}
var output []byte
for _, hi := range instrs {
output = append(output, hi.Hoststring...)
}
if len(output) == 0 {
continue
}
c.mu.Lock()
c.output = append(c.output, output...)
c.mu.Unlock()
select {
case c.outputC <- struct{}{}:
default:
}
}
}
func (c *Client) sendLoop() {
defer c.wg.Done()
ticker := time.NewTicker(tickInterval)
defer ticker.Stop()
for {
select {
case <-c.done:
return
case <-ticker.C:
c.tick()
}
}
}