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}