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}
236
237impl AttachHeader {
238 pub fn local_env_get(&self, var: &str) -> Option<&str> {
239 self.local_env.iter().find(|(k, _)| k == var).map(|(_, v)| v.as_str())
240 }
241}
242
243/// AttachReplyHeader is the blob of metadata that the shpool service prefixes
244/// the data stream with after an attach. In can be used to indicate a
245/// connection error.
246#[derive(Serialize, Deserialize, Debug)]
247pub struct AttachReplyHeader {
248 #[serde(default)]
249 pub status: AttachStatus,
250}
251
252/// ListReply is contains a list of active sessions to be displayed to the user.
253#[derive(Serialize, Deserialize, Debug)]
254pub struct ListReply {
255 #[serde(default)]
256 pub sessions: Vec<Session>,
257}
258
259/// Session describes an active session.
260#[derive(Serialize, Deserialize, Debug)]
261pub struct Session {
262 #[serde(default)]
263 pub name: String,
264 #[serde(default)]
265 pub started_at_unix_ms: i64,
266 #[serde(default)]
267 pub last_connected_at_unix_ms: Option<i64>,
268 #[serde(default)]
269 pub last_disconnected_at_unix_ms: Option<i64>,
270 #[serde(default)]
271 pub status: SessionStatus,
272}
273
274/// Indicates if a shpool session currently has a client attached.
275#[derive(Serialize, Deserialize, Debug, Default)]
276pub enum SessionStatus {
277 #[default]
278 Attached,
279 Disconnected,
280}
281
282impl fmt::Display for SessionStatus {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 match self {
285 SessionStatus::Attached => write!(f, "attached"),
286 SessionStatus::Disconnected => write!(f, "disconnected"),
287 }
288 }
289}
290
291/// AttachStatus indicates what happened during an attach attempt.
292#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
293pub enum AttachStatus {
294 /// Attached indicates that there was an existing shell session with
295 /// the given name, and `shpool attach` successfully connected to it.
296 ///
297 /// NOTE: warnings is not currently used, but it used to be, and we
298 /// might want it in the future, so it is not worth breaking the protocol
299 /// over.
300 Attached { warnings: Vec<String> },
301 /// Created indicates that there was no existing shell session with the
302 /// given name, so `shpool` created a new one.
303 ///
304 /// NOTE: warnings is not currently used, see above.
305 Created { warnings: Vec<String> },
306 /// Busy indicates that there is an existing shell session with the given
307 /// name, but another shpool session is currently connected to
308 /// it, so the connection attempt was rejected.
309 Busy,
310 /// Forbidden indicates that the daemon has rejected the connection
311 /// attempt for security reasons.
312 Forbidden(String),
313 /// Some unexpected error
314 UnexpectedError(String),
315}
316
317impl Default for AttachStatus {
318 fn default() -> Self {
319 AttachStatus::UnexpectedError(String::from("default"))
320 }
321}
322
323#[derive(Serialize, Deserialize, Debug, Default, Clone)]
324pub struct TtySize {
325 pub rows: u16,
326 pub cols: u16,
327 pub xpixel: u16,
328 pub ypixel: u16,
329}
330
331// Some metadata that may or may not require a shpool attach process
332// to switch sessions.
333#[derive(Serialize, Deserialize, Debug, Default, Clone)]
334pub struct MaybeSwitch {
335 /// If non-empty, this indicates that the receiving attach process
336 /// should hang up, then reattach to the given session name.
337 ///
338 /// Session switching is incompatible with session name templates,
339 /// so the attach process should exit and display an error to the
340 /// user if it gets a MaybeSwitch telling to to switch to a specific
341 /// session name.
342 pub switch_to: Option<String>,
343 /// The shpool wide variable environment. The attach process should
344 /// hang up and reattach if it has a session name template which
345 /// produces a new result with this new environment.
346 pub vars: Vec<(String, String)>,
347}
348
349/// ChunkKind is a tag that indicates what type of frame is being transmitted
350/// through the socket.
351#[derive(Copy, Clone, Debug, PartialEq)]
352pub enum ChunkKind {
353 /// After the kind tag, the chunk will have a 4 byte little endian length
354 /// prefix followed by the actual data.
355 Data = 0,
356 /// An empty chunk sent so that the daemon can check to make sure the attach
357 /// process is still listening.
358 Heartbeat = 1,
359 /// The child shell has exited. After the kind tag, the chunk will
360 /// have exactly 4 bytes of data, which will contain a little endian
361 /// code indicating the child's exit status.
362 ExitStatus = 2,
363 /// After the kind tag, the chunk contains a 4 byte little endian length
364 /// prefix, followed by a msgpack encoded MaybeSwitch struct.
365 MaybeSwitch = 3,
366}
367
368impl TryFrom<u8> for ChunkKind {
369 type Error = anyhow::Error;
370
371 fn try_from(v: u8) -> anyhow::Result<Self> {
372 match v {
373 0 => Ok(ChunkKind::Data),
374 1 => Ok(ChunkKind::Heartbeat),
375 2 => Ok(ChunkKind::ExitStatus),
376 3 => Ok(ChunkKind::MaybeSwitch),
377 _ => Err(anyhow!("unknown ChunkKind {}", v)),
378 }
379 }
380}
381
382/// Chunk represents of a chunk of data in the output stream
383///
384/// format:
385///
386/// ```text
387/// 1 byte: kind tag
388/// little endian 4 byte word: length prefix
389/// N bytes: data
390/// ```
391#[derive(Debug, PartialEq)]
392pub struct Chunk<'data> {
393 pub kind: ChunkKind,
394 pub buf: &'data [u8],
395}