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