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#[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}