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}
67
68/// KillRequest represents a request to kill
69/// the given named sessions.
70#[derive(Serialize, Deserialize, Debug)]
71pub struct KillRequest {
72 /// The sessions to detach
73 #[serde(default)]
74 pub sessions: Vec<String>,
75}
76
77#[derive(Serialize, Deserialize, Debug)]
78pub struct KillReply {
79 #[serde(default)]
80 pub not_found_sessions: Vec<String>,
81}
82
83/// DetachRequest represents a request to detach
84/// from the given named sessions.
85#[derive(Serialize, Deserialize, Debug)]
86pub struct DetachRequest {
87 /// The sessions to detach
88 #[serde(default)]
89 pub sessions: Vec<String>,
90}
91
92#[derive(Serialize, Deserialize, Debug)]
93pub struct DetachReply {
94 /// sessions that are not even in the session table
95 #[serde(default)]
96 pub not_found_sessions: Vec<String>,
97 /// sessions that are in the session table, but have no
98 /// tty attached
99 #[serde(default)]
100 pub not_attached_sessions: Vec<String>,
101}
102
103#[derive(Serialize, Deserialize, Debug, Default, ValueEnum, Clone)]
104pub enum LogLevel {
105 #[default]
106 Off,
107 Error,
108 Warn,
109 Info,
110 Debug,
111 Trace,
112}
113
114// SetLogLevelRequest contains a request to set a new
115// log level
116#[derive(Serialize, Deserialize, Debug)]
117pub struct SetLogLevelRequest {
118 #[serde(default)]
119 pub level: LogLevel,
120}
121
122#[derive(Serialize, Deserialize, Debug)]
123pub struct SetLogLevelReply {}
124
125/// SessionMessageRequest represents a request that
126/// ought to be routed to the session indicated by
127/// `session_name`.
128#[derive(Serialize, Deserialize, Debug)]
129pub struct SessionMessageRequest {
130 /// The session to route this request to.
131 #[serde(default)]
132 pub session_name: String,
133 /// The actual message to send to the session.
134 #[serde(default)]
135 pub payload: SessionMessageRequestPayload,
136}
137
138/// SessionMessageRequestPayload contains a request for
139/// a running session.
140#[derive(Serialize, Deserialize, Debug, Default)]
141pub enum SessionMessageRequestPayload {
142 /// Resize a named session's pty. Generated when
143 /// a `shpool attach` process receives a SIGWINCH.
144 Resize(ResizeRequest),
145 /// Detach the given session. Generated internally
146 /// by the server from a batch detach request.
147 #[default]
148 Detach,
149}
150
151/// ResizeRequest resizes the pty for a named session.
152///
153/// We use an out-of-band request rather than doing this
154/// in the input stream because we don't want to have to
155/// introduce a framing protocol for the input stream.
156#[derive(Serialize, Deserialize, Debug)]
157pub struct ResizeRequest {
158 /// The size of the client's tty
159 #[serde(default)]
160 pub tty_size: TtySize,
161}
162
163#[derive(Serialize, Deserialize, Debug, PartialEq)]
164pub enum SessionMessageReply {
165 /// The session was not found in the session table
166 NotFound,
167 /// There is not terminal attached to the session so
168 /// it can't handle messages right now.
169 NotAttached,
170 /// The response to a resize message
171 Resize(ResizeReply),
172 /// The response to a detach message
173 Detach(SessionMessageDetachReply),
174}
175
176/// A reply to a detach message
177#[derive(Serialize, Deserialize, Debug, PartialEq)]
178pub enum SessionMessageDetachReply {
179 Ok,
180}
181
182/// A reply to a resize message
183#[derive(Serialize, Deserialize, Debug, PartialEq)]
184pub enum ResizeReply {
185 Ok,
186}
187
188/// AttachHeader is the blob of metadata that a client transmits when it
189/// first dials into the shpool daemon indicating which shell it wants
190/// to attach to.
191#[derive(Serialize, Deserialize, Debug, Default)]
192pub struct AttachHeader {
193 /// The name of the session to create or attach to.
194 #[serde(default)]
195 pub name: String,
196 /// The size of the local tty. Passed along so that the remote
197 /// pty can be kept in sync (important so curses applications look
198 /// right).
199 #[serde(default)]
200 pub local_tty_size: TtySize,
201 /// A subset of the environment of the shell that `shpool attach` is run
202 /// in. Contains only some variables needed to set up the shell when
203 /// shpool forks off a process. For now the list is just `SSH_AUTH_SOCK`
204 /// and `TERM`.
205 #[serde(default)]
206 pub local_env: Vec<(String, String)>,
207 /// If specified, sets a time limit on how long the shell will be open
208 /// when the shell is first created (does nothing in the case of a
209 /// reattach). The daemon is responsible for automatically killing the
210 /// session once the ttl is over.
211 #[serde(default)]
212 pub ttl_secs: Option<u64>,
213 /// If specified, a command to run instead of the users default shell.
214 #[serde(default)]
215 pub cmd: Option<String>,
216}
217
218impl AttachHeader {
219 pub fn local_env_get(&self, var: &str) -> Option<&str> {
220 self.local_env.iter().find(|(k, _)| k == var).map(|(_, v)| v.as_str())
221 }
222}
223
224/// AttachReplyHeader is the blob of metadata that the shpool service prefixes
225/// the data stream with after an attach. In can be used to indicate a
226/// connection error.
227#[derive(Serialize, Deserialize, Debug)]
228pub struct AttachReplyHeader {
229 #[serde(default)]
230 pub status: AttachStatus,
231}
232
233/// ListReply is contains a list of active sessions to be displayed to the user.
234#[derive(Serialize, Deserialize, Debug)]
235pub struct ListReply {
236 #[serde(default)]
237 pub sessions: Vec<Session>,
238}
239
240/// Session describes an active session.
241#[derive(Serialize, Deserialize, Debug)]
242pub struct Session {
243 #[serde(default)]
244 pub name: String,
245 #[serde(default)]
246 pub started_at_unix_ms: i64,
247 #[serde(default)]
248 pub status: SessionStatus,
249}
250
251/// Indicates if a shpool session currently has a client attached.
252#[derive(Serialize, Deserialize, Debug, Default)]
253pub enum SessionStatus {
254 #[default]
255 Attached,
256 Disconnected,
257}
258
259impl fmt::Display for SessionStatus {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 match self {
262 SessionStatus::Attached => write!(f, "attached"),
263 SessionStatus::Disconnected => write!(f, "disconnected"),
264 }
265 }
266}
267
268/// AttachStatus indicates what happened during an attach attempt.
269#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
270pub enum AttachStatus {
271 /// Attached indicates that there was an existing shell session with
272 /// the given name, and `shpool attach` successfully connected to it.
273 ///
274 /// NOTE: warnings is not currently used, but it used to be, and we
275 /// might want it in the future, so it is not worth breaking the protocol
276 /// over.
277 Attached { warnings: Vec<String> },
278 /// Created indicates that there was no existing shell session with the
279 /// given name, so `shpool` created a new one.
280 ///
281 /// NOTE: warnings is not currently used, see above.
282 Created { warnings: Vec<String> },
283 /// Busy indicates that there is an existing shell session with the given
284 /// name, but another shpool session is currently connected to
285 /// it, so the connection attempt was rejected.
286 Busy,
287 /// Forbidden indicates that the daemon has rejected the connection
288 /// attempt for security reasons.
289 Forbidden(String),
290 /// Some unexpected error
291 UnexpectedError(String),
292}
293
294impl Default for AttachStatus {
295 fn default() -> Self {
296 AttachStatus::UnexpectedError(String::from("default"))
297 }
298}
299
300#[derive(Serialize, Deserialize, Debug, Default, Clone)]
301pub struct TtySize {
302 pub rows: u16,
303 pub cols: u16,
304 pub xpixel: u16,
305 pub ypixel: u16,
306}
307
308/// ChunkKind is a tag that indicates what type of frame is being transmitted
309/// through the socket.
310#[derive(Copy, Clone, Debug, PartialEq)]
311pub enum ChunkKind {
312 /// After the kind tag, the chunk will have a 4 byte little endian length
313 /// prefix followed by the actual data.
314 Data = 0,
315 /// An empty chunk sent so that the daemon can check to make sure the attach
316 /// process is still listening.
317 Heartbeat = 1,
318 /// The child shell has exited. After the kind tag, the chunk will
319 /// have exactly 4 bytes of data, which will contain a little endian
320 /// code indicating the child's exit status.
321 ExitStatus = 2,
322}
323
324impl TryFrom<u8> for ChunkKind {
325 type Error = anyhow::Error;
326
327 fn try_from(v: u8) -> anyhow::Result<Self> {
328 match v {
329 0 => Ok(ChunkKind::Data),
330 1 => Ok(ChunkKind::Heartbeat),
331 2 => Ok(ChunkKind::ExitStatus),
332 _ => Err(anyhow!("unknown ChunkKind {}", v)),
333 }
334 }
335}
336
337/// Chunk represents of a chunk of data in the output stream
338///
339/// format:
340///
341/// ```text
342/// 1 byte: kind tag
343/// little endian 4 byte word: length prefix
344/// N bytes: data
345/// ```
346#[derive(Debug, PartialEq)]
347pub struct Chunk<'data> {
348 pub kind: ChunkKind,
349 pub buf: &'data [u8],
350}