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    /// Wrap a stream, rewriting request targets as they pass through.
101    pub fn new(inner: T) -> Self {
102        Self {
103            inner,
104            state: State::Preface,
105            out: Vec::new(),
106            out_pos: 0,
107            line: Vec::new(),
108        }
109    }
110
111    /// Give up on framing: flush what is buffered and copy through from here.
112    fn give_up(&mut self) {
113        let line = std::mem::take(&mut self.line);
114        self.out.extend_from_slice(&line);
115        self.state = State::PassThrough;
116    }
117
118    /// Feed freshly read bytes through the state machine.
119    fn process(&mut self, mut data: &[u8]) {
120        while !data.is_empty() {
121            match self.state {
122                State::PassThrough => {
123                    self.out.extend_from_slice(data);
124                    return;
125                }
126                State::Preface => {
127                    // Held back rather than emitted, because until enough of
128                    // it has arrived to rule the preface out these bytes might
129                    // not be a request line at all.
130                    let want = H2_PREFACE.len() - self.line.len();
131                    let take = std::cmp::min(want, data.len());
132                    self.line.extend_from_slice(&data[..take]);
133
134                    if !H2_PREFACE.starts_with(&self.line) {
135                        // An ordinary request line, and one byte is usually
136                        // enough to know it. Read it as one, from the start.
137                        self.state = State::RequestLine;
138                        let mut buffered = std::mem::take(&mut self.line);
139                        buffered.extend_from_slice(&data[take..]);
140                        self.process(&buffered);
141                        return;
142                    }
143
144                    data = &data[take..];
145                    if self.line.len() == H2_PREFACE.len() {
146                        self.give_up();
147                    }
148                }
149                State::Body(remaining) => {
150                    let take = std::cmp::min(remaining, data.len() as u64) as usize;
151                    self.out.extend_from_slice(&data[..take]);
152                    data = &data[take..];
153                    let left = remaining - take as u64;
154                    self.state = if left == 0 {
155                        State::RequestLine
156                    } else {
157                        State::Body(left)
158                    };
159                }
160                State::RequestLine | State::Headers { .. } => {
161                    match data.iter().position(|&b| b == b'\n') {
162                        Some(idx) => {
163                            self.line.extend_from_slice(&data[..=idx]);
164                            data = &data[idx + 1..];
165                            self.finish_line();
166                        }
167                        None => {
168                            self.line.extend_from_slice(data);
169                            data = &[];
170                            if self.line.len() > MAX_LINE {
171                                self.give_up();
172                            }
173                        }
174                    }
175                }
176            }
177        }
178    }
179
180    /// Handle one complete line, newline included.
181    fn finish_line(&mut self) {
182        let line = std::mem::take(&mut self.line);
183
184        match self.state {
185            State::RequestLine => {
186                self.out.extend_from_slice(&normalize_request_line(&line));
187                self.state = State::Headers { content_length: 0 };
188            }
189            State::Headers { content_length } => {
190                let is_blank = matches!(line.as_slice(), b"\r\n" | b"\n");
191                if is_blank {
192                    self.out.extend_from_slice(&line);
193                    self.state = if content_length == 0 {
194                        State::RequestLine
195                    } else {
196                        State::Body(content_length)
197                    };
198                    return;
199                }
200
201                // A chunked body is framed by the body itself, which this does
202                // not read, so where the next message starts is unknown.
203                if header_is(&line, b"transfer-encoding") {
204                    self.out.extend_from_slice(&line);
205                    self.state = State::PassThrough;
206                    return;
207                }
208
209                let content_length = match header_value(&line, b"content-length") {
210                    Some(value) => match std::str::from_utf8(value)
211                        .ok()
212                        .and_then(|v| v.trim().parse::<u64>().ok())
213                    {
214                        Some(len) => len,
215                        // Unparseable framing: stop guessing.
216                        None => {
217                            self.out.extend_from_slice(&line);
218                            self.state = State::PassThrough;
219                            return;
220                        }
221                    },
222                    None => content_length,
223                };
224
225                self.out.extend_from_slice(&line);
226                self.state = State::Headers { content_length };
227            }
228            State::Preface | State::Body(_) | State::PassThrough => {
229                unreachable!("not a line state")
230            }
231        }
232    }
233}
234
235/// Whether a header line names the given (lowercase) header.
236fn header_is(line: &[u8], name: &[u8]) -> bool {
237    match line.iter().position(|&b| b == b':') {
238        Some(colon) => line[..colon].eq_ignore_ascii_case(name),
239        None => false,
240    }
241}
242
243/// The value of a header line, if it names the given (lowercase) header.
244fn header_value<'a>(line: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
245    let colon = line.iter().position(|&b| b == b':')?;
246    line[..colon]
247        .eq_ignore_ascii_case(name)
248        .then(|| &line[colon + 1..])
249}
250
251/// Percent-encode the target of a request line, leaving the rest alone.
252///
253/// A line that is not shaped like `METHOD SP TARGET SP VERSION` is returned
254/// unchanged, for hyper to reject as it sees fit.
255fn normalize_request_line(line: &[u8]) -> Vec<u8> {
256    let end = line.len() - trailing_newline_len(line);
257    let head = &line[..end];
258
259    let (Some(first), Some(last)) = (
260        head.iter().position(|&b| b == b' '),
261        head.iter().rposition(|&b| b == b' '),
262    ) else {
263        return line.to_vec();
264    };
265    if first == last {
266        return line.to_vec();
267    }
268
269    let target = &head[first + 1..last];
270    if !target.iter().copied().any(needs_encoding) {
271        return line.to_vec();
272    }
273
274    let mut out = Vec::with_capacity(line.len() + 16);
275    out.extend_from_slice(&head[..=first]);
276    for &b in target {
277        if needs_encoding(b) {
278            out.extend_from_slice(format!("%{:02X}", b).as_bytes());
279        } else {
280            out.push(b);
281        }
282    }
283    out.extend_from_slice(&head[last..]);
284    out.extend_from_slice(&line[end..]);
285    out
286}
287
288fn trailing_newline_len(line: &[u8]) -> usize {
289    if line.ends_with(b"\r\n") {
290        2
291    } else if line.ends_with(b"\n") {
292        1
293    } else {
294        0
295    }
296}
297
298impl<T: AsyncRead + Unpin> AsyncRead for LenientStream<T> {
299    fn poll_read(
300        self: Pin<&mut Self>,
301        cx: &mut Context<'_>,
302        dst: &mut ReadBuf<'_>,
303    ) -> Poll<io::Result<()>> {
304        let me = self.get_mut();
305
306        loop {
307            if me.out_pos < me.out.len() {
308                let take = std::cmp::min(dst.remaining(), me.out.len() - me.out_pos);
309                dst.put_slice(&me.out[me.out_pos..me.out_pos + take]);
310                me.out_pos += take;
311                if me.out_pos == me.out.len() {
312                    me.out.clear();
313                    me.out_pos = 0;
314                }
315                return Poll::Ready(Ok(()));
316            }
317
318            // Once framing is no longer tracked there is nothing to do but
319            // hand the reader the socket directly.
320            if matches!(me.state, State::PassThrough) && me.line.is_empty() {
321                return Pin::new(&mut me.inner).poll_read(cx, dst);
322            }
323
324            let mut scratch = [0u8; 8192];
325            let mut read = ReadBuf::new(&mut scratch);
326            match Pin::new(&mut me.inner).poll_read(cx, &mut read) {
327                Poll::Ready(Ok(())) => {
328                    let filled = read.filled();
329                    if filled.is_empty() {
330                        // End of stream. Anything half-read is still owed to
331                        // the reader, so it is not dropped silently.
332                        if !me.line.is_empty() {
333                            me.give_up();
334                            continue;
335                        }
336                        return Poll::Ready(Ok(()));
337                    }
338                    let owned = filled.to_vec();
339                    me.process(&owned);
340                }
341                Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
342                Poll::Pending => return Poll::Pending,
343            }
344        }
345    }
346}
347
348impl<T: AsyncWrite + Unpin> AsyncWrite for LenientStream<T> {
349    fn poll_write(
350        mut self: Pin<&mut Self>,
351        cx: &mut Context<'_>,
352        buf: &[u8],
353    ) -> Poll<io::Result<usize>> {
354        Pin::new(&mut self.inner).poll_write(cx, buf)
355    }
356
357    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
358        Pin::new(&mut self.inner).poll_flush(cx)
359    }
360
361    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
362        Pin::new(&mut self.inner).poll_shutdown(cx)
363    }
364}
365
366/// A [`TcpListener`](tokio::net::TcpListener) whose connections normalise
367/// request targets.
368pub struct LenientListener(pub tokio::net::TcpListener);
369
370impl axum::serve::Listener for LenientListener {
371    type Io = LenientStream<tokio::net::TcpStream>;
372    type Addr = std::net::SocketAddr;
373
374    async fn accept(&mut self) -> (Self::Io, Self::Addr) {
375        loop {
376            match self.0.accept().await {
377                Ok((stream, addr)) => return (LenientStream::new(stream), addr),
378                // Mirrors axum's own listener: a failed accept is transient,
379                // and spinning on it would burn a core.
380                Err(e) => {
381                    tracing::debug!("accept failed: {e}");
382                    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
383                }
384            }
385        }
386    }
387
388    fn local_addr(&self) -> io::Result<Self::Addr> {
389        self.0.local_addr()
390    }
391}
392
393/// The same leniency over a Unix domain socket, for `server_unix_socket`.
394///
395/// A request arriving over a socket goes through the same URI parser as one
396/// arriving over TCP, so it needs the same treatment; serving the socket with
397/// a plain `UnixListener` would make the two transports disagree about which
398/// request targets are acceptable.
399#[cfg(unix)]
400pub struct LenientUnixListener(pub tokio::net::UnixListener);
401
402#[cfg(unix)]
403impl axum::serve::Listener for LenientUnixListener {
404    type Io = LenientStream<tokio::net::UnixStream>;
405    type Addr = tokio::net::unix::SocketAddr;
406
407    async fn accept(&mut self) -> (Self::Io, Self::Addr) {
408        loop {
409            match self.0.accept().await {
410                Ok((stream, addr)) => return (LenientStream::new(stream), addr),
411                Err(e) => {
412                    tracing::debug!("accept failed: {e}");
413                    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
414                }
415            }
416        }
417    }
418
419    fn local_addr(&self) -> io::Result<Self::Addr> {
420        self.0.local_addr()
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    fn run(chunks: &[&[u8]]) -> Vec<u8> {
429        let mut s = LenientStream::new(tokio::io::empty());
430        for chunk in chunks {
431            s.process(chunk);
432        }
433        s.out
434    }
435
436    #[test]
437    fn encodes_only_what_the_uri_parser_rejects() {
438        let out = run(&[b"GET /t?select=data->>id HTTP/1.1\r\n\r\n"]);
439        assert_eq!(
440            String::from_utf8(out).unwrap(),
441            "GET /t?select=data-%3E%3Eid HTTP/1.1\r\n\r\n"
442        );
443    }
444
445    #[test]
446    fn encodes_quotes() {
447        let out = run(&[b"GET /t?a=in.(\"x\") HTTP/1.1\r\n\r\n"]);
448        assert!(String::from_utf8(out).unwrap().contains("in.(%22x%22)"));
449    }
450
451    #[test]
452    fn leaves_an_ordinary_request_untouched() {
453        let raw: &[u8] = b"GET /t?a=eq.1 HTTP/1.1\r\nHost: x\r\n\r\n";
454        assert_eq!(run(&[raw]), raw.to_vec());
455    }
456
457    #[test]
458    fn percent_escapes_already_present_are_not_re_encoded() {
459        let raw: &[u8] = b"GET /t?select=data-%3E%3Eid HTTP/1.1\r\n\r\n";
460        assert_eq!(run(&[raw]), raw.to_vec());
461    }
462
463    #[test]
464    fn a_body_is_never_rewritten() {
465        // The body is a request line character for character. It must survive.
466        let body = b"GET /evil?x=a>b HTTP/1.1";
467        let mut raw = Vec::new();
468        raw.extend_from_slice(b"POST /t HTTP/1.1\r\nContent-Length: 24\r\n\r\n");
469        raw.extend_from_slice(body);
470        let out = run(&[&raw]);
471        assert_eq!(out, raw, "body bytes were altered");
472    }
473
474    #[test]
475    fn the_request_after_a_body_is_still_rewritten() {
476        let mut raw = Vec::new();
477        raw.extend_from_slice(b"POST /t HTTP/1.1\r\nContent-Length: 2\r\n\r\nhi");
478        raw.extend_from_slice(b"GET /t?s=a->b HTTP/1.1\r\n\r\n");
479        let out = String::from_utf8(run(&[&raw])).unwrap();
480        assert!(out.contains("\r\n\r\nhi"), "body changed: {out}");
481        assert!(
482            out.contains("s=a-%3Eb"),
483            "second request not rewritten: {out}"
484        );
485    }
486
487    #[test]
488    fn a_chunked_body_stops_rewriting_rather_than_guessing() {
489        let mut raw = Vec::new();
490        raw.extend_from_slice(b"POST /t HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n");
491        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");
492        // Passed through verbatim: not rewritten, but not corrupted either.
493        assert_eq!(run(&[&raw]), raw);
494    }
495
496    #[test]
497    fn a_request_split_across_reads_is_still_rewritten() {
498        let out = run(&[b"GET /t?s=da", b"ta->>id HT", b"TP/1.1\r\n\r\n"]);
499        assert_eq!(
500            String::from_utf8(out).unwrap(),
501            "GET /t?s=data-%3E%3Eid HTTP/1.1\r\n\r\n"
502        );
503    }
504
505    #[test]
506    fn keeps_pipelined_requests_aligned() {
507        let out = String::from_utf8(run(&[
508            b"GET /a?s=x->y HTTP/1.1\r\n\r\nGET /b?s=p->q HTTP/1.1\r\n\r\n",
509        ]))
510        .unwrap();
511        assert!(out.contains("/a?s=x-%3Ey"), "{out}");
512        assert!(out.contains("/b?s=p-%3Eq"), "{out}");
513    }
514
515    #[test]
516    fn a_malformed_request_line_is_left_for_hyper_to_reject() {
517        let raw: &[u8] = b"nonsense\r\n";
518        assert_eq!(run(&[raw]), raw.to_vec());
519    }
520
521    /// HPACK sets the high bit constantly, so a frame read as a request line
522    /// is full of bytes this would otherwise percent-encode.
523    #[test]
524    fn an_http2_connection_is_passed_through_untouched() {
525        let mut raw = Vec::new();
526        raw.extend_from_slice(H2_PREFACE);
527        // A SETTINGS frame, then header-block bytes with the high bit set and
528        // a `>` and a newline among them, which is what would be mangled.
529        raw.extend_from_slice(b"\x00\x00\x00\x04\x00\x00\x00\x00\x00");
530        raw.extend_from_slice(b"\x82\x86\x84\x41\x8a > \n\xff\xfe HTTP/1.1\r\n");
531        assert_eq!(run(&[&raw]), raw);
532    }
533
534    /// The preface is decided on the bytes as they arrive, not on a whole
535    /// read landing at once.
536    #[test]
537    fn an_http2_preface_split_across_reads_is_still_recognised() {
538        let out = run(&[b"PRI * HTTP/2.0\r\n", b"\r\nSM\r\n\r\n", b"\xff\xfe->\n"]);
539        assert_eq!(
540            out,
541            [H2_PREFACE, b"\xff\xfe->\n"].concat(),
542            "an HTTP/2 connection was rewritten"
543        );
544    }
545
546    /// Holding bytes back to check for the preface must not lose them.
547    #[test]
548    fn a_request_beginning_like_the_preface_is_read_as_http1() {
549        let out = run(&[b"PRI /t?s=a->b HTTP/1.1\r\n\r\n"]);
550        assert_eq!(
551            String::from_utf8(out).unwrap(),
552            "PRI /t?s=a-%3Eb HTTP/1.1\r\n\r\n"
553        );
554    }
555
556    /// Including where it is split inside the shared prefix.
557    #[test]
558    fn a_request_beginning_like_the_preface_survives_being_split() {
559        let out = run(&[b"P", b"OST /t?s=a->b HTTP/1.1\r\n", b"\r\n"]);
560        assert_eq!(
561            String::from_utf8(out).unwrap(),
562            "POST /t?s=a-%3Eb HTTP/1.1\r\n\r\n"
563        );
564    }
565}