Files
lpra/internal/proto/proto.go
T

382 lines
15 KiB
Go

// Package proto describes the wire messages shared by the server and the agent.
//
// All agent<->server traffic is plain JSON over HTTP(S) long-polling so it
// survives NAT and restrictive egress firewalls: the agent only ever makes
// outbound requests.
package proto
import "encoding/json"
// Control poll message types (server -> agent, returned from /api/agent/poll).
const (
// CtrlIdle is returned when the long-poll times out with nothing to do.
CtrlIdle = "idle"
// CtrlOpenTerminal tells the agent to spawn a PTY for the session id.
CtrlOpenTerminal = "open_terminal"
// CtrlOpenForward tells the agent to dial 127.0.0.1:Port and bridge it to
// the session id (reverse TCP port-forward).
CtrlOpenForward = "open_forward"
// CtrlTask tells the agent to fetch and run the one-shot task TaskID.
CtrlTask = "task"
// CtrlOpenSSH tells the agent to open an SSH connection to SSH and bridge
// its shell to the session id, as a terminal.
CtrlOpenSSH = "open_ssh"
)
// SSHTarget is where an SSH terminal connects and how it signs in: a
// password, a private key (PEM or OpenSSH, with its passphrase if it has
// one), or, with neither, the agent user's own keys in ~/.ssh.
type SSHTarget struct {
Host string `json:"host"`
Port int `json:"port,omitempty"`
User string `json:"user"`
Password string `json:"password,omitempty"`
Key string `json:"key,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
}
// EnrollRequest is sent once by a freshly installed agent.
type EnrollRequest struct {
Name string `json:"name"`
Hostname string `json:"hostname"`
OS string `json:"os"`
Arch string `json:"arch"`
Version string `json:"version"`
User string `json:"user,omitempty"` // OS user the agent runs as
}
// EnrollResponse carries the identity the agent persists and uses from then on.
type EnrollResponse struct {
AgentID string `json:"agent_id"`
Token string `json:"token"`
}
// ControlMessage is the body returned by the control long-poll.
type ControlMessage struct {
Type string `json:"type"`
SessionID string `json:"session_id,omitempty"`
Port int `json:"port,omitempty"` // CtrlOpenForward: client-side port to dial
TaskID string `json:"task_id,omitempty"` // CtrlTask
User string `json:"user,omitempty"` // CtrlOpenTerminal: OS user for the shell ("" = the agent's own)
SSH *SSHTarget `json:"ssh,omitempty"` // CtrlOpenSSH
}
// Transports an agent can reach the server by.
const (
TransportAuto = "auto" // WebSocket, long polling while it cannot connect
TransportWS = "ws" // WebSocket only
TransportLP = "lp" // long polling only
)
// TransportHeader carries the agent's transport setting on poll and config
// answers.
const TransportHeader = "X-Transport"
// FallbackHeader carries the server's direct address (one not behind a
// proxy like Cloudflare) on poll and config answers, for the agent to turn
// to when the main one is out of reach.
const FallbackHeader = "X-Fallback-Host"
// MainHeader carries the server's main address beside FallbackHeader, for an
// agent set to the direct one to fall back to the main one.
const MainHeader = "X-Main-Host"
// WSMsg is one message on an agent's WebSocket, either way. Server to agent:
// "ctrl" (Ctrl, as a control poll would answer), "cfg" (Tunnel and
// Transport, also a keep-alive), "in" (Frames for session SID), "gone"
// (session SID is over). Agent to server: "sub" (send the input of session
// SID here), "out" (Out of session SID).
type WSMsg struct {
T string `json:"t"`
Ctrl *ControlMessage `json:"ctrl,omitempty"`
SID string `json:"sid,omitempty"`
Frames []InFrame `json:"frames,omitempty"`
Out *OutMessage `json:"out,omitempty"`
Tunnel string `json:"tunnel,omitempty"`
Transport string `json:"transport,omitempty"`
Fallback string `json:"fallback,omitempty"`
Main string `json:"main,omitempty"`
}
// Terminal / stream frame types on the session "in" channel (server -> agent).
// Reused for both PTY terminals and raw TCP port-forward streams.
const (
FrameStdin = "stdin" // base64 bytes for the PTY / socket
FrameResize = "resize" // PTY window size (terminal only)
FrameClose = "close" // tear the stream down
)
// InFrame is one element of the stream downstream batch (server -> agent).
type InFrame struct {
Type string `json:"type"`
Data string `json:"data,omitempty"` // base64 bytes
Cols uint16 `json:"cols,omitempty"`
Rows uint16 `json:"rows,omitempty"`
}
// InBatch is the JSON body returned by GET /api/agent/term/{sid}/in.
type InBatch struct {
Frames []InFrame `json:"frames"`
}
// OutMessage is POSTed by the agent to /api/agent/term/{sid}/out.
type OutMessage struct {
Data string `json:"data,omitempty"` // base64 bytes
Exit bool `json:"exit,omitempty"`
// Status tells the panel how an SSH terminal's connection goes: one of
// the SSHStatus values, with Note to show.
Status string `json:"status,omitempty"`
Note string `json:"note,omitempty"`
}
// The steps of an SSH terminal's connection, as OutMessage.Status.
const (
SSHDial = "dial" // connecting to the host
SSHAuth = "auth" // the host answered, signing in
SSHOK = "ok" // signed in, the shell runs
SSHErr = "err" // it failed; Note says why
)
// --- one-shot tasks (file manager, native in the agent) ---
const (
TaskFSList = "fs.list" // list a directory
TaskFSRead = "fs.read" // read file content (for view/edit)
TaskFSWrite = "fs.write" // write/overwrite file (edit save, create, upload)
TaskFSMkdir = "fs.mkdir" // create directory
TaskFSDelete = "fs.delete" // delete a single file or empty dir
TaskFSDelTree = "fs.deltree" // recursively delete a directory
TaskFSUnzip = "fs.unzip" // unzip an archive (needs `unzip` on the client)
TaskFSZip = "fs.zip" // zip selected paths (native archive/zip)
TaskFSMove = "fs.move" // move/rename
TaskFSDownload = "fs.download" // read a whole file (larger cap) for browser download
TaskFSStat = "fs.stat" // a file's properties: type, size, mode, owner
TaskFSChmod = "fs.chmod" // change mode and owner, of a folder's contents too
TaskFSCopy = "fs.copy" // copy or move, with a rule for what is already there
TaskFSPut = "fs.put" // write a file the browser sends in pieces
TaskFSSend = "fs.send" // send a file to the browser in pieces
TaskSysUpdate = "sys.update" // download the latest agent binary and restart
TaskSysUsers = "sys.users" // report the agent's user and the login-capable users
TaskSysUserAdd = "sys.useradd" // create a new OS user
TaskSysStat = "sys.stat" // CPU load, RAM and disk usage right now
TaskSysSpeed = "sys.speed" // measure the link to the server (ping, down, up)
TaskDBTables = "db.tables" // an SQLite database's tables and views
TaskDBRows = "db.rows" // a page of a table's rows, with each row's key
TaskDBSet = "db.set" // set one value of one row
TaskDBAdd = "db.add" // add a row
TaskDBDel = "db.del" // delete rows by their keys
TaskDBSQL = "db.sql" // run a statement typed in the panel
TaskSSHFSOpen = "sshfs.open" // connect to an SSH host for a file pane; result {id, home}
TaskSSHFSClose = "sshfs.close" // drop that connection
TaskSSHFSOp = "sshfs.op" // a file operation on the host: list, read, write, mkdir, delete, deltree, move, copy, stat, chmod
TaskSSHFSSend = "sshfs.send" // a file of the host to the browser in pieces (FSXferArgs with Conn)
TaskSSHFSPut = "sshfs.put" // a file from the browser to the host in pieces
TaskFSShipOut = "fs.ship.out" // pack a file or folder and send it to another machine
TaskFSShipIn = "fs.ship.in" // unpack what another machine sends
)
// FSReadCap bounds how many bytes TaskFSRead returns (5 MiB). The panel lets a
// file be edited only when it fits, so that a save never cuts it short.
const FSReadCap = 5 * 1024 * 1024
// FSFileCap bounds a file sent either way in pieces (TaskFSPut, TaskFSSend).
const FSFileCap = 512 << 20
// FSXferArgs are the arguments of TaskFSPut and TaskFSSend: the file, and the
// transfer on the server whose pieces carry it - GET them for a put, POST them
// for a send, Chunk bytes each. Size is a put's length; -1 for a send.
type FSXferArgs struct {
Path string `json:"path"`
Xfer string `json:"xfer"`
Size int64 `json:"size"`
Chunk int `json:"chunk"`
// Conn is the SSH file connection the file is on (sshfs.send, sshfs.put).
Conn string `json:"conn,omitempty"`
}
// SSHFSArgs are the arguments of the sshfs.* tasks: the connection (Conn), or
// where to connect (SSH, for open); for an operation its name (Op) and its
// arguments (Args) in the shapes of the fs.* tasks.
type SSHFSArgs struct {
Conn string `json:"conn,omitempty"`
SSH *SSHTarget `json:"ssh,omitempty"`
Op string `json:"op,omitempty"`
Args json.RawMessage `json:"args,omitempty"`
}
// TaskEnvelope is what the agent fetches for a task id.
type TaskEnvelope struct {
Type string `json:"type"`
Args json.RawMessage `json:"args"`
}
// TaskResult is POSTed back by the agent.
type TaskResult struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// fs task argument shapes.
type FSPathArgs struct {
Path string `json:"path"`
}
type FSWriteArgs struct {
Path string `json:"path"`
Data string `json:"data"` // base64 content
}
type FSUnzipArgs struct {
Path string `json:"path"`
Dest string `json:"dest"`
}
type FSZipArgs struct {
Paths []string `json:"paths"`
Dest string `json:"dest"`
}
type FSMoveArgs struct {
Src string `json:"src"`
Dst string `json:"dst"`
}
// FSStatResult is returned for TaskFSStat. Mode holds the permission bits
// with setuid, setgid and sticky (07777). Users and Groups are the names on
// the machine, offered when choosing an owner.
type FSStatResult struct {
Path string `json:"path"`
IsDir bool `json:"is_dir"`
Link string `json:"link,omitempty"` // where a symlink points
Size int64 `json:"size"`
MTime int64 `json:"mtime"`
Mode uint32 `json:"mode"`
UID int `json:"uid"`
GID int `json:"gid"`
Owner string `json:"owner"`
Group string `json:"group"`
Users []string `json:"users"`
Groups []string `json:"groups"`
}
// FSChmodArgs are the arguments of TaskFSChmod. Mode nil, Owner or Group
// empty leave that as it is. Recursive applies to everything inside a folder
// as well, Only narrowing it to "files" or "dirs".
type FSChmodArgs struct {
Path string `json:"path"`
Mode *uint32 `json:"mode,omitempty"`
Owner string `json:"owner,omitempty"`
Group string `json:"group,omitempty"`
Recursive bool `json:"recursive,omitempty"`
Only string `json:"only,omitempty"`
}
// FSCopyArgs are the arguments of TaskFSCopy: Src goes to Dst (the full new
// path), copied or, with Move, moved. Conflict says what to do when Dst is
// already there: "error" (the default) stops, "replace" puts Src in its place,
// "skip" keeps it. Folders on both sides are merged under replace and skip.
type FSCopyArgs struct {
Src string `json:"src"`
Dst string `json:"dst"`
Move bool `json:"move,omitempty"`
Conflict string `json:"conflict,omitempty"`
}
// FSEntry is one directory entry.
type FSEntry struct {
Name string `json:"name"`
IsDir bool `json:"is_dir"`
Size int64 `json:"size"`
Mode string `json:"mode"`
MTime int64 `json:"mtime"`
}
// FSListResult is returned for TaskFSList.
type FSListResult struct {
Path string `json:"path"`
Entries []FSEntry `json:"entries"`
}
// FSReadResult is returned for TaskFSRead.
type FSReadResult struct {
Path string `json:"path"`
Size int64 `json:"size"`
Content string `json:"content"` // base64
Truncated bool `json:"truncated"`
Binary bool `json:"binary"`
}
// SysUsersResult is returned for TaskSysUsers.
type SysUsersResult struct {
Current string `json:"current"` // user the agent runs as
CanSwitch bool `json:"can_switch"` // agent is root, so it can open shells as (and create) other users
Users []string `json:"users"`
}
// SysUserAddArgs are the arguments of TaskSysUserAdd.
type SysUserAddArgs struct {
Name string `json:"name"`
Password string `json:"password,omitempty"` // empty leaves the account without a password
Sudo bool `json:"sudo"` // add to the sudo/wheel group
}
// SysStatResult is returned for TaskSysStat. Sizes are in bytes.
type SysStatResult struct {
CPU float64 `json:"cpu"` // load across all cores, 0..100
NCPU int `json:"ncpu"`
MemTotal uint64 `json:"mem_total"`
MemUsed uint64 `json:"mem_used"`
DiskTotal uint64 `json:"disk_total"` // filesystem holding "/"
DiskFree uint64 `json:"disk_free"`
}
// SysSpeedArgs are the arguments of TaskSysSpeed.
type SysSpeedArgs struct {
// Relay makes the agent use the relayed streams, which the server pipes
// to and from the browser.
Relay bool `json:"relay,omitempty"`
// WindowMs is how long to measure each direction for; 0 leaves it to the
// agent (3 s, all an agent that predates this argument does).
WindowMs int `json:"window_ms,omitempty"`
}
// SysSpeedResult is returned for TaskSysSpeed: the agent's HTTP link to the
// server as the agent measured it. Down is server -> agent.
type SysSpeedResult struct {
PingMs float64 `json:"ping_ms"`
DownBps float64 `json:"down_bps"` // bits per second
UpBps float64 `json:"up_bps"`
Relay bool `json:"relay,omitempty"` // the streams went through the relay
// Partial marks a test the server cut short: the numbers are what it had
// counted on its own side by then.
Partial bool `json:"partial,omitempty"`
}
// FSShipArgs are the arguments of TaskFSShipOut and TaskFSShipIn: Src (on the
// sending machine) lands in Dir as Name (on the receiving one), following
// Conflict as FSCopyArgs does. The sender packs it as a tar stream and either
// posts it in pieces of Chunk bytes to Relay on the server, for the receiver
// to take, or with SSH set writes it to tar on that host itself, and with
// Move then deletes Src. Both report to Job how far they are.
type FSShipArgs struct {
Src string `json:"src,omitempty"`
Name string `json:"name"`
Dir string `json:"dir"`
Conflict string `json:"conflict,omitempty"`
Move bool `json:"move,omitempty"`
Relay string `json:"relay,omitempty"`
Chunk int `json:"chunk,omitempty"`
Job string `json:"job,omitempty"`
SSH *SSHTarget `json:"ssh,omitempty"`
// Conn: the files of this side are on the SSH host of this agent's file
// connection (an SSH file pane), not on the machine itself.
Conn string `json:"conn,omitempty"`
}
// ShipProgress is what a sending machine reports: bytes of files sent out of
// Total, and files done.
type ShipProgress struct {
Total int64 `json:"total"`
Done int64 `json:"done"`
Files int `json:"files"`
Note string `json:"note,omitempty"` // what is going on: connecting, signing in
}