// 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" ) // 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) } // 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"` } // --- 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) ) // 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"` } // 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"` }