//! The HTTP client over an in-memory stream: what it writes, how it reads each kind of body, its //! caps, and the wait a 429 asks for (M4a spec, section 5). Do not edit. use std::io::{Cursor, Read, Write}; use std::time::{Duration, UNIX_EPOCH}; use gatewayd::http::{Head, HttpError, MAX_BODY, MAX_HEAD, rate_limit_wait, read_head, request}; /// Reads from `input`, records what is written. struct Duplex { input: Cursor>, output: Vec, } impl Duplex { fn new(input: &[u8]) -> Duplex { Duplex { input: Cursor::new(input.to_vec()), output: Vec::new(), } } } impl Read for Duplex { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.input.read(buf) } } impl Write for Duplex { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.output.write(buf) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } fn get(response: &[u8]) -> Result<(u16, Vec), HttpError> { let mut d = Duplex::new(response); let r = request(&mut d, "GET", "a.example", "/api/v4/users/me", &[], None)?; Ok((r.head.status, r.body)) } #[test] fn the_request_is_exactly_this() { let mut d = Duplex::new(b"HTTP/1.1 201 Created\r\nContent-Length: 2\r\n\r\n{}"); let r = request( &mut d, "POST", "straylight.example", "/api/v4/posts", &[ ("Authorization", "Bearer t"), ("Content-Type", "application/json"), ], Some(b"{\"message\":\"hi\"}"), ) .unwrap(); assert_eq!( String::from_utf8(d.output).unwrap(), "POST /api/v4/posts HTTP/1.1\r\nHost: straylight.example\r\nAuthorization: Bearer t\r\n\ Content-Type: application/json\r\nContent-Length: 16\r\nConnection: close\r\n\r\n{\"message\":\"hi\"}" ); assert_eq!((r.head.status, r.body), (201, b"{}".to_vec())); } #[test] fn bodies_by_length_chunks_or_close() { assert_eq!( get(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello").unwrap(), (200, b"hello".to_vec()) ); assert_eq!( get(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6;x=y\r\n world\r\n0\r\nTrailer: z\r\n\r\n").unwrap(), (200, b"hello world".to_vec()) ); assert_eq!( get(b"HTTP/1.1 200 OK\r\ntransfer-encoding: CHUNKED\r\n\r\nA\r\n0123456789\r\n0\r\n\r\n") .unwrap() .1, b"0123456789".to_vec() ); assert_eq!( get(b"HTTP/1.0 200 OK\r\n\r\nuntil the end").unwrap(), (200, b"until the end".to_vec()) ); assert_eq!( get(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n").unwrap(), (204, Vec::new()) ); } #[test] fn headers_are_found_whatever_their_case_and_trimmed() { let mut d = Duplex::new( b"HTTP/1.1 101 Switching Protocols\r\nUPGRADE: websocket \r\nX-A: 1\r\n\r\nFRAMES", ); let head = read_head(&mut d).unwrap(); assert_eq!(head.status, 101); assert_eq!(head.header("upgrade"), Some("websocket")); assert_eq!(head.header("x-a"), Some("1")); assert_eq!(head.header("missing"), None); let mut rest = String::new(); d.read_to_string(&mut rest).unwrap(); assert_eq!( rest, "FRAMES", "read_head reads nothing past the blank line" ); } #[test] fn malformed_responses_are_errors_not_panics() { for bad in [ &b""[..], b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n", b"HTTP/2 200 OK\r\n\r\n", b"HTTP/1.1 2000 OK\r\n\r\n", b"HTTP/1.1 abc OK\r\n\r\n", b"HTTP/1.1 99 OK\r\n\r\n", b"HTTP/1.1 200 OK\r\nno colon here\r\n\r\n", b"HTTP/1.1 200 OK\r\nContent-Length: five\r\n\r\n", b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nshort", b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n", b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhelloXX0\r\n\r\n", b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhel", b"HTTP/1.1 200 OK\r\n\xff\xfe: x\r\n\r\n", ] { assert!(get(bad).is_err(), "{:?}", String::from_utf8_lossy(bad)); } } #[test] fn caps_are_checked_before_reading_or_allocating() { let mut long_head = b"HTTP/1.1 200 OK\r\nX: ".to_vec(); long_head.extend(std::iter::repeat_n(b'a', MAX_HEAD)); long_head.extend(b"\r\n\r\n"); assert!(matches!(get(&long_head), Err(HttpError::TooLarge("head")))); let huge = format!( "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", MAX_BODY + 1 ); assert!( matches!(get(huge.as_bytes()), Err(HttpError::TooLarge("body"))), "refused from the header alone" ); let huge_chunk = format!( "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n{:x}\r\n", MAX_BODY + 1 ); assert!(matches!( get(huge_chunk.as_bytes()), Err(HttpError::TooLarge("body")) )); let overflow = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nffffffffffffffffffff\r\n"; assert!(get(overflow.as_bytes()).is_err()); let mut to_close = b"HTTP/1.0 200 OK\r\n\r\n".to_vec(); to_close.extend(std::iter::repeat_n(b'b', MAX_BODY + 1)); assert!(matches!(get(&to_close), Err(HttpError::TooLarge("body")))); } fn head_with(reset: Option<&str>) -> Head { let mut headers = vec![("X-Ratelimit-Limit".to_string(), "10".to_string())]; if let Some(r) = reset { headers.push(("X-Ratelimit-Reset".to_string(), r.to_string())); } Head { status: 429, headers, } } #[test] fn a_rate_limit_is_waited_out_within_bounds() { let now = UNIX_EPOCH + Duration::from_secs(1_800_000_000); assert_eq!( rate_limit_wait(&head_with(Some("3")), now), Duration::from_secs(3) ); assert_eq!( rate_limit_wait(&head_with(Some("1800000005")), now), Duration::from_secs(5), "a Unix time" ); assert_eq!( rate_limit_wait(&head_with(Some("1799999999")), now), Duration::from_secs(1), "already past" ); assert_eq!( rate_limit_wait(&head_with(Some("0")), now), Duration::from_secs(1) ); assert_eq!( rate_limit_wait(&head_with(Some("999999")), now), Duration::from_secs(60), "capped" ); assert_eq!( rate_limit_wait(&head_with(None), now), Duration::from_secs(1) ); assert_eq!( rate_limit_wait(&head_with(Some("soon")), now), Duration::from_secs(1) ); }