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}