1use ipnet::IpNet;
17use reqwest::dns::{Addrs, Name, Resolve, Resolving};
18use reqwest::header::HeaderMap;
19use std::fmt;
20use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
21use std::sync::Arc;
22use std::time::Duration;
23use url::Url;
24
25#[derive(Clone, Debug)]
27pub struct ClientConfig {
28 pub allow_private: Vec<IpNet>,
30 pub user_agent: String,
32 pub connect_timeout: Duration,
34 pub timeout: Duration,
36 pub max_response_bytes: usize,
38 pub max_redirects: usize,
40 pub pool_idle_timeout: Duration,
43 pub pool_max_idle_per_host: usize,
51}
52
53impl Default for ClientConfig {
54 fn default() -> Self {
55 Self {
56 allow_private: Vec::new(),
57 user_agent: concat!("ojak/", env!("CARGO_PKG_VERSION")).to_owned(),
58 connect_timeout: Duration::from_secs(10),
59 timeout: Duration::from_secs(30),
60 max_response_bytes: 1024 * 1024,
61 max_redirects: 3,
62 pool_idle_timeout: Duration::from_secs(10),
63 pool_max_idle_per_host: 2,
64 }
65 }
66}
67
68#[derive(Debug)]
70pub enum RequestError {
71 InvalidUrl(String),
73 Refused(String),
76 TooLarge,
78 Network(reqwest::Error),
80}
81
82impl fmt::Display for RequestError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::InvalidUrl(url) => write!(f, "not a URL Ojak will request: {url}"),
86 Self::Refused(why) => write!(f, "refused: {why}"),
87 Self::TooLarge => f.write_str("response too large"),
88 Self::Network(error) => write!(f, "network: {error}"),
89 }
90 }
91}
92
93impl std::error::Error for RequestError {}
94
95#[derive(Clone, Debug)]
97pub struct Response {
98 pub status: u16,
99 pub headers: HeaderMap,
100 pub url: Url,
102 pub body: Vec<u8>,
103}
104
105#[derive(Clone, Debug)]
107pub struct Client {
108 get: reqwest::Client,
109 direct: reqwest::Client,
111 config: Arc<ClientConfig>,
112}
113
114impl Client {
115 pub fn new(config: ClientConfig) -> Result<Self, RequestError> {
121 let config = Arc::new(config);
122 let resolver = Arc::new(GuardedResolver {
123 allow: config.allow_private.clone(),
124 });
125 let builder = || {
126 reqwest::Client::builder()
127 .user_agent(config.user_agent.clone())
128 .connect_timeout(config.connect_timeout)
129 .timeout(config.timeout)
130 .pool_idle_timeout(config.pool_idle_timeout)
131 .pool_max_idle_per_host(config.pool_max_idle_per_host)
132 .dns_resolver(resolver.clone())
133 };
134 let redirect_config = config.clone();
135 let get = builder()
136 .redirect(reqwest::redirect::Policy::custom(move |attempt| {
137 if attempt.previous().len() > redirect_config.max_redirects {
138 return attempt.error("too many redirects");
139 }
140 match validate_url(attempt.url(), &redirect_config.allow_private) {
141 Ok(()) => attempt.follow(),
142 Err(RequestError::Refused(why)) => attempt.error(Refused(why)),
143 Err(error) => attempt.error(error.to_string()),
144 }
145 }))
146 .build()
147 .map_err(RequestError::Network)?;
148 let direct = builder()
153 .redirect(reqwest::redirect::Policy::none())
154 .build()
155 .map_err(RequestError::Network)?;
156 Ok(Self {
157 get,
158 direct,
159 config,
160 })
161 }
162
163 #[must_use]
165 pub fn config(&self) -> &ClientConfig {
166 &self.config
167 }
168
169 pub async fn get(&self, url: &Url, headers: HeaderMap) -> Result<Response, RequestError> {
176 validate_url(url, &self.config.allow_private)?;
177 let response = self
178 .get
179 .get(url.clone())
180 .headers(headers)
181 .send()
182 .await
183 .map_err(classify)?;
184 self.read(response).await
185 }
186
187 pub async fn get_direct(
193 &self,
194 url: &Url,
195 headers: HeaderMap,
196 ) -> Result<Response, RequestError> {
197 validate_url(url, &self.config.allow_private)?;
198 let response = self
199 .direct
200 .get(url.clone())
201 .headers(headers)
202 .send()
203 .await
204 .map_err(classify)?;
205 self.read(response).await
206 }
207
208 pub async fn post(
214 &self,
215 url: &Url,
216 headers: HeaderMap,
217 body: Vec<u8>,
218 ) -> Result<Response, RequestError> {
219 validate_url(url, &self.config.allow_private)?;
220 let response = self
221 .direct
222 .post(url.clone())
223 .headers(headers)
224 .body(body)
225 .send()
226 .await
227 .map_err(classify)?;
228 self.read(response).await
229 }
230
231 pub fn request(
248 &self,
249 method: reqwest::Method,
250 url: &Url,
251 ) -> Result<reqwest::RequestBuilder, RequestError> {
252 validate_url(url, &self.config.allow_private)?;
253 Ok(self.get.request(method, url.clone()))
254 }
255
256 async fn read(&self, mut response: reqwest::Response) -> Result<Response, RequestError> {
257 let status = response.status().as_u16();
258 let headers = response.headers().clone();
259 let url = response.url().clone();
260 let limit = self.config.max_response_bytes;
261 if response
262 .content_length()
263 .is_some_and(|length| length > limit as u64)
264 {
265 return Err(RequestError::TooLarge);
266 }
267 let mut body = Vec::new();
268 while let Some(chunk) = response.chunk().await.map_err(RequestError::Network)? {
269 if body.len() + chunk.len() > limit {
270 return Err(RequestError::TooLarge);
271 }
272 body.extend_from_slice(&chunk);
273 }
274 Ok(Response {
275 status,
276 headers,
277 url,
278 body,
279 })
280 }
281}
282
283impl From<reqwest::Error> for RequestError {
284 fn from(error: reqwest::Error) -> Self {
288 classify(error)
289 }
290}
291
292fn classify(error: reqwest::Error) -> RequestError {
295 let mut source: Option<&dyn std::error::Error> = Some(&error);
296 while let Some(current) = source {
297 if let Some(refused) = current.downcast_ref::<Refused>() {
298 return RequestError::Refused(refused.0.clone());
299 }
300 source = current.source();
301 }
302 RequestError::Network(error)
303}
304
305#[derive(Debug)]
308struct Refused(String);
309
310impl fmt::Display for Refused {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 write!(f, "refused: {}", self.0)
313 }
314}
315
316impl std::error::Error for Refused {}
317
318pub fn validate_url(url: &Url, allow: &[IpNet]) -> Result<(), RequestError> {
332 if !matches!(url.scheme(), "http" | "https") {
333 return Err(RequestError::InvalidUrl(url.to_string()));
334 }
335 match url.host() {
336 None => Err(RequestError::InvalidUrl(url.to_string())),
337 Some(url::Host::Ipv4(address)) => check_address(IpAddr::V4(address), allow),
338 Some(url::Host::Ipv6(address)) => check_address(IpAddr::V6(address), allow),
339 Some(url::Host::Domain(_)) => Ok(()),
340 }
341}
342
343fn check_address(address: IpAddr, allow: &[IpNet]) -> Result<(), RequestError> {
344 if permitted(address, allow) {
345 Ok(())
346 } else {
347 Err(RequestError::Refused(format!(
348 "{address} is not a public address"
349 )))
350 }
351}
352
353fn permitted(address: IpAddr, allow: &[IpNet]) -> bool {
354 is_public(address) || allow.iter().any(|network| network.contains(&address))
355}
356
357struct GuardedResolver {
360 allow: Vec<IpNet>,
361}
362
363impl Resolve for GuardedResolver {
364 fn resolve(&self, name: Name) -> Resolving {
365 let allow = self.allow.clone();
366 Box::pin(async move {
367 let host = name.as_str().to_owned();
368 let addresses: Vec<SocketAddr> = tokio::net::lookup_host((host.as_str(), 0))
369 .await?
370 .filter(|address| permitted(address.ip(), &allow))
371 .collect();
372 if addresses.is_empty() {
373 return Err(
374 Box::new(Refused(format!("{host} resolves to no public address")))
375 as Box<dyn std::error::Error + Send + Sync>,
376 );
377 }
378 Ok(Box::new(addresses.into_iter()) as Addrs)
379 })
380 }
381}
382
383#[must_use]
394pub fn is_public(address: IpAddr) -> bool {
395 match address {
396 IpAddr::V4(v4) => is_public_v4(v4),
397 IpAddr::V6(v6) => is_public_v6(v6),
398 }
399}
400
401fn is_public_v4(address: Ipv4Addr) -> bool {
402 let [a, b, c, _] = address.octets();
403 !(a == 0 || a == 10 || a == 127 || (a == 100 && (64..128).contains(&b)) || (a == 169 && b == 254) || (a == 172 && (16..32).contains(&b)) || (a == 192 && b == 0 && c == 0) || (a == 192 && b == 0 && c == 2) || (a == 192 && b == 88 && c == 99) || (a == 192 && b == 168) || (a == 198 && (18..20).contains(&b)) || (a == 198 && b == 51 && c == 100) || (a == 203 && b == 0 && c == 113) || a >= 224) }
418
419fn is_public_v6(address: Ipv6Addr) -> bool {
420 let segments = address.segments();
421 if let Some(v4) = address.to_ipv4_mapped() {
422 return is_public_v4(v4);
423 }
424 !(address.is_unspecified()
425 || address.is_loopback()
426 || segments[0] & 0xfe00 == 0xfc00 || segments[0] & 0xffc0 == 0xfe80 || segments[0] & 0xffc0 == 0xfec0 || segments[0] & 0xff00 == 0xff00 || (segments[0] == 0x2001 && segments[1] == 0x0db8) || (segments[0] == 0x2001 && segments[1] < 0x0200) || segments[..4] == [0x0100, 0, 0, 0] || segments[..6] == [0x64, 0xff9b, 0, 0, 0, 0] || segments[..3] == [0x64, 0xff9b, 1] || segments[0] == 0x2002 || segments[0] & 0xfff0 == 0x3ff0 || segments[..6] == [0, 0, 0, 0, 0, 0]) }
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 fn public(address: &str) -> bool {
445 is_public(address.parse().expect("address"))
446 }
447
448 #[test]
449 fn public_addresses_are_permitted() {
450 for address in [
451 "1.1.1.1",
452 "8.8.8.8",
453 "93.184.216.34",
454 "2606:4700::1111",
455 "2a00:1450::1",
456 ] {
457 assert!(public(address), "{address}");
458 }
459 }
460
461 #[test]
462 fn reserved_addresses_are_refused() {
463 for address in [
464 "0.0.0.0",
465 "10.1.2.3",
466 "127.0.0.1",
467 "100.64.0.1",
468 "169.254.169.254",
469 "172.16.0.1",
470 "172.31.255.255",
471 "192.0.0.8",
472 "192.0.2.1",
473 "192.168.1.1",
474 "198.18.0.1",
475 "224.0.0.1",
476 "255.255.255.255",
477 "::",
478 "::1",
479 "fc00::1",
480 "fd12:3456::1",
481 "fe80::1",
482 "fec0::1",
483 "ff02::1",
484 "2001:db8::1",
485 "2001::1",
486 "100::1",
487 ] {
488 assert!(!public(address), "{address}");
489 }
490 }
491
492 #[test]
494 fn embedded_ipv4_addresses_are_judged_by_what_they_carry() {
495 assert!(!public("::ffff:127.0.0.1"));
496 assert!(!public("::ffff:10.0.0.1"));
497 assert!(public("::ffff:1.1.1.1"));
498 assert!(!public("::127.0.0.1")); }
500
501 #[test]
504 fn translation_prefixes_are_refused_whatever_they_carry() {
505 for address in [
506 "64:ff9b::a9fe:a9fe", "64:ff9b::101:101", "64:ff9b:1::101:101", "2002:7f00:1::1", "2002:101:101::1", "3fff::1", ] {
513 assert!(!public(address), "{address}");
514 }
515 let allow: Vec<IpNet> = vec!["64:ff9b::/96".parse().expect("network")];
516 let url = Url::parse("http://[64:ff9b::101:101]/").expect("url");
517 assert!(validate_url(&url, &allow).is_ok());
518 }
519
520 #[test]
521 fn a_literal_address_in_a_url_is_checked() {
522 let allow = [];
523 for url in [
524 "http://127.0.0.1/inbox",
525 "https://[::1]/inbox",
526 "http://169.254.169.254/latest/meta-data",
527 ] {
528 let url = Url::parse(url).expect("url");
529 assert!(
530 matches!(validate_url(&url, &allow), Err(RequestError::Refused(_))),
531 "{url}"
532 );
533 }
534 assert!(matches!(
535 validate_url(&Url::parse("file:///etc/passwd").expect("url"), &allow),
536 Err(RequestError::InvalidUrl(_))
537 ));
538 assert!(validate_url(&Url::parse("https://example.com/").expect("url"), &allow).is_ok());
539 }
540
541 #[test]
542 fn an_allowed_network_is_reachable() {
543 let allow: Vec<IpNet> = vec!["127.0.0.0/8".parse().expect("network")];
544 let url = Url::parse("http://127.0.0.1:8080/inbox").expect("url");
545 assert!(validate_url(&url, &allow).is_ok());
546 let url = Url::parse("http://10.0.0.1/inbox").expect("url");
547 assert!(validate_url(&url, &allow).is_err());
548 }
549}