rustclamp 1.0.0

A minimal synchronous entrypoint for Clamp applications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
use std::any::Any;
use std::fmt;
use std::io::{BufRead, Read};
use std::net::IpAddr;
use std::sync::Arc;

use rustclamp_core::Reference;

use super::{Response, Session, error};

/// Longest accepted request line or header line, in bytes.
const MAX_LINE: u64 = 8 * 1024;
/// Most headers accepted on one request.
const MAX_HEADERS: usize = 100;
/// Largest accepted request body, in bytes, except multipart forms (see
/// [`MAX_UPLOAD`](super::MAX_UPLOAD)). Larger bodies get `413`.
pub const MAX_BODY: usize = 1024 * 1024;

/// An incoming HTTP request.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Request {
    /// The request method, such as `GET`.
    pub method: String,
    /// The request path without its query string or fragment.
    pub path: String,
    /// The raw query string, without the leading `?`.
    pub query: String,
    /// Header names (lowercased) and values, in arrival order.
    pub headers: Vec<(String, String)>,
    /// The request body.
    pub body: Vec<u8>,
    /// The address of the connecting client, when known.
    pub peer: Option<IpAddr>,
    /// Set by [`Sessions::middleware`](super::Sessions::middleware).
    pub(super) session: Option<Session>,
    /// `{name}` route segments, set when the route matches.
    pub(super) params: Vec<(String, String)>,
    /// Values from [`Router::state`](super::Router::state).
    pub(super) state: State,
    /// Values from [`Request::with_extension`], such as the signed-in user.
    pub(super) extensions: State,
    /// The declared body was over the limit and was not read.
    pub(super) too_large: bool,
    /// Names this request in logs and in `X-Request-Id` (ADR 0021).
    pub(super) reference: Minted,
    /// Whether the client lets the connection stay open for another request:
    /// HTTP/1.1 without `Connection: close`.
    pub(super) keep_alive: bool,
}

/// Values the app shares with every handler, such as its database.
#[derive(Clone, Default)]
pub(super) struct State(pub(super) Arc<Vec<Arc<dyn Any + Send + Sync>>>);

impl fmt::Debug for State {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("State").finish_non_exhaustive()
    }
}

// Requests compare by what the client sent; shared state is not part of that.
impl PartialEq for State {
    fn eq(&self, _: &Self) -> bool {
        true
    }
}

impl Eq for State {}

/// The request's [`Reference`], minted when it arrived. Like [`State`], it is
/// not part of what the client sent, so it does not affect equality.
#[derive(Clone, Copy, Debug)]
pub(super) struct Minted(pub(super) Reference);

impl PartialEq for Minted {
    fn eq(&self, _: &Self) -> bool {
        true
    }
}

impl Eq for Minted {}

impl Request {
    /// Creates a request, for example to call routes from a test. A `?` in
    /// `target` starts the query string.
    pub fn new(method: &str, target: &str) -> Self {
        let target = target.split('#').next().unwrap_or_default();
        let (path, query) = target.split_once('?').unwrap_or((target, ""));
        Self {
            method: method.into(),
            path: path.into(),
            query: query.into(),
            headers: Vec::new(),
            body: Vec::new(),
            peer: None,
            session: None,
            params: Vec::new(),
            state: State::default(),
            extensions: State::default(),
            too_large: false,
            keep_alive: false,
            reference: Minted(Reference::new()),
        }
    }

    /// The reference minted for this request: sent back in `X-Request-Id`
    /// and added to every log line written while it is handled (ADR 0021).
    /// A client's own `X-Request-Id` is never used.
    pub fn reference(&self) -> Reference {
        self.reference.0
    }

    /// A `GET` request for `target`.
    pub fn get(target: &str) -> Self {
        Self::new("GET", target)
    }

    /// A `POST` request for `target`.
    pub fn post(target: &str) -> Self {
        Self::new("POST", target)
    }

    /// Adds a header, as a client would send it.
    #[must_use]
    pub fn with_header(mut self, name: &str, value: &str) -> Self {
        self.headers.push((name.to_ascii_lowercase(), value.into()));
        self
    }

    /// Sets the body.
    #[must_use]
    pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
        self.body = body.into();
        self
    }

    /// Sets the client address.
    #[must_use]
    pub fn with_peer(mut self, peer: IpAddr) -> Self {
        self.peer = Some(peer);
        self
    }

    /// The first value of header `name`, matched case-insensitively.
    pub fn header(&self, name: &str) -> Option<&str> {
        self.headers
            .iter()
            .find(|(key, _)| key.eq_ignore_ascii_case(name))
            .map(|(_, value)| value.as_str())
    }

    /// The decoded query parameter `key`.
    pub fn query(&self, key: &str) -> Option<String> {
        field(&self.query, key)
    }

    /// The decoded field `key` of a URL-encoded or multipart form body.
    pub fn form(&self, key: &str) -> Option<String> {
        if let Some(value) = super::upload::form_field(self, key) {
            return value;
        }
        field(&String::from_utf8_lossy(&self.body), key)
    }

    /// The route segment `{name}`, such as `slug` in `/blog/{slug}`. Not decoded.
    pub fn param(&self, name: &str) -> Option<&str> {
        self.params
            .iter()
            .find(|(key, _)| key == name)
            .map(|(_, value)| value.as_str())
    }

    /// The route segment `{name}` parsed as `T`, or `None` when it is missing
    /// or does not parse. With a typed segment, `/users/{id:u64}`, the route
    /// only matches when it does.
    pub fn param_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
        self.param(name)?.parse().ok()
    }

    /// The visitor's session, when the route runs behind
    /// [`Sessions::middleware`](super::Sessions::middleware).
    pub fn session(&self) -> Option<&Session> {
        self.session.as_ref()
    }

    /// The value of type `T` the app added with
    /// [`Router::state`](super::Router::state).
    pub fn state<T: Any>(&self) -> Option<&T> {
        self.state.0.iter().find_map(|value| value.downcast_ref())
    }

    /// A copy of the request that also carries `value`, read with
    /// [`extension`](Self::extension): how middleware hands the handler what it
    /// found, such as the authenticated user, without a header a client could
    /// forge. One value per type; a second of the same type replaces the first.
    ///
    /// ```
    /// use rustclamp::web::{Request, Response, Router};
    ///
    /// struct UserId(u64);
    ///
    /// let app = Router::new()
    ///     .middleware(|request, next| next(&request.clone().with_extension(UserId(7))))
    ///     .get("/me", |request| {
    ///         Response::text(200, &request.extension::<UserId>().unwrap().0.to_string())
    ///     });
    /// assert_eq!(app.handle(&Request::get("/me")).body, b"7");
    /// ```
    #[must_use]
    pub fn with_extension<T: Any + Send + Sync>(mut self, value: T) -> Self {
        let kept = Arc::make_mut(&mut self.extensions.0);
        kept.retain(|kept| (**kept).type_id() != std::any::TypeId::of::<T>());
        kept.push(Arc::new(value));
        self
    }

    /// The value of type `T` middleware added with
    /// [`with_extension`](Self::with_extension).
    pub fn extension<T: Any>(&self) -> Option<&T> {
        self.extensions
            .0
            .iter()
            .find_map(|value| value.downcast_ref())
    }

    /// Whether the client declared a body over the limit
    /// ([`Router::body_limit`](super::Router::body_limit)). Such a body is not
    /// read, so [`body`](Self::body) is empty; the matched route answers `413`
    /// once the middleware has run.
    pub fn body_too_large(&self) -> bool {
        self.too_large
    }

    /// The value of cookie `name`.
    pub fn cookie(&self, name: &str) -> Option<&str> {
        self.headers
            .iter()
            .filter(|(key, _)| key == "cookie")
            .flat_map(|(_, value)| value.split(';'))
            .find_map(|pair| {
                let (key, value) = pair.trim().split_once('=')?;
                (key == name).then_some(value)
            })
    }
}

/// The body limit when the app set none: [`MAX_BODY`], or
/// [`MAX_UPLOAD`](super::MAX_UPLOAD) for multipart forms.
pub(super) fn default_limit(request: &Request) -> usize {
    if request
        .header("content-type")
        .is_some_and(super::upload::is_multipart)
    {
        super::MAX_UPLOAD
    } else {
        MAX_BODY
    }
}

/// Reads one request: request line, headers, then a `Content-Length` body.
/// `limit` says, from the request line and headers, how large a body to
/// accept; a larger one is left unread and the request is marked
/// [`too_large`](Request::body_too_large), for the router to answer `413` after
/// its middleware. A malformed request is answered with the returned error.
pub(super) fn parse(
    reader: &mut impl BufRead,
    peer: Option<IpAddr>,
    limit: &dyn Fn(&Request) -> usize,
) -> Result<Request, Response> {
    let line = read_line(reader)?;
    let mut parts = line.split_whitespace();
    let (Some(method), Some(target), Some(version)) = (parts.next(), parts.next(), parts.next())
    else {
        return Err(error(400));
    };
    let mut request = Request::new(method, target);
    request.peer = peer;
    loop {
        let line = read_line(reader)?;
        if line.is_empty() {
            break;
        }
        if request.headers.len() == MAX_HEADERS {
            return Err(error(431));
        }
        let (name, value) = line.split_once(':').ok_or_else(|| error(400))?;
        request = request.with_header(name.trim(), value.trim());
    }
    request.keep_alive = version == "HTTP/1.1"
        && !request
            .header("connection")
            .is_some_and(|value| value.eq_ignore_ascii_case("close"));
    // ponytail: no chunked bodies; clients that stream must send Content-Length
    if request.header("transfer-encoding").is_some() {
        return Err(error(411));
    }
    if let Some(length) = request.header("content-length") {
        let length: usize = length.parse().map_err(|_| error(400))?;
        if length > limit(&request) {
            // The unread body would be taken for the next request.
            request.too_large = true;
            request.keep_alive = false;
            return Ok(request);
        }
        let mut body = vec![0; length];
        reader.read_exact(&mut body).map_err(|_| error(400))?;
        request.body = body;
    }
    Ok(request)
}

/// One line without its line ending. Longer than [`MAX_LINE`] is `431`.
fn read_line(reader: &mut impl BufRead) -> Result<String, Response> {
    let mut line = Vec::new();
    reader
        .take(MAX_LINE + 1)
        .read_until(b'\n', &mut line)
        .map_err(|_| error(400))?;
    if line.len() as u64 > MAX_LINE {
        return Err(error(431));
    }
    let line = String::from_utf8(line).map_err(|_| error(400))?;
    Ok(line.trim_end_matches(['\r', '\n']).to_owned())
}

/// The decoded value of `key` in `a=1&b=2`-style `pairs`.
pub(super) fn field(pairs: &str, key: &str) -> Option<String> {
    pairs.split('&').find_map(|pair| {
        let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
        (decode(name) == key).then(|| decode(value))
    })
}

/// Percent-encodes `text` for a form body or query string.
pub(super) fn encode(text: &str) -> String {
    use std::fmt::Write;
    let mut out = String::with_capacity(text.len());
    for byte in text.bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                out.push(byte as char);
            }
            _ => {
                let _ = write!(out, "%{byte:02X}");
            }
        }
    }
    out
}

/// Percent-decodes `text`, reading `+` as a space. Invalid escapes stay as they are.
pub(super) fn decode(text: &str) -> String {
    let bytes = text.as_bytes();
    let mut out = Vec::with_capacity(bytes.len());
    let mut i = 0;
    while i < bytes.len() {
        match bytes[i] {
            b'+' => out.push(b' '),
            b'%' if i + 2 < bytes.len() => {
                match std::str::from_utf8(&bytes[i + 1..i + 3])
                    .ok()
                    .and_then(|hex| u8::from_str_radix(hex, 16).ok())
                {
                    Some(byte) => {
                        out.push(byte);
                        i += 2;
                    }
                    None => out.push(b'%'),
                }
            }
            byte => out.push(byte),
        }
        i += 1;
    }
    String::from_utf8_lossy(&out).into_owned()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn parse_raw(raw: &str) -> Result<Request, u16> {
        parse(&mut raw.as_bytes(), None, &default_limit).map_err(|response| response.status)
    }

    #[test]
    fn parses_line_headers_query_and_body() {
        let request = parse_raw(
            "POST /contact?ref=home%20page HTTP/1.1\r\nHost: x\r\nContent-Type: application/x-www-form-urlencoded\r\nCookie: a=1; session=abc\r\nContent-Length: 20\r\n\r\nname=Neo+C&msg=hi%21extra",
        )
        .unwrap();
        assert_eq!(
            (request.method.as_str(), request.path.as_str()),
            ("POST", "/contact")
        );
        assert_eq!(request.query("ref").as_deref(), Some("home page"));
        assert_eq!(request.header("HOST"), Some("x"));
        assert_eq!(request.cookie("session"), Some("abc"));
        assert_eq!(request.body, b"name=Neo+C&msg=hi%21");
        assert_eq!(request.form("name").as_deref(), Some("Neo C"));
        assert_eq!(request.form("msg").as_deref(), Some("hi!"));
    }

    #[test]
    fn refuses_malformed_and_oversized_requests() {
        assert_eq!(parse_raw("").unwrap_err(), 400);
        assert_eq!(parse_raw("GET /\r\n\r\n").unwrap_err(), 400);
        assert_eq!(
            parse_raw("GET / HTTP/1.1\r\nno colon\r\n\r\n").unwrap_err(),
            400
        );
        assert_eq!(
            parse_raw("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n").unwrap_err(),
            411
        );
        let big = format!(
            "POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n",
            MAX_BODY + 1
        );
        // Not an error: the router answers 413 after its middleware.
        let big = parse_raw(&big).unwrap();
        assert!(big.body_too_large() && big.body.is_empty() && !big.keep_alive);
        let small = parse(
            &mut "POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nabcd".as_bytes(),
            None,
            &|_| 3,
        )
        .unwrap();
        assert!(small.body_too_large());
        // Multipart forms may be larger, up to MAX_UPLOAD; this body is
        // missing, so it fails later with 400.
        let upload = |length| {
            format!(
                "POST / HTTP/1.1\r\nContent-Type: multipart/form-data; boundary=b\r\nContent-Length: {length}\r\n\r\n"
            )
        };
        assert_eq!(parse_raw(&upload(MAX_BODY + 1)).unwrap_err(), 400);
        assert!(
            parse_raw(&upload(super::super::MAX_UPLOAD + 1))
                .unwrap()
                .body_too_large()
        );
        let long = format!("GET /{} HTTP/1.1\r\n\r\n", "a".repeat(MAX_LINE as usize));
        assert_eq!(parse_raw(&long).unwrap_err(), 431);
        let many = format!(
            "GET / HTTP/1.1\r\n{}\r\n",
            "X: 1\r\n".repeat(MAX_HEADERS + 1)
        );
        assert_eq!(parse_raw(&many).unwrap_err(), 431);
        assert_eq!(
            parse_raw("POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\nab").unwrap_err(),
            400
        );
    }

    #[test]
    fn decode_handles_bad_escapes() {
        assert_eq!(decode("100%"), "100%");
        assert_eq!(decode("%zz%41"), "%zzA");
        assert_eq!(decode("a+b%2Bc"), "a b+c");
    }
}