// 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 "strings" 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"` // Saved: a host the user saved on the server; it fills the rest there and // never reaches an agent. Saved int64 `json:"saved,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) Dir string `json:"dir,omitempty"` // CtrlOpenTerminal: the folder to start in ("" or missing = home) 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"` // Link: a symbolic link (IsDir then says where it leads: a folder). Link bool `json:"link,omitempty"` } // 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"` // More: other things of Src's folder sent in the same stream under their // own names (several files dragged at once). Agents before 0.2.0 do not // know it; the server sends to them one at a time. More []string `json:"more,omitempty"` } // ShipNames is what lands at the other end: Name, then More by their names. func (a FSShipArgs) ShipNames() []string { n := []string{a.Name} for _, p := range a.More { p = strings.TrimRight(p, "/") n = append(n, p[strings.LastIndex(p, "/")+1:]) } return n } // 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 }