Skip to main content

simoom_http1/
h2c.rs

1//! HTTP/2 over cleartext: prior-knowledge preface detection and the `Upgrade: h2c` request
2//! (RFC 9113 §3.3, RFC 7540 §3.2).
3
4use crate::parse::has_token;
5use crate::{base64url_decode, Framing, Request, Version};
6
7/// The HTTP/2 connection preface.
8pub const PREFACE: &[u8; 24] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
9
10/// The `101` reply that accepts an h2c upgrade; HTTP/2 frames follow it.
11pub const SWITCHING_PROTOCOLS: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: h2c\r\n\r\n";
12
13/// What the first bytes of a connection are.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Preface {
16    /// The HTTP/2 preface: hand the connection to the HTTP/2 codec, preface included.
17    H2,
18    /// Anything else: parse as HTTP/1.
19    Http1,
20    /// Too few bytes to tell (a prefix of the preface, or nothing).
21    Partial,
22    /// The bytes start with the preface's request line, `PRI * HTTP/2.0`, but the rest of the
23    /// preface is wrong: an HTTP/2 client with a broken preface. RFC 9113 §3.4 makes that a
24    /// connection error of type PROTOCOL_ERROR, so hand it to the HTTP/2 codec, which answers
25    /// GOAWAY(PROTOCOL_ERROR) and closes, rather than to the HTTP/1 parser.
26    Invalid,
27}
28
29/// Length of `PRI * HTTP/2.0`, the request line of the preface. The `PRI` method is reserved
30/// for it (RFC 9113 §11.6), so no HTTP/1 request starts with these bytes.
31const PREFACE_LINE: usize = 14;
32
33/// Classifies the start of a connection.
34pub fn detect_preface(buf: &[u8]) -> Preface {
35    let n = buf.len().min(PREFACE.len());
36    if buf[..n] != PREFACE[..n] {
37        if n > PREFACE_LINE && buf[..PREFACE_LINE] == PREFACE[..PREFACE_LINE] {
38            Preface::Invalid
39        } else {
40            Preface::Http1
41        }
42    } else if n == PREFACE.len() {
43        Preface::H2
44    } else {
45        Preface::Partial
46    }
47}
48
49/// If `req` asks to upgrade to h2c, appends the decoded `HTTP2-Settings` payload to `settings`
50/// and returns true.
51///
52/// The request must be HTTP/1.1, have `Upgrade` listing `h2c`, `Connection` listing both
53/// `Upgrade` and `HTTP2-Settings`, exactly one `HTTP2-Settings` field holding canonical
54/// base64url whose length is a multiple of 6 bytes, and no body (a server may ignore an
55/// upgrade on a request with a body, and this one does, which keeps the switch simple).
56pub fn upgrade_settings(req: &Request<'_, '_>, settings: &mut Vec<u8>) -> bool {
57    let Some(up) = req.upgrade else { return false };
58    if req.version != Version::Http11 || req.framing != Framing::None || !has_token(up, b"h2c") {
59        return false;
60    }
61    let names_settings =
62        req.headers.iter().any(|h| h.name.eq_ignore_ascii_case(b"connection") && has_token(h.value, b"http2-settings"));
63    let mut fields = req.headers.iter().filter(|h| h.name.eq_ignore_ascii_case(b"http2-settings"));
64    let (Some(field), None) = (fields.next(), fields.next()) else { return false };
65    if !names_settings {
66        return false;
67    }
68    let start = settings.len();
69    if !base64url_decode(field.value, settings) || (settings.len() - start) % 6 != 0 {
70        settings.truncate(start);
71        return false;
72    }
73    true
74}