shpool_protocol/lib.rs
1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{default::Default, fmt};
16
17use anyhow::anyhow;
18use clap::ValueEnum;
19use serde_derive::{Deserialize, Serialize};
20
21pub const VERSION: &str = env!("CARGO_PKG_VERSION");
22
23/// The header used to advertize daemon version.
24///
25/// This header gets written by the daemon to every stream as
26/// soon as it is opened, which allows the client to compare
27/// version strings for protocol negotiation (basically just
28/// deciding if the user ought to be warned about mismatched
29/// versions).
30#[derive(Serialize, Deserialize, Debug)]
31pub struct VersionHeader {
32 pub version: String,
33}
34
35/// The blob of metadata that a client transmits when it
36/// first connects.
37///
38/// It uses an enum to allow different connection types
39/// to be initiated on the same socket. The ConnectHeader is always prefixed
40/// with a 4 byte little endian unsigned word to indicate length.
41#[derive(Serialize, Deserialize, Debug)]
42pub enum ConnectHeader {
43 /// Attach to the named session indicated by the given header.
44 ///
45 /// Responds with an AttachReplyHeader.
46 Attach(AttachHeader),
47 /// List all of the currently active sessions.
48 List,
49 /// A message for a named, running sessions. This
50 /// provides a mechanism for RPC-like calls to be
51 /// made to running sessions. Messages are only
52 /// delivered if there is currently a client attached
53 /// to the session because we need a servicing thread
54 /// with access to the SessionInner to respond to requests
55 /// (we could implement a mailbox system or something
56 /// for detached threads, but so far we have not needed to).
57 SessionMessage(SessionMessageRequest),
58 /// A message to request that a list of running
59 /// sessions get detached from.
60 Detach(DetachRequest),
61 /// A message to request that a list of running
62 /// sessions get killed.
63 Kill(KillRequest),
64 /// A request to set the log level to a new value.
65 SetLogLevel(SetLogLevelRequest),
66 /// A request to get the current vars. The reply is just a Vars struct.
67 GetVars,
68 /// A request to modify the variable environment.
69 ModifyVar(ModifyVarRequest),
70}
71
72/// KillRequest represents a request to kill
73/// the given named sessions.
74#[derive(Serialize, Deserialize, Debug)]
75pub struct KillRequest {
76 /// The sessions to detach
77 #[serde(default)]
78 pub sessions: Vec<String>,
79}
80
81#[derive(Serialize, Deserialize, Debug)]
82pub struct KillReply {
83 #[serde(default)]
84 pub not_found_sessions: Vec<String>,
85}
86
87/// DetachRequest represents a request to detach
88/// from the given named sessions.
89#[derive(Serialize, Deserialize, Debug)]
90pub struct DetachRequest {
91 /// The sessions to detach
92 #[serde(default)]
93 pub sessions: Vec<String>,
94}
95
96#[derive(Serialize, Deserialize, Debug)]
97pub struct DetachReply {
98 /// sessions that are not even in the session table
99 #[serde(default)]
100 pub not_found_sessions: Vec<String>,
101 /// sessions that are in the session table, but have no
102 /// tty attached
103 #[serde(default)]
104 pub not_attached_sessions: Vec<String>,
105}
106
107#[derive(Serialize, Deserialize, Debug, Default, ValueEnum, Clone)]
108pub enum LogLevel {
109 #[default]
110 Off,
111 Error,
112 Warn,
113 Info,
114 Debug,
115 Trace,
116}
117
118// SetLogLevelRequest contains a request to set a new
119// log level
120#[derive(Serialize, Deserialize, Debug)]
121pub struct SetLogLevelRequest {
122 #[serde(default)]
123 pub level: LogLevel,
124}
125
126#[derive(Serialize, Deserialize, Debug)]
127pub struct SetLogLevelReply {}
128
129#[derive(Serialize, Deserialize, Debug)]
130pub struct ModifyVarRequest {
131 pub var: String,
132 /// If none, this is a request to remove the var from the
133 /// environment entirely.
134 pub val: Option<String>,
135}
136
137#[derive(Serialize, Deserialize, Debug)]
138pub struct ModifyVarReply {}
139
140/// SessionMessageRequest represents a request that
141/// ought to be routed to the session indicated by
142/// `session_name`.
143#[derive(Serialize, Deserialize, Debug)]
144pub struct SessionMessageRequest {
145 /// The session to route this request to.
146 #[serde(default)]
147 pub session_name: String,
148 /// The actual message to send to the session.
149 #[serde(default)]
150 pub payload: SessionMessageRequestPayload,
151}
152
153/// SessionMessageRequestPayload contains a request for
154/// a running session.
155#[derive(Serialize, Deserialize, Debug, Default)]
156pub enum SessionMessageRequestPayload {
157 /// Resize a named session's pty. Generated when
158 /// a `shpool attach` process receives a SIGWINCH.
159 Resize(ResizeRequest),
160 /// Detach the given session. Generated internally
161 /// by the server from a batch detach request.
162 #[default]
163 Detach,
164}
165
166/// ResizeRequest resizes the pty for a named session.
167///
168/// We use an out-of-band request rather than doing this
169/// in the input stream because we don't want to have to
170/// introduce a framing protocol for the input stream.
171#[derive(Serialize, Deserialize, Debug)]
172pub struct ResizeRequest {
173 /// The size of the client's tty
174 #[serde(default)]
175 pub tty_size: TtySize,
176}
177
178#[derive(Serialize, Deserialize, Debug, PartialEq)]
179pub enum SessionMessageReply {
180 /// The session was not found in the session table
181 NotFound,
182 /// There is not terminal attached to the session so
183 /// it can't handle messages right now.
184 NotAttached,
185 /// The response to a resize message
186 Resize(ResizeReply),
187 /// The response to a detach message
188 Detach(SessionMessageDetachReply),
189}
190
191/// A reply to a detach message
192#[derive(Serialize, Deserialize, Debug, PartialEq)]
193pub enum SessionMessageDetachReply {
194 Ok,
195}
196
197/// A reply to a resize message
198#[derive(Serialize, Deserialize, Debug, PartialEq)]
199pub enum ResizeReply {
200 Ok,
201}
202
203/// AttachHeader is the blob of metadata that a client transmits when it
204/// first dials into the shpool daemon indicating which shell it wants
205/// to attach to.
206#[derive(Serialize, Deserialize, Debug, Default)]
207pub struct AttachHeader {
208 /// The name of the session to create or attach to.
209 #[serde(default)]
210 pub name: String,
211 /// The size of the local tty. Passed along so that the remote
212 /// pty can be kept in sync (important so curses applications look
213 /// right).
214 #[serde(default)]
215 pub local_tty_size: TtySize,
216 /// A subset of the environment of the shell that `shpool attach` is run
217 /// in. Contains only some variables needed to set up the shell when
218 /// shpool forks off a process. For now the list is just `SSH_AUTH_SOCK`
219 /// and `TERM`.
220 #[serde(default)]
221 pub local_env: Vec<(String, String)>,
222 /// If specified, sets a time limit on how long the shell will be open
223 /// when the shell is first created (does nothing in the case of a
224 /// reattach). The daemon is responsible for automatically killing the
225 /// session once the ttl is over.
226 #[serde(default)]
227 pub ttl_secs: Option<u64>,
228 /// If specified, a command to run instead of the users default shell.
229 #[serde(default)]
230 pub cmd: Option<String>,
231 /// If specified, the directory to start the shell in. If not, $HOME
232 /// should be used.
233 #[serde(default)]
234 pub dir: Option<String>,
235 /// If specified, shpool will inject the given command into the shell
236 /// when it first starts up. This option is ignored for reattaches.
237 /// Note that this differs from the cmd option in that it is run
238 /// directly in the shell rather than replacing the shell. Think of
239 /// it as running `source cmd` rather than `exec cmd`. The main
240 /// usecase is to be able to automatically enter some useful context
241 /// such as a particular directory with a python virtual environment
242 /// already set up for example.
243 #[serde(default)]
244 pub start_cmd: Option<String>,
245}
246
247impl AttachHeader {
248 pub fn local_env_get(&self, var: &str) -> Option<&str> {
249 self.local_env.iter().find(|(k, _)| k == var).map(|(_, v)| v.as_str())
250 }
251}
252
253/// AttachReplyHeader is the blob of metadata that the shpool service prefixes
254/// the data stream with after an attach. In can be used to indicate a
255/// connection error.
256#[derive(Serialize, Deserialize, Debug)]
257pub struct AttachReplyHeader {
258 #[serde(default)]
259 pub status: AttachStatus,
260}
261
262/// ListReply is contains a list of active sessions to be displayed to the user.
263#[derive(Serialize, Deserialize, Debug)]
264pub struct ListReply {
265 #[serde(default)]
266 pub sessions: Vec<Session>,
267}
268
269/// Session describes an active session.
270#[derive(Serialize, Deserialize, Debug)]
271pub struct Session {
272 #[serde(default)]
273 pub name: String,
274 #[serde(default)]
275 pub started_at_unix_ms: i64,
276 #[serde(default)]
277 pub last_connected_at_unix_ms: Option<i64>,
278 #[serde(default)]
279 pub last_disconnected_at_unix_ms: Option<i64>,
280 #[serde(default)]
281 pub status: SessionStatus,
282}
283
284/// Indicates if a shpool session currently has a client attached.
285#[derive(Serialize, Deserialize, Debug, Default)]
286pub enum SessionStatus {
287 #[default]
288 Attached,
289 Disconnected,
290}
291
292impl fmt::Display for SessionStatus {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 match self {
295 SessionStatus::Attached => write!(f, "attached"),
296 SessionStatus::Disconnected => write!(f, "disconnected"),
297 }
298 }
299}
300
301/// AttachStatus indicates what happened during an attach attempt.
302#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
303pub enum AttachStatus {
304 /// Attached indicates that there was an existing shell session with
305 /// the given name, and `shpool attach` successfully connected to it.
306 ///
307 /// NOTE: warnings is not currently used, but it used to be, and we
308 /// might want it in the future, so it is not worth breaking the protocol
309 /// over.
310 Attached { warnings: Vec<String> },
311 /// Created indicates that there was no existing shell session with the
312 /// given name, so `shpool` created a new one.
313 ///
314 /// NOTE: warnings is not currently used, see above.
315 Created { warnings: Vec<String> },
316 /// Busy indicates that there is an existing shell session with the given
317 /// name, but another shpool session is currently connected to
318 /// it, so the connection attempt was rejected.
319 Busy,
320 /// Forbidden indicates that the daemon has rejected the connection
321 /// attempt for security reasons.
322 Forbidden(String),
323 /// Some unexpected error
324 UnexpectedError(String),
325}
326
327impl Default for AttachStatus {
328 fn default() -> Self {
329 AttachStatus::UnexpectedError(String::from("default"))
330 }
331}
332
333#[derive(Serialize, Deserialize, Debug, Default, Clone)]
334pub struct TtySize {
335 pub rows: u16,
336 pub cols: u16,
337 pub xpixel: u16,
338 pub ypixel: u16,
339}
340
341// Some metadata that may or may not require a shpool attach process
342// to switch sessions.
343#[derive(Serialize, Deserialize, Debug, Default, Clone)]
344pub struct MaybeSwitch {
345 /// If non-empty, this indicates that the receiving attach process
346 /// should hang up, then reattach to the given session name.
347 ///
348 /// Session switching is incompatible with session name templates,
349 /// so the attach process should exit and display an error to the
350 /// user if it gets a MaybeSwitch telling to to switch to a specific
351 /// session name.
352 pub switch_to: Option<String>,
353 /// The shpool wide variable environment. The attach process should
354 /// hang up and reattach if it has a session name template which
355 /// produces a new result with this new environment.
356 pub vars: Vec<(String, String)>,
357}
358
359/// ChunkKind is a tag that indicates what type of frame is being transmitted
360/// through the socket.
361#[derive(Copy, Clone, Debug, PartialEq)]
362pub enum ChunkKind {
363 /// After the kind tag, the chunk will have a 4 byte little endian length
364 /// prefix followed by the actual data.
365 Data = 0,
366 /// An empty chunk sent so that the daemon can check to make sure the attach
367 /// process is still listening.
368 Heartbeat = 1,
369 /// The child shell has exited. After the kind tag, the chunk will
370 /// have exactly 4 bytes of data, which will contain a little endian
371 /// code indicating the child's exit status.
372 ExitStatus = 2,
373 /// After the kind tag, the chunk contains a 4 byte little endian length
374 /// prefix, followed by a msgpack encoded MaybeSwitch struct.
375 MaybeSwitch = 3,
376}
377
378impl TryFrom<u8> for ChunkKind {
379 type Error = anyhow::Error;
380
381 fn try_from(v: u8) -> anyhow::Result<Self> {
382 match v {
383 0 => Ok(ChunkKind::Data),
384 1 => Ok(ChunkKind::Heartbeat),
385 2 => Ok(ChunkKind::ExitStatus),
386 3 => Ok(ChunkKind::MaybeSwitch),
387 _ => Err(anyhow!("unknown ChunkKind {}", v)),
388 }
389 }
390}
391
392/// Chunk represents of a chunk of data in the output stream
393///
394/// format:
395///
396/// ```text
397/// 1 byte: kind tag
398/// little endian 4 byte word: length prefix
399/// N bytes: data
400/// ```
401#[derive(Debug, PartialEq)]
402pub struct Chunk<'data> {
403 pub kind: ChunkKind,
404 pub buf: &'data [u8],
405}