Skip to main content

conmonrs/streaming_server/remotecommand/
websocket.rs

1//! Exec and attach over a WebSocket: the framing of each subprotocol and
2//! the input of the client.
3
4use super::{
5    CLOSE_BYTE, ClientEvent, ClientInput, Output, RESIZE_BYTE, STDIN_BYTE, Stdin, Transport,
6    Version,
7};
8use crate::streaming_server::{
9    channel,
10    idle::Activity,
11    stream_protocol::{ClientBytes, ClientStr, first_supported},
12    web::{self, ClientMessage, Message, WebSocket},
13};
14use bytes::Bytes;
15use futures::stream::SplitStream;
16use tracing::{debug, trace};
17
18/// How the messages of a WebSocket session look.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub(crate) struct Framing {
21    pub(crate) version: Version,
22
23    /// Whether the payloads are base64 encoded in text messages, see
24    /// [`channel`].
25    pub(crate) base64: bool,
26}
27
28/// The WebSocket subprotocols, without one a client gets binary `v1`.
29pub(super) const WEBSOCKET_PROTOCOLS: [(&str, Framing); 5] = [
30    ("v5.channel.k8s.io", Framing::V5),
31    (
32        "v4.channel.k8s.io",
33        Framing {
34            version: Version::V4,
35            base64: false,
36        },
37    ),
38    (
39        "v4.base64.channel.k8s.io",
40        Framing {
41            version: Version::V4,
42            base64: true,
43        },
44    ),
45    (
46        "channel.k8s.io",
47        Framing {
48            version: Version::V1,
49            base64: false,
50        },
51    ),
52    (
53        "base64.channel.k8s.io",
54        Framing {
55            version: Version::V1,
56            base64: true,
57        },
58    ),
59];
60
61impl Framing {
62    /// The framing of `v5.channel.k8s.io`.
63    pub(crate) const V5: Self = Self {
64        version: Version::V5,
65        base64: false,
66    };
67
68    /// The framing for the subprotocols the client requested, in the order
69    /// of the client, with the subprotocol the server answers with.
70    pub(crate) fn negotiate<'a>(
71        requested: impl IntoIterator<Item = &'a str>,
72    ) -> Option<(Self, Option<&'static str>)> {
73        let mut requested = requested.into_iter().peekable();
74        if requested.peek().is_none() {
75            return Some((
76                Self {
77                    version: Version::V1,
78                    base64: false,
79                },
80                None,
81            ));
82        }
83        first_supported(requested, &WEBSOCKET_PROTOCOLS).map(|(framing, p)| (framing, Some(p)))
84    }
85
86    /// A message of the server on a channel.
87    pub(crate) fn message(self, channel: u8, data: &[u8]) -> Message {
88        channel::message(self.base64, channel, data)
89    }
90
91    /// What a message of the client means, none for one which needs no
92    /// handling. Like for Kubernetes, text messages carry the same as binary
93    /// ones, also for v5, its server does not look at the type.
94    pub(crate) fn event(self, message: Message) -> Option<ClientEvent> {
95        let data = match message {
96            Message::Binary(data) => data,
97            Message::Text(text) => Bytes::from(text),
98            Message::Close(c) => {
99                if let Some(cf) = c {
100                    debug!(
101                        "Got WebSocket close with code {} and reason {:?}",
102                        cf.code,
103                        ClientStr(&cf.reason)
104                    );
105                } else {
106                    debug!("Got WebSocket close without close frame");
107                }
108                return Some(ClientEvent::Closed);
109            }
110            // Skipped by web::next_message, reads never return raw frames.
111            Message::Pong(_) | Message::Ping(_) | Message::Frame(_) => return None,
112        };
113
114        if self.version == Version::V5 {
115            let Some(&channel) = data.first() else {
116                trace!("Got empty message");
117                return None;
118            };
119            trace!("Got {} bytes", data.len());
120            return match channel {
121                STDIN_BYTE => Stdin::data(data.slice(1..)).map(ClientEvent::Stdin),
122                // Only stdin can be closed, the message names it, like
123                // `[CLOSE_BYTE, STDIN_BYTE]`.
124                CLOSE_BYTE if data[..] == [CLOSE_BYTE, STDIN_BYTE] => {
125                    debug!("Got close message");
126                    Some(ClientEvent::Stdin(Stdin::Close))
127                }
128                CLOSE_BYTE => {
129                    debug!("Ignoring close message {:?}", ClientBytes(&data[1..]));
130                    None
131                }
132                RESIZE_BYTE => Some(ClientEvent::Resize(data.slice(1..))),
133                x => {
134                    debug!("Ignoring message for unknown channel {x}");
135                    None
136                }
137            };
138        }
139
140        // Before v5 there is no close signal, only stdin and resize events
141        // come from the client.
142        match channel::decode(self.base64, data)? {
143            (STDIN_BYTE, payload) => Stdin::data(payload).map(ClientEvent::Stdin),
144            (RESIZE_BYTE, payload) => Some(ClientEvent::Resize(payload)),
145            (channel, _) => {
146                trace!("Ignoring message for channel {channel}");
147                None
148            }
149        }
150    }
151}
152
153/// The input of a WebSocket client.
154pub(crate) struct WebSocketInput {
155    pub(crate) messages: SplitStream<WebSocket>,
156    pub(crate) framing: Framing,
157
158    /// Touched by every message of the client except for pings and pongs,
159    /// which a client like the WebSocket executor of client-go sends as a
160    /// heartbeat. CRI-O does not count them either.
161    pub(crate) activity: Activity,
162
163    /// Touched whenever a part of what the server sends went out, see
164    /// [`web::ClientIo::written`].
165    pub(crate) written: Activity,
166
167    /// Drops the input of a client which sent a message larger than the
168    /// server takes, see [`web::Discard`].
169    pub(crate) discard: web::Discard,
170}
171
172impl ClientInput for WebSocketInput {
173    async fn next(&mut self) -> Option<ClientEvent> {
174        loop {
175            let message = match web::next_message(&mut self.messages, &self.discard).await {
176                ClientMessage::Data(message) => message,
177                // The framing logs it and ends the input.
178                ClientMessage::Closed(frame) => Message::Close(frame),
179                ClientMessage::TooLarge => return Some(ClientEvent::TooLarge),
180                ClientMessage::Gone => return None,
181            };
182            self.activity.touch();
183            if let Some(event) = self.framing.event(message) {
184                return Some(event);
185            }
186        }
187    }
188
189    async fn answer_close(&mut self) {
190        web::answer_close(&mut self.messages, &self.written).await;
191    }
192
193    async fn drain(self, output: Output) {
194        if let Transport::WebSocket { sender, .. } = output.transport {
195            web::drain(sender, self.messages).await;
196        }
197    }
198}