Skip to main content

simoom_h3/
validate.rs

1//! Field section validation (RFC 9114 §4.2, §4.3; RFC 9110 §5.5).
2
3use simoom_qpack::Field;
4
5/// Facts learned while validating a header section.
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub struct Validated {
8    pub content_length: Option<u64>,
9    /// A 1xx response: more headers follow.
10    pub informational: bool,
11}
12
13/// Returns the value of a pseudo-header or field.
14pub fn pseudo<'a>(fields: &'a [Field], name: &[u8]) -> Option<&'a [u8]> {
15    fields.iter().find(|f| f.name == name).map(|f| &f.value[..])
16}
17
18/// RFC 9110 token characters, lowercase only (RFC 9114 §4.2).
19fn tchar_lower(c: u8) -> bool {
20    c.is_ascii_lowercase() || c.is_ascii_digit() || b"!#$%&'*+-.^_`|~".contains(&c)
21}
22
23fn valid_name(n: &[u8]) -> bool {
24    !n.is_empty() && n.iter().all(|&c| tchar_lower(c))
25}
26
27fn valid_method(m: &[u8]) -> bool {
28    !m.is_empty() && m.iter().all(|&c| tchar_lower(c.to_ascii_lowercase()))
29}
30
31fn valid_value(v: &[u8]) -> bool {
32    if let (Some(&a), Some(&b)) = (v.first(), v.last()) {
33        if a == b' ' || a == b'\t' || b == b' ' || b == b'\t' {
34            return false;
35        }
36    }
37    v.iter().all(|&c| c != 0 && c != b'\r' && c != b'\n')
38}
39
40/// Checks the regular fields; returns the content length if present.
41fn regular(fields: &[Field], start: usize) -> Result<Option<u64>, ()> {
42    let mut cl: Option<u64> = None;
43    for f in &fields[start..] {
44        if f.name.first() == Some(&b':') || !valid_name(&f.name) || !valid_value(&f.value) {
45            return Err(());
46        }
47        match &f.name[..] {
48            b"connection" | b"keep-alive" | b"proxy-connection" | b"transfer-encoding" | b"upgrade" => return Err(()),
49            b"te" if f.value != b"trailers" => return Err(()),
50            b"content-length" => {
51                if f.value.is_empty() || f.value.len() > 19 || !f.value.iter().all(u8::is_ascii_digit) {
52                    return Err(());
53                }
54                let v: u64 = std::str::from_utf8(&f.value).map_err(|_| ())?.parse().map_err(|_| ())?;
55                if cl.is_some_and(|c| c != v) {
56                    return Err(());
57                }
58                cl = Some(v);
59            }
60            _ => {}
61        }
62    }
63    Ok(cl)
64}
65
66/// Request header validation: pseudo-headers first, no duplicates, the required ones
67/// present (CONNECT has only `:method` and `:authority`; extended CONNECT adds
68/// `:protocol`, `:scheme` and `:path`).
69pub fn validate_request(fields: &[Field]) -> Result<Validated, ()> {
70    let (mut method, mut scheme, mut path, mut authority) = (None, None, None, None);
71    let mut protocol = false;
72    let mut i = 0;
73    while i < fields.len() && fields[i].name.first() == Some(&b':') {
74        let f = &fields[i];
75        i += 1;
76        if !valid_value(&f.value) {
77            return Err(());
78        }
79        let slot = match &f.name[..] {
80            b":method" => &mut method,
81            b":scheme" => &mut scheme,
82            b":path" => &mut path,
83            b":authority" => &mut authority,
84            b":protocol" => {
85                if protocol {
86                    return Err(());
87                }
88                protocol = true;
89                continue;
90            }
91            _ => return Err(()),
92        };
93        if slot.is_some() {
94            return Err(());
95        }
96        *slot = Some(&f.value[..]);
97    }
98    let method: &[u8] = method.ok_or(())?;
99    if !valid_method(method) {
100        return Err(());
101    }
102    let connect = method == b"CONNECT";
103    if connect && !protocol {
104        if scheme.is_some() || path.is_some() || authority.is_none_or(|a| a.is_empty()) {
105            return Err(());
106        }
107    } else {
108        if scheme.is_none_or(|s| s.is_empty()) || path.is_none_or(|p| p.is_empty()) {
109            return Err(());
110        }
111        if protocol && !connect {
112            return Err(());
113        }
114    }
115    let content_length = regular(fields, i)?;
116    Ok(Validated { content_length, informational: false })
117}
118
119/// Response header validation: exactly one three-digit `:status`, no other pseudo-header.
120pub fn validate_response(fields: &[Field]) -> Result<Validated, ()> {
121    let mut status = None;
122    let mut i = 0;
123    while i < fields.len() && fields[i].name.first() == Some(&b':') {
124        if fields[i].name != b":status" || status.is_some() {
125            return Err(());
126        }
127        status = Some(&fields[i].value[..]);
128        i += 1;
129    }
130    let s = status.ok_or(())?;
131    if s.len() != 3 || !s.iter().all(u8::is_ascii_digit) || s[0] == b'0' {
132        return Err(());
133    }
134    let content_length = regular(fields, i)?;
135    Ok(Validated { content_length, informational: s[0] == b'1' })
136}
137
138/// Trailers carry no pseudo-headers.
139pub fn validate_trailers(fields: &[Field]) -> Result<(), ()> {
140    regular(fields, 0).map(|_| ())
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    fn f(list: &[(&str, &str)]) -> Vec<Field> {
148        list.iter().map(|(n, v)| Field::new(n.as_bytes(), v.as_bytes())).collect()
149    }
150
151    #[test]
152    fn request_rules() {
153        let ok = f(&[(":method", "GET"), (":scheme", "https"), (":authority", "x"), (":path", "/"), ("accept", "*/*")]);
154        assert!(validate_request(&ok).is_ok());
155        let bad = [
156            f(&[(":method", "GET"), (":path", "/")]),
157            f(&[(":method", "GET"), (":scheme", "https"), (":path", "")]),
158            f(&[(":method", "GET"), (":method", "GET"), (":scheme", "h"), (":path", "/")]),
159            f(&[(":method", "GET"), ("a", "b"), (":scheme", "h"), (":path", "/")]),
160            f(&[(":method", "GET"), (":scheme", "h"), (":path", "/"), (":status", "200")]),
161            f(&[(":method", "GET"), (":scheme", "h"), (":path", "/"), ("Upper", "x")]),
162            f(&[(":method", "GET"), (":scheme", "h"), (":path", "/"), ("connection", "close")]),
163            f(&[(":method", "GET"), (":scheme", "h"), (":path", "/"), ("te", "gzip")]),
164            f(&[(":method", "GET"), (":scheme", "h"), (":path", "/"), ("x", " lead")]),
165            f(&[(":method", "GET"), (":scheme", "h"), (":path", "/"), ("x", "a\r\nb")]),
166            f(&[(":method", "GET"), (":scheme", "h"), (":path", "/"), ("content-length", "1x")]),
167            f(&[(":method", "GET"), (":scheme", "h"), (":path", "/"), ("content-length", "1"), ("content-length", "2")]),
168            f(&[(":method", "CONNECT"), (":authority", "x"), (":path", "/")]),
169            f(&[(":method", "GET"), (":protocol", "websocket"), (":scheme", "h"), (":path", "/")]),
170            f(&[(":method", "G T"), (":scheme", "h"), (":path", "/")]),
171        ];
172        for b in &bad {
173            assert!(validate_request(b).is_err(), "{b:?}");
174        }
175        assert!(validate_request(&f(&[(":method", "CONNECT"), (":authority", "x:443")])).is_ok());
176        let ws = f(&[(":method", "CONNECT"), (":protocol", "websocket"), (":scheme", "https"), (":path", "/c"), (":authority", "x")]);
177        assert!(validate_request(&ws).is_ok());
178        let cl = f(&[(":method", "POST"), (":scheme", "h"), (":path", "/"), ("content-length", "12"), ("te", "trailers")]);
179        assert_eq!(validate_request(&cl).unwrap().content_length, Some(12));
180    }
181
182    #[test]
183    fn response_and_trailer_rules() {
184        assert!(validate_response(&f(&[(":status", "200")])).is_ok());
185        assert!(validate_response(&f(&[(":status", "103")])).unwrap().informational);
186        assert!(validate_response(&f(&[(":status", "20")])).is_err());
187        assert!(validate_response(&f(&[(":status", "200"), (":status", "200")])).is_err());
188        assert!(validate_response(&f(&[(":path", "/")])).is_err());
189        assert!(validate_trailers(&f(&[("grpc-status", "0")])).is_ok());
190        assert!(validate_trailers(&f(&[(":status", "200")])).is_err());
191    }
192}