postrust_server/
lenient_uri.rs1use std::io;
33use std::pin::Pin;
34use std::task::{Context, Poll};
35use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
36
37fn needs_encoding(b: u8) -> bool {
48 matches!(b, b'"' | b'<' | b'>' | b'#') || b >= 0x80
49}
50
51const MAX_LINE: usize = 64 * 1024;
56
57const H2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
72
73#[derive(Debug)]
74enum State {
75 Preface,
77 RequestLine,
79 Headers { content_length: u64 },
81 Body(u64),
83 PassThrough,
85}
86
87#[derive(Debug)]
89pub struct LenientStream<T> {
90 inner: T,
91 state: State,
92 out: Vec<u8>,
94 out_pos: usize,
95 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 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 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 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 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 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 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 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
234fn 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
242fn 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
250fn 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 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 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
365pub 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 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 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 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 #[test]
492 fn an_http2_connection_is_passed_through_untouched() {
493 let mut raw = Vec::new();
494 raw.extend_from_slice(H2_PREFACE);
495 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 #[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 #[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 #[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}