Skip to main content

netget/server/guacamole/
wire.rs

1//! The Guacamole protocol's text framing, shared by both roles: `LEN.VALUE,…;` instructions
2//! whose lengths count Unicode code points, read with guacd's own bounds; X11 keysyms for
3//! the keys a handler names; and text rendered to PNG, so a handler draws words rather than
4//! writing image bytes.
5use anyhow::{bail, ensure, Context, Result};
6use base64::Engine;
7use tokio::io::{AsyncRead, AsyncReadExt};
8
9/// guacd's `GUAC_INSTRUCTION_MAX_LENGTH`: one instruction, in bytes.
10pub const MAX_INSTRUCTION: usize = 8192;
11/// guacd's `GUAC_INSTRUCTION_MAX_ELEMENTS`.
12pub const MAX_ELEMENTS: usize = 128;
13/// The protocol version both roles speak.
14pub const VERSION: &str = "VERSION_1_3_0";
15/// Base64 carried in one `blob`: what guacd itself sends at most.
16pub const BLOB_CHUNK: usize = 6048;
17/// A clipboard stream is at most this long, decoded.
18pub const MAX_CLIPBOARD: usize = 64 * 1024;
19pub const PORT: u16 = 4822;
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct Instruction {
23    pub opcode: String,
24    pub args: Vec<String>,
25}
26
27impl Instruction {
28    pub fn new(opcode: &str, args: &[&str]) -> Self {
29        Self {
30            opcode: opcode.into(),
31            args: args.iter().map(|s| s.to_string()).collect(),
32        }
33    }
34    pub fn arg(&self, i: usize) -> &str {
35        self.args.get(i).map(String::as_str).unwrap_or("")
36    }
37}
38
39fn element(out: &mut String, v: &str) {
40    out.push_str(&v.chars().count().to_string());
41    out.push('.');
42    out.push_str(v);
43}
44
45pub fn encode(opcode: &str, args: &[&str]) -> String {
46    let mut out = String::new();
47    element(&mut out, opcode);
48    for a in args {
49        out.push(',');
50        element(&mut out, a);
51    }
52    out.push(';');
53    out
54}
55
56pub fn encode_ins(i: &Instruction) -> String {
57    let args: Vec<&str> = i.args.iter().map(String::as_str).collect();
58    encode(&i.opcode, &args)
59}
60
61/// Reads instructions from a byte stream, refusing one past guacd's bounds.
62pub struct Reader<R> {
63    inner: R,
64    buf: Vec<u8>,
65}
66
67impl<R: AsyncRead + Unpin> Reader<R> {
68    pub fn new(inner: R) -> Self {
69        Self {
70            inner,
71            buf: Vec::new(),
72        }
73    }
74
75    /// The next instruction; None at a clean end of stream.
76    pub async fn next(&mut self) -> Result<Option<Instruction>> {
77        loop {
78            if let Some((ins, used)) = parse(&self.buf)? {
79                self.buf.drain(..used);
80                return Ok(Some(ins));
81            }
82            ensure!(
83                self.buf.len() <= MAX_INSTRUCTION,
84                "instruction longer than {MAX_INSTRUCTION} bytes"
85            );
86            let mut chunk = [0u8; 4096];
87            let n = self.inner.read(&mut chunk).await?;
88            if n == 0 {
89                ensure!(self.buf.is_empty(), "connection closed mid-instruction");
90                return Ok(None);
91            }
92            self.buf.extend_from_slice(&chunk[..n]);
93        }
94    }
95}
96
97/// One complete instruction at the start of `b` and the bytes it used, or None when more
98/// bytes are needed.
99pub fn parse(b: &[u8]) -> Result<Option<(Instruction, usize)>> {
100    let mut pos = 0;
101    let mut elements: Vec<String> = Vec::new();
102    loop {
103        // LENGTH '.'
104        let dot = match b[pos..].iter().take(12).position(|&c| c == b'.') {
105            Some(d) => d,
106            None if b.len() - pos < 12 => return Ok(None),
107            None => bail!("element length is not a number followed by '.'"),
108        };
109        let len_text = std::str::from_utf8(&b[pos..pos + dot])?;
110        ensure!(
111            !len_text.is_empty() && len_text.bytes().all(|c| c.is_ascii_digit()),
112            "element length {len_text:?} is not a number"
113        );
114        let chars: usize = len_text.parse()?;
115        ensure!(chars <= MAX_INSTRUCTION, "element of {chars} characters");
116        pos += dot + 1;
117        // `chars` code points of UTF-8.
118        let start = pos;
119        let mut seen = 0;
120        while seen < chars {
121            let Some(&lead) = b.get(pos) else {
122                return Ok(None);
123            };
124            let width = match lead {
125                0x00..=0x7f => 1,
126                0xc0..=0xdf => 2,
127                0xe0..=0xef => 3,
128                0xf0..=0xf7 => 4,
129                _ => bail!("invalid UTF-8 in an element"),
130            };
131            pos += width;
132            seen += 1;
133        }
134        if pos >= b.len() {
135            return Ok(None);
136        }
137        let value = std::str::from_utf8(&b[start..pos]).context("element is not UTF-8")?;
138        elements.push(value.to_string());
139        ensure!(
140            elements.len() <= MAX_ELEMENTS,
141            "more than {MAX_ELEMENTS} elements"
142        );
143        match b[pos] {
144            b',' => pos += 1,
145            b';' => {
146                pos += 1;
147                let mut it = elements.into_iter();
148                let opcode = it.next().unwrap_or_default();
149                return Ok(Some((
150                    Instruction {
151                        opcode,
152                        args: it.collect(),
153                    },
154                    pos,
155                )));
156            }
157            c => bail!("expected ',' or ';' after an element, got {:?}", c as char),
158        }
159    }
160}
161
162// ---------------------------------------------------------------------------------------
163// Keysyms
164
165const NAMED: &[(&str, u32)] = &[
166    ("Return", 0xff0d),
167    ("Enter", 0xff0d),
168    ("BackSpace", 0xff08),
169    ("Tab", 0xff09),
170    ("Escape", 0xff1b),
171    ("Delete", 0xffff),
172    ("Home", 0xff50),
173    ("Left", 0xff51),
174    ("Up", 0xff52),
175    ("Right", 0xff53),
176    ("Down", 0xff54),
177    ("Page_Up", 0xff55),
178    ("Page_Down", 0xff56),
179    ("End", 0xff57),
180    ("Insert", 0xff63),
181    ("Shift_L", 0xffe1),
182    ("Control_L", 0xffe3),
183    ("Alt_L", 0xffe9),
184    ("Super_L", 0xffeb),
185    ("space", 0x20),
186];
187
188/// The keysym a character types: Latin-1 directly, everything else in the Unicode range.
189pub fn keysym_of_char(c: char) -> u32 {
190    match c {
191        '\n' | '\r' => 0xff0d,
192        '\t' => 0xff09,
193        c if (c as u32) < 0x100 => c as u32,
194        c => 0x0100_0000 + c as u32,
195    }
196}
197
198/// A key a handler names: `Return`, `Escape`, `F1`…`F12`, a single character, or a keysym.
199pub fn keysym_of_name(name: &str) -> Result<u32> {
200    if let Some((_, k)) = NAMED.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)) {
201        return Ok(*k);
202    }
203    if let Some(n) = name.strip_prefix('F').and_then(|n| n.parse::<u32>().ok()) {
204        ensure!((1..=35).contains(&n), "function keys are F1-F35");
205        return Ok(0xffbe + n - 1);
206    }
207    let mut chars = name.chars();
208    if let (Some(c), None) = (chars.next(), chars.clone().next()) {
209        return Ok(keysym_of_char(c));
210    }
211    if let Some(hex) = name.strip_prefix("0x") {
212        return u32::from_str_radix(hex, 16).context("keysym is not hex");
213    }
214    bail!("unknown key {name:?}: use Return, Escape, Tab, BackSpace, an arrow, F1-F35, a character or 0x<keysym>")
215}
216
217/// What a pressed keysym is, as the handler is told it: Some(char) for one that types text.
218pub fn char_of_keysym(k: u32) -> Option<char> {
219    match k {
220        0x20..=0x7e | 0xa0..=0xff => char::from_u32(k),
221        0x0100_0100..=0x0110_ffff => char::from_u32(k - 0x0100_0000),
222        _ => None,
223    }
224}
225
226pub fn name_of_keysym(k: u32) -> String {
227    if let Some((n, _)) = NAMED.iter().find(|(_, v)| *v == k) {
228        return n.to_string();
229    }
230    if (0xffbe..0xffbe + 35).contains(&k) {
231        return format!("F{}", k - 0xffbe + 1);
232    }
233    match char_of_keysym(k) {
234        Some(c) => c.to_string(),
235        None => format!("0x{k:x}"),
236    }
237}
238
239// ---------------------------------------------------------------------------------------
240// Colours and text
241
242/// `#rrggbb` (or `rrggbb`) to channels.
243pub fn color(s: &str) -> Result<[u8; 3]> {
244    let h = s.trim_start_matches('#');
245    ensure!(h.len() == 6, "colour must be #rrggbb");
246    let v = u32::from_str_radix(h, 16).context("colour must be #rrggbb")?;
247    Ok([(v >> 16) as u8, (v >> 8) as u8, v as u8])
248}
249
250/// The largest text image NetGet renders, in pixels per side.
251pub const MAX_TEXT_SIDE: u32 = 4096;
252
253/// `text` in an 8×8 bitmap font, `scale` times enlarged, as a PNG; its width and height.
254pub fn render_text(
255    text: &str,
256    fg: [u8; 3],
257    bg: Option<[u8; 3]>,
258    scale: u32,
259) -> Result<(Vec<u8>, u32, u32)> {
260    use font8x8::UnicodeFonts;
261    let lines: Vec<&str> = text.split('\n').collect();
262    let cols = lines
263        .iter()
264        .map(|l| l.chars().count())
265        .max()
266        .unwrap_or(0)
267        .max(1) as u32;
268    let w = cols * 8 * scale;
269    let h = lines.len() as u32 * 8 * scale;
270    ensure!(
271        w <= MAX_TEXT_SIDE && h <= MAX_TEXT_SIDE,
272        "text renders larger than {MAX_TEXT_SIDE} pixels; shorten it or lower scale"
273    );
274    let mut img = image::RgbaImage::from_pixel(
275        w,
276        h,
277        match bg {
278            Some([r, g, b]) => image::Rgba([r, g, b, 255]),
279            None => image::Rgba([0, 0, 0, 0]),
280        },
281    );
282    for (row, line) in lines.iter().enumerate() {
283        for (col, c) in line.chars().enumerate() {
284            let glyph = font8x8::BASIC_FONTS
285                .get(c)
286                .or_else(|| font8x8::LATIN_FONTS.get(c))
287                .or_else(|| font8x8::BASIC_FONTS.get('?'))
288                .unwrap_or([0; 8]);
289            for (gy, bits) in glyph.iter().enumerate() {
290                for gx in 0..8 {
291                    if bits & (1 << gx) == 0 {
292                        continue;
293                    }
294                    for sy in 0..scale {
295                        for sx in 0..scale {
296                            let x = (col as u32 * 8 + gx) * scale + sx;
297                            let y = (row as u32 * 8 + gy as u32) * scale + sy;
298                            img.put_pixel(x, y, image::Rgba([fg[0], fg[1], fg[2], 255]));
299                        }
300                    }
301                }
302            }
303        }
304    }
305    let mut png = Vec::new();
306    img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)?;
307    Ok((png, w, h))
308}
309
310/// Bytes as the `blob` instructions of one stream.
311pub fn blobs(stream: &str, data: &[u8]) -> String {
312    let b64 = base64::engine::general_purpose::STANDARD.encode(data);
313    let mut out = String::new();
314    for chunk in b64.as_bytes().chunks(BLOB_CHUNK) {
315        out.push_str(&encode(
316            "blob",
317            &[stream, std::str::from_utf8(chunk).unwrap_or("")],
318        ));
319    }
320    out
321}
322
323pub fn unbase64(s: &str) -> Result<Vec<u8>> {
324    base64::engine::general_purpose::STANDARD
325        .decode(s)
326        .context("blob is not base64")
327}
328
329/// Guacamole status codes used here.
330pub const STATUS_SERVER_ERROR: u32 = 0x0200;
331pub const STATUS_CLIENT_UNAUTHORIZED: u32 = 0x0301;
332pub const STATUS_CLIENT_FORBIDDEN: u32 = 0x0303;