Skip to main content

brk/
http1.rs

1mod connection;
2mod decode;
3
4use std::net::{SocketAddr, ToSocketAddrs};
5use std::sync::Arc;
6
7use bytes::Bytes;
8use http::{StatusCode, Uri};
9
10use crate::{BreakReason, Counts};
11pub(crate) use connection::{Connection, Window, run_slot};
12
13/// One GET target and optional exact response validation.
14#[derive(Clone, Debug)]
15pub struct Target {
16    uri: Uri,
17    status: Option<StatusCode>,
18    body: Option<Bytes>,
19}
20
21impl Target {
22    pub fn new(url: &str) -> Result<Self, BreakReason> {
23        let uri: Uri = url
24            .parse()
25            .map_err(|_| BreakReason::Invalid("invalid target URL"))?;
26        if uri.scheme_str() != Some("http") || uri.host().is_none() {
27            return Err(BreakReason::Invalid(
28                "this adapter requires an absolute http:// URL",
29            ));
30        }
31        let authority = uri
32            .authority()
33            .ok_or(BreakReason::Invalid("missing host"))?
34            .as_str();
35        if authority.contains('@') {
36            return Err(BreakReason::Invalid("userinfo is not supported"));
37        }
38        let port = if authority.starts_with('[') {
39            authority.split_once("]:").map(|(_, port)| port)
40        } else {
41            authority.rsplit_once(':').map(|(_, port)| port)
42        };
43        if uri.host() == Some("")
44            || port.is_some_and(|port| port.parse::<u16>().is_err())
45        {
46            return Err(BreakReason::Invalid("invalid target host or port"));
47        }
48        Ok(Self { uri, status: None, body: None })
49    }
50
51    pub fn expect_status(
52        mut self,
53        status: StatusCode,
54    ) -> Self {
55        self.status = Some(status);
56        self
57    }
58
59    pub fn expect_body(
60        mut self,
61        body: impl Into<Bytes>,
62    ) -> Self {
63        self.body = Some(body.into());
64        self
65    }
66
67    pub(crate) fn resolve(&self) -> Result<Prepared, BreakReason> {
68        let host =
69            self.uri.host().ok_or(BreakReason::Invalid("missing host"))?;
70        let host = host
71            .strip_prefix('[')
72            .and_then(|s| s.strip_suffix(']'))
73            .unwrap_or(host);
74        let addresses: Arc<[SocketAddr]> =
75            (host, self.uri.port_u16().unwrap_or(80))
76                .to_socket_addrs()?
77                .collect::<Vec<_>>()
78                .into();
79        if addresses.is_empty() {
80            return Err(BreakReason::Invalid("host resolved to no addresses"));
81        }
82        let path = self.uri.path_and_query().map_or("/", |path| path.as_str());
83        let request = format!(
84            "GET {path} HTTP/1.1\r\nHost: {}\r\n\r\n",
85            self.uri.authority().unwrap()
86        );
87        Ok(Prepared {
88            addresses,
89            request: request.into_bytes(),
90            status: self.status,
91            body: self.body.clone(),
92        })
93    }
94}
95
96pub(crate) struct Prepared {
97    addresses: Arc<[SocketAddr]>,
98    request: Vec<u8>,
99    status: Option<StatusCode>,
100    body: Option<Bytes>,
101}
102
103impl Prepared {
104    pub(crate) fn client(&self) -> Client {
105        Client {
106            addresses: self.addresses.clone(),
107            request: self.request.clone().into_boxed_slice(),
108            status: self.status,
109            body: self.body.clone(),
110        }
111    }
112}
113
114pub(crate) struct Client {
115    addresses: Arc<[SocketAddr]>,
116    request: Box<[u8]>,
117    status: Option<StatusCode>,
118    body: Option<Bytes>,
119}
120
121#[derive(Clone, Copy, Debug)]
122pub(crate) enum Outcome {
123    Accepted,
124    Timeout,
125    Transport,
126    Status,
127    Body,
128}
129
130impl Outcome {
131    pub(crate) fn record(
132        self,
133        counts: &mut Counts,
134    ) {
135        counts.attempted += 1;
136        match self {
137            Self::Accepted => counts.successful += 1,
138            Self::Timeout => counts.timeout += 1,
139            Self::Transport => counts.transport += 1,
140            Self::Status => counts.status += 1,
141            Self::Body => counts.body += 1,
142        }
143    }
144}
145
146pub(crate) fn is_local_resource(error: &std::io::Error) -> bool {
147    let local = matches!(
148        error.kind(),
149        std::io::ErrorKind::OutOfMemory | std::io::ErrorKind::AddrNotAvailable
150    );
151    #[cfg(unix)]
152    let local = local
153        || matches!(
154            error.raw_os_error(),
155            Some(libc::EMFILE | libc::ENFILE | libc::ENOBUFS)
156        );
157    local
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn target_rejects_unsupported_or_incomplete_urls() {
166        for invalid in [
167            "",
168            "/plain",
169            "https://example.test",
170            "http:///",
171            "ftp://example.test",
172            "http://user:pass@example.test",
173            "http://example.test:70000/plain",
174            "http://example.test:not-a-port/plain",
175            "http://[::1]:70000/plain",
176        ] {
177            assert!(Target::new(invalid).is_err(), "{invalid}");
178        }
179        assert!(Target::new("http://[::1]:8080/plain?q=1").is_ok());
180    }
181
182    #[cfg(unix)]
183    #[test]
184    fn descriptor_exhaustion_is_a_client_resource_failure() {
185        assert!(is_local_resource(&std::io::Error::from_raw_os_error(
186            libc::EMFILE
187        )));
188        assert!(!is_local_resource(&std::io::Error::from_raw_os_error(
189            libc::ECONNREFUSED
190        )));
191    }
192}