Benjamin Jacobs: 1 Add proxy protocol support 7 files changed, 646 insertions(+), 14 deletions(-)
Welcome! Honestly it's just nice to know that anyone else out there is using it and cares. My personal enthusiasm for Gemini has waned since I wrote Stargazer. But it's the most substantial project I've created that I can share in the open (not for work) and I'm proud of it. Though it's far from perfect. If I had a lot more time, energy, and will, I'd make some substantial changes. I'm not really happy with the config layout, and I think the current way routes are defined can be unintuitive and confusing. Maybe one day I'll redo it and there'll be a Stargazer 2.0 with a new config syntax!
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~zethra/public-inbox/patches/54349/mbox | git am -3Learn more about email & git
--- Cargo.lock | 134 ++++++++++- Cargo.toml | 5 + doc/stargazer-ini.scd | 9 + doc/stargazer.1.txt | 2 +- doc/stargazer.ini.5.txt | 12 +- src/config.rs | 6 + src/main.rs | 492 +++++++++++++++++++++++++++++++++++++++- 7 files changed, 646 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de04070..4fdec0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,7 +41,7 @@ checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.60", "synstructure", ] @@ -53,7 +53,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.60", ] [[package]] @@ -271,6 +271,18 @@ dependencies = [ "serde", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + [[package]] name = "cc" version = "1.0.96" @@ -430,7 +442,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.60", ] [[package]] @@ -442,6 +454,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "errno" version = "0.3.8" @@ -875,6 +893,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.81" @@ -884,6 +911,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proxy-protocol" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e50c72c21c738f5c5f350cc33640aee30bf7cd20f9d9da20ed41bce2671d532" +dependencies = [ + "bytes", + "snafu", +] + [[package]] name = "quote" version = "1.0.36" @@ -893,6 +930,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rcgen" version = "0.13.1" @@ -1067,7 +1134,7 @@ checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.60", ] [[package]] @@ -1121,6 +1188,27 @@ dependencies = [ "autocfg", ] +[[package]] +name = "snafu" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "socket2" version = "0.4.10" @@ -1174,6 +1262,8 @@ dependencies = [ "mime_guess", "once_cell", "percent-encoding", + "proxy-protocol", + "rand", "rcgen", "regex", "rust-ini", @@ -1194,6 +1284,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.60" @@ -1213,7 +1314,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.60", ] [[package]] @@ -1233,7 +1334,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.60", ] [[package]] @@ -1552,6 +1653,27 @@ dependencies = [ "time", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 22779c3..c2cedcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ mime_guess = "2.0.4" socket2 = "0.5.7" percent-encoding = "2.3.1" uriparse = "0.6" +proxy-protocol = "0.5.0" anyhow = "1.0.82" once_cell = "1.19.0" @@ -89,3 +90,7 @@ features = ["std"] [profile.release] lto = "fat" codegen-units = 1 + +[dev-dependencies] +rand = "0.8.5" + diff --git a/doc/stargazer-ini.scd b/doc/stargazer-ini.scd index 2c23867..c03cfa4 100644 --- a/doc/stargazer-ini.scd +++ b/doc/stargazer-ini.scd @@ -81,6 +81,15 @@ that *stargazer* will server requests to. At least one route must be specified. default. *Warning*, if this is set, large files and cgi scripts may be cut off before their response finishes. +*proxy-protocol* + Whether or not to expect a proxy protocol header before the tls connection. + It is generally used with a reverse proxy in situations where you need to + know the real IP address of the client. Off by default. + + See the formal specification of proxy protocol: + + http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + ## TLS KEYS The following keys are accepted under the *[:tls]* section: diff --git a/doc/stargazer.1.txt b/doc/stargazer.1.txt index 1f24b10..4a190f0 100644 --- a/doc/stargazer.1.txt +++ b/doc/stargazer.1.txt @@ -51,4 +51,4 @@ AUTHORS bugs/patches can be submitted by email to ~zethra/public-inβ box@lists.sr.ht with the prefix stargazer. - 2024-08-03 stargazer(1) + 2024-08-05 stargazer(1) diff --git a/doc/stargazer.ini.5.txt b/doc/stargazer.ini.5.txt index 826bff3..fa5c67e 100644 --- a/doc/stargazer.ini.5.txt +++ b/doc/stargazer.ini.5.txt @@ -83,6 +83,16 @@ CONFIGURATION KEYS Warning, if this is set, large files and cgi scripts may be cut off before their response finishes. + proxy-protocol + Whether or not to expect a proxy protocol header before the + tls connection. It is designed for use with a reverse proxy + in situations where you need to know the IP address of the + client. Off by default. + + See the formal specification of proxy protocol: + + http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + TLS KEYS The following keys are accepted under the [:tls] section: @@ -410,4 +420,4 @@ AUTHORS bugs/patches can be submitted by email to ~zethra/public-inβ box@lists.sr.ht with the prefix stargazer. - 2024-08-03 stargazer.ini(5) + 2024-08-05 stargazer.ini(5) diff --git a/src/config.rs b/src/config.rs index 7f1196b..5d5cc25 100644 --- a/src/config.rs +++ b/src/config.rs @@ -60,6 +60,8 @@ pub struct Config { pub cert_lifetime: u64, /// Should stargazer log full or partial IPs or not at all pub ip_log: IpLogAmount, + /// Should stargazer expect a proxy protocol header + pub proxy_protocol: bool, } #[derive(Debug, Clone, Copy)] @@ -122,6 +124,8 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> { "log-ip and log-ip-partial can't both be turned on at once" ), }; + + let proxy_protocol = general.remove_yn("proxy-protocol"); check_section_empty("general", general)?; let tls_section = conf @@ -332,6 +336,7 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> { regen_certs, cert_lifetime, ip_log, + proxy_protocol, }) })(); res.with_context(|| { @@ -381,6 +386,7 @@ pub fn dev_config() -> Result<Config> { regen_certs: true, cert_lifetime: 0, ip_log: IpLogAmount::Full, + proxy_protocol: true, }; fs::create_dir_all(&conf.store).with_context(|| { format!( diff --git a/src/main.rs b/src/main.rs index b0fa9ba..a4eca13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,12 +29,14 @@ use async_channel::{Receiver, Sender}; use async_executor::Executor; use async_io::Timer; use async_net::{SocketAddrV6, TcpListener, TcpStream}; +use bstr::ByteSlice; use cgi::{serve_cgi, serve_scgi}; use futures_lite::*; use futures_rustls::{server::TlsStream, TlsAcceptor}; use get_file::get_file; use log::{debug, error}; use once_cell::sync::{Lazy, OnceCell}; +use proxy_protocol::ProxyHeader; use router::{route, Request, Route}; use std::clone::Clone; use std::convert::TryFrom; @@ -229,20 +231,157 @@ async fn exit_on_sig() -> Result<()> { std::process::exit(0); } -#[cfg(not(unix))] -async fn exit_on_sig() -> Result<()> { - Ok(future::pending::<()>().await) +/// A Reader which can peek into the stream without changing the cursor. +/// +/// This is used with TcpStream and str so parse_proxy_protocol can be tested +trait AsyncPeek { + async fn peek(&mut self, buf: &mut [u8]) -> io::Result<usize>; +} + +impl<'a> AsyncPeek for &'a [u8] { + async fn peek(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let to_read = buf.len().min(self.len()); + buf[..to_read].copy_from_slice(&self[..to_read]); + Ok(to_read) + } +} + +impl AsyncPeek for TcpStream { + async fn peek(&mut self, buf: &mut [u8]) -> io::Result<usize> { + TcpStream::peek(self, buf).await + } +} + +/// Parse the proxy protocol given a peek buffer and a stream. +/// +/// It is separated out from handle_proxy_protocol for testing purposes. +async fn parse_proxy_protocol<R: AsyncReadExt + AsyncPeek + Unpin>( + stream: &mut R, +) -> anyhow::Result<Option<ProxyHeader>> { + let mut peek = [0u8; 12]; + let n = stream.peek(&mut peek).await?; + let peek = &peek[..n]; + + if peek.starts_with(b"PROXY ") { + // V1 - Read until \r\n + let mut buf: Vec<u8> = Vec::new(); + loop { + // Peek in blocks of 64 bytes, and read only until the \n. + let mut peek = [0u8; 64]; + let n = stream.peek(&mut peek).await?; + let peek = &mut peek[..n]; + let (found, i) = match peek.find(b"\n") { + Some(i) => (true, i + 1), + None => (false, n), + }; + let mut peek = &mut peek[..i]; + stream.read_exact(&mut peek).await?; + buf.extend_from_slice(peek); + if found { + break; + } + } + let mut buf = buf.as_slice(); + + Ok(Some(proxy_protocol::parse(&mut buf)?)) + } else if peek.starts_with(&[ + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, + ]) { + // V2 - Read header length bytes after the first 16 at [14..16] + + let mut header = [0u8; 16]; + stream.read_exact(&mut header).await?; + let len = u16::from_be_bytes([header[14], header[15]]); + + let mut buf = vec![0u8; 16 + len as usize]; + let buf_slice = buf.as_mut_slice(); + buf_slice[..16].copy_from_slice(&header); + stream.read_exact(&mut buf_slice[16..]).await?; + let mut buf = buf.as_slice(); + + Ok(Some(proxy_protocol::parse(&mut buf)?)) + } else { + Ok(None) + } +} + +/// Try to parse and apply a proxy protocol header. +/// +/// * If the header is not found, then no data is read from the stream and no +/// action is taken. +/// * If the header is found: `remote_addr` will be swapped with the header's +/// source addr. +/// * If the header is found, but contains invalid data: no action is taken. +/// * If the header is found, but could not be parsed: an error is returned and +/// the connection should be closed. +async fn handle_proxy_protocol( + stream: &mut TcpStream, + remote_addr: &mut SocketAddr, +) -> anyhow::Result<()> { + use proxy_protocol::{ + version1::ProxyAddresses as ProxyAddrV1, + version2::ProxyAddresses as ProxyAddrV2, + }; + // proxy-protocol doesn't support async streams, so in order to parse the + // proxy protocol, we need to pre-read the header before passing it in + // without reading into tls data. + // + // First, check if there is a proxy protocol header with the first 12 bytes. + // Second, read the whole proxy protocol header. + // Third, pass the raw data into proxy-protocol to get parsed. + + match parse_proxy_protocol(stream).await? { + Some(ProxyHeader::Version1 { addresses }) => match addresses { + ProxyAddrV1::Ipv4 { + source, + destination: _, + } => *remote_addr = SocketAddr::V4(source), + ProxyAddrV1::Ipv6 { + source, + destination: _, + } => *remote_addr = SocketAddr::V6(source), + _ => {} + }, + Some(ProxyHeader::Version2 { + command: _, + transport_protocol: _, + addresses, + }) => match addresses { + ProxyAddrV2::Ipv4 { + source, + destination: _, + } => *remote_addr = SocketAddr::V4(source), + ProxyAddrV2::Ipv6 { + source, + destination: _, + } => *remote_addr = SocketAddr::V6(source), + _ => {} + }, + _ => {} + } + Ok(()) } // This is a separate function for ease of error handling async fn start_task( - stream: TcpStream, + mut stream: TcpStream, acceptor: TlsAcceptor, - remote_addr: SocketAddr, + mut remote_addr: SocketAddr, server_port: u16, ) { use anyhow::Context; - // Accept tsl connection + + if CONF.proxy_protocol { + if let Err(e) = handle_proxy_protocol(&mut stream, &mut remote_addr) + .await + .context("Error parsing proxy protocol") + { + debug!("{:#}", e); + return; + } + } + + // Accept tls connection match acceptor .accept(stream) .await @@ -593,3 +732,344 @@ impl From<GemError> for ErrorLogInfo { } } } + +#[cfg(test)] +mod test { + use std::time::{Duration, Instant}; + + use super::*; + + use async_net::SocketAddrV4; + use proxy_protocol::{ + version1::ProxyAddresses as ProxyAddrV1, + version2::{ + ProxyAddresses as ProxyAddrV2, ProxyCommand, ProxyTransportProtocol, + }, + ParseError, ProxyHeader, + }; + use rand::{Rng, RngCore}; + + fn parse_proxy(body: &[u8]) -> anyhow::Result<Option<ProxyHeader>> { + parse_proxy_len(body).0 + } + fn parse_proxy_len( + body: &[u8], + ) -> (anyhow::Result<Option<ProxyHeader>>, usize) { + async_io::block_on(async { + let mut stream = body; + let output = parse_proxy_protocol(&mut stream).await; + (output, body.len() - stream.len()) + }) + } + + fn random_u8() -> u8 { + rand::random() + } + #[inline] + fn random_bool() -> bool { + rand::random() + } + + fn random_addr_v4() -> SocketAddrV4 { + let mut rng = rand::thread_rng(); + let addr: (u8, u8, u8, u8) = rng.gen(); + SocketAddrV4::new( + Ipv4Addr::new(addr.0, addr.1, addr.2, addr.3), + rng.gen(), + ) + } + fn random_addr_v6() -> SocketAddrV6 { + let mut rng = rand::thread_rng(); + let addr: (u16, u16, u16, u16, u16, u16, u16, u16) = rng.gen(); + SocketAddrV6::new( + Ipv6Addr::new( + addr.0, addr.1, addr.2, addr.3, addr.4, addr.5, addr.6, addr.7, + ), + rng.gen(), + 0, + 0, + ) + } + + fn random_header() -> ProxyHeader { + if random_bool() { + let addresses = if random_bool() { + ProxyAddrV1::Ipv4 { + source: random_addr_v4(), + destination: random_addr_v4(), + } + } else { + ProxyAddrV1::Ipv6 { + source: random_addr_v6(), + destination: random_addr_v6(), + } + }; + ProxyHeader::Version1 { addresses } + } else { + let addresses = match random_u8() & 0b11 { + 0 => ProxyAddrV2::Unspec, + 1 => ProxyAddrV2::Ipv4 { + source: random_addr_v4(), + destination: random_addr_v4(), + }, + 2 => ProxyAddrV2::Ipv6 { + source: random_addr_v6(), + destination: random_addr_v6(), + }, + 3 => { + let mut source = [0u8; 108]; + let mut destination = [0u8; 108]; + let mut rng = rand::thread_rng(); + rng.fill_bytes(&mut source); + rng.fill_bytes(&mut destination); + ProxyAddrV2::Unix { + source, + destination, + } + } + _ => unreachable!(), + }; + ProxyHeader::Version2 { + command: match random_bool() { + true => ProxyCommand::Local, + false => ProxyCommand::Proxy, + }, + transport_protocol: match random_u8() & 0b11 { + 0 => ProxyTransportProtocol::Unspec, + 1 => ProxyTransportProtocol::Stream, + 2 => ProxyTransportProtocol::Datagram, + 3 => ProxyTransportProtocol::Stream, + _ => unreachable!(), + }, + addresses, + } + } + } + + /// Perform a simple check that v1 headers parse properly + #[test] + fn test_proxy_proto_v1() { + assert_eq!(parse_proxy(b"No header here").unwrap(), None); + + let hdr = ProxyHeader::Version1 { + addresses: ProxyAddrV1::Ipv4 { + source: random_addr_v4(), + destination: random_addr_v4(), + }, + }; + let data = proxy_protocol::encode(hdr.clone()).unwrap(); + assert_eq!(parse_proxy(&data).unwrap(), Some(hdr)); + + let hdr = ProxyHeader::Version1 { + addresses: ProxyAddrV1::Ipv6 { + source: random_addr_v6(), + destination: random_addr_v6(), + }, + }; + let data = proxy_protocol::encode(hdr.clone()).unwrap(); + assert_eq!(parse_proxy(&data).unwrap(), Some(hdr)); + } + + /// Perform a simple check that v2 headers parse properly + #[test] + fn test_proxy_proto_v2() { + let hdr = ProxyHeader::Version2 { + command: ProxyCommand::Proxy, + transport_protocol: ProxyTransportProtocol::Stream, + addresses: ProxyAddrV2::Ipv4 { + source: random_addr_v4(), + destination: random_addr_v4(), + }, + }; + let data = proxy_protocol::encode(hdr.clone()).unwrap(); + assert_eq!(parse_proxy(&data).unwrap(), Some(hdr)); + + let hdr = ProxyHeader::Version2 { + command: ProxyCommand::Proxy, + transport_protocol: ProxyTransportProtocol::Stream, + addresses: ProxyAddrV2::Ipv6 { + source: random_addr_v6(), + destination: random_addr_v6(), + }, + }; + let data = proxy_protocol::encode(hdr.clone()).unwrap(); + assert_eq!(parse_proxy(&data).unwrap(), Some(hdr)); + } + + /// Check against as many as possible inputs to make sure that all valid headers do not error + /// and all invalid headers match the error received by proxy_protocol + /// + /// This will always run for `duration` seconds + #[test] + fn test_proxy_proto_fuzz() { + let duration = Duration::from_secs(15); + let deadline = Instant::now() + duration; + + let mut count = 0; + let mut v1_count = 0; + let mut v2_count = 0; + + let mut rng = rand::thread_rng(); + + while Instant::now() < deadline { + // Create a random header and verify that it parses correctly + let header = random_header(); + let mut data = proxy_protocol::encode(header.clone()).unwrap(); + let len = data.len(); + let mut extra = [0u8; 64]; + rng.fill_bytes(&mut extra); + data.extend_from_slice(&extra); + // We add a newline here because otherwise, parser will wait indefinitely until a + // newline. + data.extend_from_slice(b"\n"); + + let mut data = data.to_vec(); + count += 1; + + assert_eq!(parse_proxy(&data).unwrap(), Some(header)); + + let is_ascii = data.starts_with(b"PROXY "); + if is_ascii { + v1_count += 1 + len * 8; + } else { + v2_count += 1 + len * 8; + } + + // perform N permutations, corrupting the header 1 random bit at a time where N is the + // number of bits in the header + for _ in 0..len * 8 { + let i = random_u8() as usize % len; + let o = random_u8() & 0b111; + data[i] = data[i] ^ (1 << o); + + count += 1; + + let genbuf = || { + if is_ascii { + data[..len] + .iter() + .map(|c| match *c >= 0x20 && *c <= 0x7F { + true => (*c as char).to_string(), + false => format!("\\{c:X}"), + }) + .collect::<Vec<String>>() + .join("") + } else { + data[..len] + .iter() + .map(|c| format!("{c:02X}")) + .collect::<Vec<_>>() + .join(" ") + } + }; + + let (output, read) = parse_proxy_len(&data); + if let Err(e) = &output { + if let Some(_) = e.downcast_ref::<io::Error>() { + // This is an error with the length of the data. + // If we reach this point, then the input is absolutely corrupted and we + // cannot continue. + continue; + } + } + if let Ok(None) = &output { + if read != 0 { + let buf = genbuf(); + panic!("{buf}\nThere no header, but {read} bytes were read") + } + } + + let mut expected_slice = data.as_slice(); + let expected = match proxy_protocol::parse(&mut expected_slice) + { + Ok(output) => Ok(Some(output)), + Err(ParseError::NotProxyHeader) => Ok(None), + Err(err) => Err(err), + }; + let expected_read = data.len() - expected_slice.len(); + + // Compare the outputs of the expected behaviour and the actual behaviour. + // There are some edge cases because of the way we parse the data + match (output, expected) { + (Ok(a), Ok(b)) => { + if a != b { + let buf = genbuf(); + assert_eq!(a, b, "{buf}"); + } + if read != expected_read { + let buf = genbuf(); + assert_eq!(read, expected_read, "{buf}"); + } + } + (Err(a), Err(b)) => { + // This is where results may differ. What matters however is that both + // produce the same type of error + match (a.downcast::<ParseError>().unwrap(), b) { + (ParseError::NotProxyHeader, _) => { + let buf = genbuf(); + panic!("{buf}\nIt should not be possible for a to return a NotProxyHeader error") + } + ( + ParseError::InvalidVersion { version: a }, + ParseError::InvalidVersion { version: b }, + ) => { + let buf = genbuf(); + assert_eq!( + ParseError::InvalidVersion { version: a } + .to_string(), + ParseError::InvalidVersion { version: b } + .to_string(), + "{buf}" + ); + } + ( + ParseError::Version1 { source: _ }, + ParseError::Version1 { source: _ }, + ) => {} + ( + ParseError::Version2 { source: _ }, + ParseError::Version2 { source: _ }, + ) => {} + (a, b) => { + let buf = genbuf(); + assert_eq!( + a.to_string(), + b.to_string(), + "{buf}" + ); + } + } + } + (Err(a), b) => match a.downcast::<ParseError>().unwrap() { + ParseError::Version1 { source } => { + match source { + // (V1 error - unexpected eof, an okay result) + // This is a rare occurrence, but it is possible that a technically + // valid header gets read as invalid because there is a \n hidden + // inside the header. This is okay so should not panic. + proxy_protocol::version1::ParseError::UnexpectedEof => {}, + a => { + let buf = genbuf(); + panic!("{buf}\n{a:?} != {b:?}") + } + } + } + a => { + let buf = genbuf(); + panic!("{buf}\n{a:?} != {b:?}") + } + }, + (a, b) => { + let buf = genbuf(); + panic!("{buf}\n{a:?} != {b:?}") + } + } + } + } + println!( + "Checked {count} headers ({}% v1 headers and {}% v2 headers)", + (v1_count as f64 / count as f64 * 100.0).round(), + (v2_count as f64 / count as f64 * 100.0).round() + ) + } +} -- 2.30.2
Thanks! I appreciate you taking feedback and making changes. To git@git.sr.ht:~zethra/stargazer 5e4baa2..57a58ab main -> main If you have the inclination, it'd be good to add a functional test to ./stargazer/scripts/gemini-diagnostics to ensure that it doesn't break in some other way later. But this isn't required. I may add one myself at some point. I plan to cut a new release containing this feature within the next week, when I have time.
builds.sr.ht <builds@sr.ht>stargazer/patches: SUCCESS in 3m18s [Add proxy protocol support][0] v2 from [Benjamin Jacobs][1] [0]: https://lists.sr.ht/~zethra/public-inbox/patches/54349 [1]: mailto:bj@benjaminja.com β #1297208 SUCCESS stargazer/patches/linux.yml https://builds.sr.ht/~zethra/job/1297208 β #1297207 SUCCESS stargazer/patches/freebsd.yml https://builds.sr.ht/~zethra/job/1297207
Hi, I switched the osrandom dev dependency with rand and made sure it compiles using MSRV 1.77.0. I couldn't figure out how to create an async buf reader out of the tcp stream and have it also work with the tls acceptor. So what I have done instead is to use the peek function to peek in blocks of 64 bytes and read in those blocks until a newline is reached.
It also feels a bit clunky, but is much better than reading one byte at a time. In order to make it work with the tests, I had to create the AsyncPeek trait. I'm not super happy with this, but it's the best that I could come up with. Best, Ben