Skip to main content

postrust_server/
lenient_uri.rs

1//! Accepting the request targets PostgREST clients actually send.
2//!
3//! `select=data->>id` is ordinary PostgREST usage: it is what curl puts on the
4//! wire and what the PostgREST documentation shows. Haskell's Warp, which
5//! serves PostgREST, hands those bytes to the application unexamined.
6//!
7//! hyper builds an [`http::Uri`] from the request target, and that rejects
8//! three printable characters -- `"`, `<` and `>` -- along with everything
9//! from `0x80` up. httparse, which reads the request line first, is happy with
10//! all of them. So the request is answered `400` with an empty body before any
11//! routing, handler, or error formatting of ours runs, and there is no point
12//! above the connection where it can be caught.
13//!
14//! This adapter percent-encodes those bytes in the request target, and only
15//! there. The query string is percent-decoded when it is parsed, so nothing
16//! above this sees a difference.
17//!
18//! # Not touching anything else
19//!
20//! A request line appears only at the start of a message, and mistaking body
21//! bytes for one would corrupt the body. So the adapter follows the framing:
22//! it reads each request line, then the header block, and uses `Content-Length`
23//! to know how many body bytes to pass through before another request line can
24//! begin.
25//!
26//! The moment framing becomes something it does not model -- a chunked body, a
27//! header block too large to buffer, a request line it cannot make sense of --
28//! it stops rewriting for the remainder of the connection and copies bytes
29//! through verbatim. That degrades to exactly hyper's own behaviour. It is
30//! never left guessing whether the bytes in hand are a request line.
31
32use std::io;
33use std::pin::Pin;
34use std::task::{Context, Poll};
35use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
36
37/// Bytes that httparse accepts in a request target but `http::Uri` rejects,
38/// or reads as something other than part of the path and query.
39///
40/// Checked against http 1.4 by parsing a URI containing each byte in turn.
41///
42/// `#` is not rejected -- it is read as starting a fragment, and everything
43/// after it disappears from the query. A fragment is a client-side notion that
44/// never reaches a server, so a `#` in a request target is an ordinary
45/// character of the query: `?select=data->!@#$%^&*_d` names a JSON key that
46/// happens to contain one.
47fn needs_encoding(b: u8) -> bool {
48    matches!(b, b'"' | b'<' | b'>' | b'#') || b >= 0x80
49}
50
51/// Cap on a buffered request line or header line.
52///
53/// Beyond this the adapter stops rewriting rather than buffering without
54/// bound; hyper applies its own, smaller limits once it sees the bytes.
55const MAX_LINE: usize = 64 * 1024;
56
57/// The bytes a client sends to open an HTTP/2 connection without negotiating.
58///
59/// Nothing serves HTTP/2 here today: `axum::serve` builds on hyper-util's
60/// automatic builder, which speaks it only with `hyper-util/http2` on, and
61/// this workspace does not enable it -- so such a connection is refused
62/// before it starts.
63///
64/// Recognised anyway, because of what happens if it ever is enabled, which is
65/// the kind of thing feature unification does without anyone deciding it.
66/// Everything after this preface is binary frames, and HPACK sets the high bit
67/// constantly; read as request lines they are full of bytes this adapter would
68/// percent-encode, so the connections would be quietly corrupted rather than
69/// refused. This costs a comparison of at most one byte per connection -- no
70/// HTTP method but `PRI` shares even a first character with it.
71const H2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
72
73#[derive(Debug)]
74enum State {
75    /// At the very start of the connection, deciding whether this is HTTP/2.
76    Preface,
77    /// At the start of a message, reading the request line.
78    RequestLine,
79    /// Reading the header block that follows a request line.
80    Headers { content_length: u64 },
81    /// Copying a body through; this many bytes still to come.
82    Body(u64),
83    /// Framing is no longer modelled: copy everything, rewrite nothing.
84    PassThrough,
85}
86
87/// A connection whose request targets are normalised as they are read.
88#[derive(Debug)]
89pub struct LenientStream<T> {
90    inner: T,
91    state: State,
92    /// Bytes processed and waiting to be handed to the reader.
93    out: Vec<u8>,
94    out_pos: usize,
95    /// A request or header line still being accumulated.
96    line: Vec<u8>,
97}
98
99impl<T> LenientStream<T> {
100    pub fn new(inner: T) -> Self {
101        Self {
102            inner,
103            state: State::Preface,
104            out: Vec::new(),
105            out_pos: 0,
106            line: Vec::new(),
107        }
108    }
109
110    /// Give up on framing: flush what is buffered and copy through from here.
111    fn give_up(&mut self) {
112        let line = std::mem::take(&mut self.line);
113        self.out.extend_from_slice(&line);
114        self.state = State::PassThrough;
115    }
116
117    /// Feed freshly read bytes through the state machine.
118    fn process(&mut self, mut data: &[u8]) {
119        while !data.is_empty() {
120            match self.state {
121                State::PassThrough => {
122                    self.out.extend_from_slice(data);
123                    return;
124                }
125                State::Preface => {
126                    // Held back rather than emitted, because until enough of
127                    // it has arrived to rule the preface out these bytes might
128                    // not be a request line at all.
129                    let want = H2_PREFACE.len() - self.line.len();
130                    let take = std::cmp::min(want, data.len());
131                    self.line.extend_from_slice(&data[..take]);
132
133                    if !H2_PREFACE.starts_with(&self.line) {
134                        // An ordinary request line, and one byte is usually
135                        // enough to know it. Read it as one, from the start.
136                        self.state = State::RequestLine;
137                        let mut buffered = std::mem::take(&mut self.line);
138                        buffered.extend_from_slice(&data[take..]);
139                        self.process(&buffered);
140                        return;
141                    }
142
143                    data = &data[take..];
144                    if self.line.len() == H2_PREFACE.len() {
145                        self.give_up();
146                    }
147                }
148                State::Body(remaining) => {
149                    let take = std::cmp::min(remaining, data.len() as u64) as usize;
150                    self.out.extend_from_slice(&data[..take]);
151                    data = &data[take..];
152                    let left = remaining - take as u64;
153                    self.state = if left == 0 {
154                        State::RequestLine
155                    } else {
156                        State::Body(left)
157                    };
158                }
159                State::RequestLine | State::Headers { .. } => {
160                    match data.iter().position(|&b| b == b'\n') {
161                        Some(idx) => {
162                            self.line.extend_from_slice(&data[..=idx]);
163                            data = &data[idx + 1..];
164                            self.finish_line();
165                        }
166                        None => {
167                            self.line.extend_from_slice(data);
168                            data = &[];
169                            if self.line.len() > MAX_LINE {
170                                self.give_up();
171                            }
172                        }
173                    }
174                }
175            }
176        }
177    }
178
179    /// Handle one complete line, newline included.
180    fn finish_line(&mut self) {
181        let line = std::mem::take(&mut self.line);
182
183        match self.state {
184            State::RequestLine => {
185                self.out.extend_from_slice(&normalize_request_line(&line));
186                self.state = State::Headers { content_length: 0 };
187            }
188            State::Headers { content_length } => {
189                let is_blank = matches!(line.as_slice(), b"\r\n" | b"\n");
190                if is_blank {
191                    self.out.extend_from_slice(&line);
192                    self.state = if content_length == 0 {
193                        State::RequestLine
194                    } else {
195                        State::Body(content_length)
196                    };
197                    return;
198                }
199
200                // A chunked body is framed by the body itself, which this does
201                // not read, so where the next message starts is unknown.
202                if header_is(&line, b"transfer-encoding") {
203                    self.out.extend_from_slice(&line);
204                    self.state = State::PassThrough;
205                    return;
206                }
207
208                let content_length = match header_value(&line, b"content-length") {
209                    Some(value) => match std::str::from_utf8(value)
210                        .ok()
211                        .and_then(|v| v.trim().parse::<u64>().ok())
212                    {
213                        Some(len) => len,
214                        // Unparseable framing: stop guessing.
215                        None => {
216                            self.out.extend_from_slice(&line);
217                            self.state = State::PassThrough;
218                            return;
219                        }
220                    },
221                    None => content_length,
222                };
223
224                self.out.extend_from_slice(&line);
225                self.state = State::Headers { content_length };
226            }
227            State::Preface | State::Body(_) | State::PassThrough => {
228                unreachable!("not a line state")
229            }
230        }
231    }
232}
233
234/// Whether a header line names the given (lowercase) header.
235fn header_is(line: &[u8], name: &[u8]) -> bool {
236    match line.iter().position(|&b| b == b':') {
237        Some(colon) => line[..colon].eq_ignore_ascii_case(name),
238        None => false,
239    }
240}
241
242/// The value of a header line, if it names the given (lowercase) header.
243fn header_value<'a>(line: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
244    let colon = line.iter().position(|&b| b == b':')?;
245    line[..colon]
246        .eq_ignore_ascii_case(name)
247        .then(|| &line[colon + 1..])
248}
249
250/// Percent-encode the target of a request line, leaving the rest alone.
251///
252/// A line that is not shaped like `METHOD SP TARGET SP VERSION` is returned
253/// unchanged, for hyper to reject as it sees fit.
254fn normalize_request_line(line: &[u8]) -> Vec<u8> {
255    let end = line.len() - trailing_newline_len(line);
256    let head = &line[..end];
257
258    let (Some(first), Some(last)) = (
259        head.iter().position(|&b| b == b' '),
260        head.iter().rposition(|&b| b == b' '),
261    ) else {
262        return line.to_vec();
263    };
264    if first == last {
265        return line.to_vec();
266    }
267
268    let target = &head[first + 1..last];
269    if !target.iter().copied().any(needs_encoding) {
270        return line.to_vec();
271    }
272
273    let mut out = Vec::with_capacity(line.len() + 16);
274    out.extend_from_slice(&head[..=first]);
275    for &b in target {
276        if needs_encoding(b) {
277            out.extend_from_slice(format!("%{:02X}", b).as_bytes());
278        } else {
279            out.push(b);
280        }
281    }
282    out.extend_from_slice(&head[last..]);
283    out.extend_from_slice(&line[end..]);
284    out
285}
286
287fn trailing_newline_len(line: &[u8]) -> usize {
288    if line.ends_with(b"\r\n") {
289        2
290    } else if line.ends_with(b"\n") {
291        1
292    } else {
293        0
294    }
295}
296
297impl<T: AsyncRead + Unpin> AsyncRead for LenientStream<T> {
298    fn poll_read(
299        self: Pin<&mut Self>,
300        cx: &mut Context<'_>,
301        dst: &mut ReadBuf<'_>,
302    ) -> Poll<io::Result<()>> {
303        let me = self.get_mut();
304
305        loop {
306            if me.out_pos < me.out.len() {
307                let take = std::cmp::min(dst.remaining(), me.out.len() - me.out_pos);
308                dst.put_slice(&me.out[me.out_pos..me.out_pos + take]);
309                me.out_pos += take;
310                if me.out_pos == me.out.len() {
311                    me.out.clear();
312                    me.out_pos = 0;
313                }
314                return Poll::Ready(Ok(()));
315            }
316
317            // Once framing is no longer tracked there is nothing to do but
318            // hand the reader the socket directly.
319            if matches!(me.state, State::PassThrough) && me.line.is_empty() {
320                return Pin::new(&mut me.inner).poll_read(cx, dst);
321            }
322
323            let mut scratch = [0u8; 8192];
324            let mut read = ReadBuf::new(&mut scratch);
325            match Pin::new(&mut me.inner).poll_read(cx, &mut read) {
326                Poll::Ready(Ok(())) => {
327                    let filled = read.filled();
328                    if filled.is_empty() {
329                        // End of stream. Anything half-read is still owed to
330                        // the reader, so it is not dropped silently.
331                        if !me.line.is_empty() {
332                            me.give_up();
333                            continue;
334                        }
335                        return Poll::Ready(Ok(()));
336                    }
337                    let owned = filled.to_vec();
338                    me.process(&owned);
339                }
340                Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
341                Poll::Pending => return Poll::Pending,
342            }
343        }
344    }
345}
346
347impl<T: AsyncWrite + Unpin> AsyncWrite for LenientStream<T> {
348    fn poll_write(
349        mut self: Pin<&mut Self>,
350        cx: &mut Context<'_>,
351        buf: &[u8],
352    ) -> Poll<io::Result<usize>> {
353        Pin::new(&mut self.inner).poll_write(cx, buf)
354    }
355
356    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
357        Pin::new(&mut self.inner).poll_flush(cx)
358    }
359
360    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
361        Pin::new(&mut self.inner).poll_shutdown(cx)
362    }
363}
364
365/// A [`TcpListener`](tokio::net::TcpListener) whose connections normalise
366/// request targets.
367pub struct LenientListener(pub tokio::net::TcpListener);
368
369impl axum::serve::Listener for LenientListener {
370    type Io = LenientStream<tokio::net::TcpStream>;
371    type Addr = std::net::SocketAddr;
372
373    async fn accept(&mut self) -> (Self::Io, Self::Addr) {
374        loop {
375            match self.0.accept().await {
376                Ok((stream, addr)) => return (LenientStream::new(stream), addr),
377                // Mirrors axum's own listener: a failed accept is transient,
378                // and spinning on it would burn a core.
379                Err(e) => {
380                    tracing::debug!("accept failed: {e}");
381                    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
382                }
383            }
384        }
385    }
386
387    fn local_addr(&self) -> io::Result<Self::Addr> {
388        self.0.local_addr()
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    fn run(chunks: &[&[u8]]) -> Vec<u8> {
397        let mut s = LenientStream::new(tokio::io::empty());
398        for chunk in chunks {
399            s.process(chunk);
400        }
401        s.out
402    }
403
404    #[test]
405    fn encodes_only_what_the_uri_parser_rejects() {
406        let out = run(&[b"GET /t?select=data->>id HTTP/1.1\r\n\r\n"]);
407        assert_eq!(
408            String::from_utf8(out).unwrap(),
409            "GET /t?select=data-%3E%3Eid HTTP/1.1\r\n\r\n"
410        );
411    }
412
413    #[test]
414    fn encodes_quotes() {
415        let out = run(&[b"GET /t?a=in.(\"x\") HTTP/1.1\r\n\r\n"]);
416        assert!(String::from_utf8(out).unwrap().contains("in.(%22x%22)"));
417    }
418
419    #[test]
420    fn leaves_an_ordinary_request_untouched() {
421        let raw: &[u8] = b"GET /t?a=eq.1 HTTP/1.1\r\nHost: x\r\n\r\n";
422        assert_eq!(run(&[raw]), raw.to_vec());
423    }
424
425    #[test]
426    fn percent_escapes_already_present_are_not_re_encoded() {
427        let raw: &[u8] = b"GET /t?select=data-%3E%3Eid HTTP/1.1\r\n\r\n";
428        assert_eq!(run(&[raw]), raw.to_vec());
429    }
430
431    #[test]
432    fn a_body_is_never_rewritten() {
433        // The body is a request line character for character. It must survive.
434        let body = b"GET /evil?x=a>b HTTP/1.1";
435        let mut raw = Vec::new();
436        raw.extend_from_slice(b"POST /t HTTP/1.1\r\nContent-Length: 24\r\n\r\n");
437        raw.extend_from_slice(body);
438        let out = run(&[&raw]);
439        assert_eq!(out, raw, "body bytes were altered");
440    }
441
442    #[test]
443    fn the_request_after_a_body_is_still_rewritten() {
444        let mut raw = Vec::new();
445        raw.extend_from_slice(b"POST /t HTTP/1.1\r\nContent-Length: 2\r\n\r\nhi");
446        raw.extend_from_slice(b"GET /t?s=a->b HTTP/1.1\r\n\r\n");
447        let out = String::from_utf8(run(&[&raw])).unwrap();
448        assert!(out.contains("\r\n\r\nhi"), "body changed: {out}");
449        assert!(
450            out.contains("s=a-%3Eb"),
451            "second request not rewritten: {out}"
452        );
453    }
454
455    #[test]
456    fn a_chunked_body_stops_rewriting_rather_than_guessing() {
457        let mut raw = Vec::new();
458        raw.extend_from_slice(b"POST /t HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n");
459        raw.extend_from_slice(b"2\r\nhi\r\n0\r\n\r\nGET /t?s=a->b HTTP/1.1\r\n\r\n");
460        // Passed through verbatim: not rewritten, but not corrupted either.
461        assert_eq!(run(&[&raw]), raw);
462    }
463
464    #[test]
465    fn a_request_split_across_reads_is_still_rewritten() {
466        let out = run(&[b"GET /t?s=da", b"ta->>id HT", b"TP/1.1\r\n\r\n"]);
467        assert_eq!(
468            String::from_utf8(out).unwrap(),
469            "GET /t?s=data-%3E%3Eid HTTP/1.1\r\n\r\n"
470        );
471    }
472
473    #[test]
474    fn keeps_pipelined_requests_aligned() {
475        let out = String::from_utf8(run(&[
476            b"GET /a?s=x->y HTTP/1.1\r\n\r\nGET /b?s=p->q HTTP/1.1\r\n\r\n",
477        ]))
478        .unwrap();
479        assert!(out.contains("/a?s=x-%3Ey"), "{out}");
480        assert!(out.contains("/b?s=p-%3Eq"), "{out}");
481    }
482
483    #[test]
484    fn a_malformed_request_line_is_left_for_hyper_to_reject() {
485        let raw: &[u8] = b"nonsense\r\n";
486        assert_eq!(run(&[raw]), raw.to_vec());
487    }
488
489    /// HPACK sets the high bit constantly, so a frame read as a request line
490    /// is full of bytes this would otherwise percent-encode.
491    #[test]
492    fn an_http2_connection_is_passed_through_untouched() {
493        let mut raw = Vec::new();
494        raw.extend_from_slice(H2_PREFACE);
495        // A SETTINGS frame, then header-block bytes with the high bit set and
496        // a `>` and a newline among them, which is what would be mangled.
497        raw.extend_from_slice(b"\x00\x00\x00\x04\x00\x00\x00\x00\x00");
498        raw.extend_from_slice(b"\x82\x86\x84\x41\x8a > \n\xff\xfe HTTP/1.1\r\n");
499        assert_eq!(run(&[&raw]), raw);
500    }
501
502    /// The preface is decided on the bytes as they arrive, not on a whole
503    /// read landing at once.
504    #[test]
505    fn an_http2_preface_split_across_reads_is_still_recognised() {
506        let out = run(&[b"PRI * HTTP/2.0\r\n", b"\r\nSM\r\n\r\n", b"\xff\xfe->\n"]);
507        assert_eq!(
508            out,
509            [H2_PREFACE, b"\xff\xfe->\n"].concat(),
510            "an HTTP/2 connection was rewritten"
511        );
512    }
513
514    /// Holding bytes back to check for the preface must not lose them.
515    #[test]
516    fn a_request_beginning_like_the_preface_is_read_as_http1() {
517        let out = run(&[b"PRI /t?s=a->b HTTP/1.1\r\n\r\n"]);
518        assert_eq!(
519            String::from_utf8(out).unwrap(),
520            "PRI /t?s=a-%3Eb HTTP/1.1\r\n\r\n"
521        );
522    }
523
524    /// Including where it is split inside the shared prefix.
525    #[test]
526    fn a_request_beginning_like_the_preface_survives_being_split() {
527        let out = run(&[b"P", b"OST /t?s=a->b HTTP/1.1\r\n", b"\r\n"]);
528        assert_eq!(
529            String::from_utf8(out).unwrap(),
530            "POST /t?s=a-%3Eb HTTP/1.1\r\n\r\n"
531        );
532    }
533}