123 lines
4.7 KiB
Go
123 lines
4.7 KiB
Go
package main
|
|
|
|
// What a paired host sends down a worker lane is read at a size this machine
|
|
// chose, never at one the host chose: the host picks a frame's length, and a
|
|
// frame of any length must not be held whole in this machine's memory. The
|
|
// egress lane (egress_relay.go) and the context socket (contextsock.go) follow
|
|
// the same rule.
|
|
//
|
|
// - A job's first frame (the H spec on a harness worker, the S spec on a
|
|
// session worker) is read up to its lane's bound and refused past it, by
|
|
// name, before any of it is parsed.
|
|
// - What a lane carries after that is streamed: terminal input into the pty
|
|
// (session.go), the replica's bytes into its socket (mirror.go). Frames that
|
|
// nothing reads (a harness run and a root command, which read nothing after
|
|
// their spec) are dropped as they arrive.
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
const (
|
|
// harnessSpecMaxBytes bounds the H frame. Its bulk is the worktree tar
|
|
// (worktree_b64). The largest tree this lane hands back is
|
|
// maxReturnedTreeBytes (9 MiB, so 12 MiB in base64) inside one message of
|
|
// uvicornWSMaxSize (16 MiB), the most the host takes in (harness.go). A host
|
|
// that sends that tree into its next run needs those 12 MiB, plus the
|
|
// prompt and the run's credentials. Twice the lane's message size holds
|
|
// that with 20 MiB to spare.
|
|
harnessSpecMaxBytes = 2 * uvicornWSMaxSize
|
|
|
|
// sessionSpecMaxBytes bounds the S frame: a signed grant (op ids and the
|
|
// person's own words), ids, a public key, and for session:root:exec the
|
|
// command, itself held to rootExecCommandMax (64 KiB), which is at most
|
|
// 384 KiB even when JSON escapes every byte as \u00XX. 1 MiB is the bound
|
|
// Go's own HTTP server puts on a request head.
|
|
sessionSpecMaxBytes = http.DefaultMaxHeaderBytes
|
|
|
|
// sessionInputMaxBytes bounds one frame on an open terminal. The host relays
|
|
// what the person's browser sent it, one message each, and its own server
|
|
// takes a message up to uvicornWSMaxSize; the tag is one byte more. Input is
|
|
// streamed into the pty, so this caps a frame's length, not memory.
|
|
sessionInputMaxBytes = uvicornWSMaxSize + 1
|
|
|
|
// sessionResizeMaxBytes bounds the body of an R frame, {"cols":N,"rows":N}.
|
|
sessionResizeMaxBytes = 4 << 10
|
|
|
|
// heartbeatReplyMaxBytes bounds the host's answer to a heartbeat. It
|
|
// carries directives and the lists of grants this box should hold, each
|
|
// entry an id and a signature of about 130 bytes, so 4 MiB is tens of
|
|
// thousands of grants. Nothing in it is bulk: an update names a version,
|
|
// and the binary comes from the release manifest, not from this reply.
|
|
heartbeatReplyMaxBytes = 4 << 20
|
|
)
|
|
|
|
// errLaneNotJob is a first frame that is not a binary frame with a tag.
|
|
var errLaneNotJob = errors.New("the first frame is not a job")
|
|
|
|
// readLaneJob reads a worker's first frame: its tag, and its body up to
|
|
// maxFor(tag) bytes. A body past its bound is never kept: tooBig reports it,
|
|
// and body is nil. A tag the caller does not take should get a bound of 0;
|
|
// its body is then not read at all.
|
|
func readLaneJob(ws *websocket.Conn, maxFor func(tag byte) int64) (tag byte, body []byte, tooBig bool, err error) {
|
|
mt, r, err := ws.NextReader()
|
|
if err != nil {
|
|
return 0, nil, false, err
|
|
}
|
|
if mt != websocket.BinaryMessage {
|
|
return 0, nil, false, errLaneNotJob
|
|
}
|
|
var t [1]byte
|
|
if _, err := io.ReadFull(r, t[:]); err != nil {
|
|
return 0, nil, false, errLaneNotJob // an empty frame
|
|
}
|
|
max := maxFor(t[0])
|
|
if max <= 0 {
|
|
return t[0], nil, false, nil
|
|
}
|
|
body, err = io.ReadAll(io.LimitReader(r, max+1))
|
|
if err != nil {
|
|
return t[0], nil, false, err
|
|
}
|
|
if int64(len(body)) > max {
|
|
return t[0], nil, true, nil
|
|
}
|
|
return t[0], body, false, nil
|
|
}
|
|
|
|
// drainLane reads and drops every frame until the socket fails, holding none
|
|
// of them, and returns that failure. A lane that reads nothing after its job
|
|
// still has to read, to answer pings and to notice the host going away.
|
|
func drainLane(ws *websocket.Conn) error {
|
|
for {
|
|
_, r, err := ws.NextReader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(io.Discard, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// readHeartbeatReply reads the host's answer to a heartbeat up to
|
|
// heartbeatReplyMaxBytes. ok is false for a reply past the bound or one that
|
|
// failed to arrive: such a beat is treated as unanswered, so none of its
|
|
// directives runs and it does not count as the host answering.
|
|
func readHeartbeatReply(resp *http.Response) (body []byte, ok bool) {
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, heartbeatReplyMaxBytes+1))
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
if len(body) > heartbeatReplyMaxBytes {
|
|
fmt.Printf("heartbeat: the host's reply is more than %d MiB; ignored\n", heartbeatReplyMaxBytes>>20)
|
|
return nil, false
|
|
}
|
|
return body, true
|
|
}
|