1use url::Url;
4
5pub fn validate_url_not_private(url_str: &str) -> Result<(), &'static str> {
7 let parsed = Url::parse(url_str).map_err(|_| "Invalid URL")?;
8
9 match parsed.scheme() {
10 "http" | "https" => {}
11 _ => return Err("Only HTTP(S) URLs are allowed"),
12 }
13
14 match parsed.host() {
15 Some(url::Host::Ipv4(ip)) => {
16 let o = ip.octets();
17 if ip.is_loopback() || ip.is_private() || ip.is_link_local()
18 || ip.is_broadcast() || ip.is_unspecified()
19 || (o[0] == 100 && o[1] >= 64 && o[1] <= 127)
20 {
21 return Err("Private/internal IP addresses are not allowed");
22 }
23 }
24 Some(url::Host::Ipv6(ip)) => {
25 if ip.is_loopback() || ip.is_unspecified() || is_ipv6_private(&ip) {
26 return Err("Private/internal IP addresses are not allowed");
27 }
28 }
29 Some(url::Host::Domain(domain)) => {
30 if domain == "localhost" || domain.ends_with(".local") || domain.ends_with(".internal") {
31 return Err("Local hostnames are not allowed");
32 }
33 }
34 None => return Err("URL has no host"),
35 }
36
37 Ok(())
38}
39
40fn is_ipv6_private(ip: &std::net::Ipv6Addr) -> bool {
41 if let Some(ipv4) = ip.to_ipv4_mapped() {
42 return ipv4.is_loopback() || ipv4.is_private() || ipv4.is_link_local();
43 }
44 let segments = ip.segments();
45 if segments[0] & 0xfe00 == 0xfc00 { return true; } if segments[0] & 0xffc0 == 0xfe80 { return true; } false
48}
49
50pub use reqwest;
56
57pub const TRANSFER_STALL: std::time::Duration = std::time::Duration::from_secs(120);
70
71#[allow(clippy::disallowed_methods)]
84pub fn build_http_client(timeout: std::time::Duration) -> Result<reqwest::Client, String> {
85 build_http_client_with_options(Some(timeout), None, true)
86}
87
88static USER_AGENT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
108
109pub fn set_app_version(version: &str) {
112 let _ = USER_AGENT.set(format!("Vector/{version}"));
113}
114
115pub fn user_agent() -> String {
116 USER_AGENT
117 .get()
118 .cloned()
119 .unwrap_or_else(|| format!("Vector/{}", env!("CARGO_PKG_VERSION")))
120}
121
122static CLIENTS_BY_OPTIONS: OnceLock<std::sync::Mutex<std::collections::HashMap<ClientOptions, reqwest::Client>>> =
134 OnceLock::new();
135
136type ClientOptions = (Option<std::time::Duration>, Option<std::time::Duration>, bool);
138
139pub fn build_http_client_with_options(
140 timeout: Option<std::time::Duration>,
141 read_timeout: Option<std::time::Duration>,
142 follow_redirects: bool,
143) -> Result<reqwest::Client, String> {
144 let key = (timeout, read_timeout, follow_redirects);
145 let cell = CLIENTS_BY_OPTIONS.get_or_init(Default::default);
146 if let Ok(map) = cell.lock() {
147 if let Some(c) = map.get(&key) {
148 return Ok(c.clone());
149 }
150 }
151 let client = build_http_client_uncached(timeout, read_timeout, follow_redirects)?;
152 if let Ok(mut map) = cell.lock() {
153 map.entry(key).or_insert_with(|| client.clone());
154 }
155 Ok(client)
156}
157
158fn forget_pooled_clients() {
161 if let Some(cell) = CLIENTS_BY_OPTIONS.get() {
162 if let Ok(mut map) = cell.lock() {
163 map.clear();
164 }
165 }
166}
167
168#[cfg(target_arch = "wasm32")]
171#[allow(clippy::disallowed_methods)]
172fn build_http_client_uncached(
173 _timeout: Option<std::time::Duration>,
174 _read_timeout: Option<std::time::Duration>,
175 _follow_redirects: bool,
176) -> Result<reqwest::Client, String> {
177 reqwest::Client::builder().build().map_err(|e| format!("HTTP client build failed: {e}"))
178}
179
180#[cfg(not(target_arch = "wasm32"))]
181#[allow(clippy::disallowed_methods)]
182fn build_http_client_uncached(
183 timeout: Option<std::time::Duration>,
184 read_timeout: Option<std::time::Duration>,
185 follow_redirects: bool,
186) -> Result<reqwest::Client, String> {
187 let mut builder = reqwest::Client::builder()
188 .pool_max_idle_per_host(8)
192 .pool_idle_timeout(std::time::Duration::from_secs(90))
193 .tcp_keepalive(std::time::Duration::from_secs(30))
194 .http2_keep_alive_interval(std::time::Duration::from_secs(30))
195 .http2_keep_alive_while_idle(true)
196 .user_agent(user_agent())
201 .connect_timeout(std::time::Duration::from_secs(15));
204 if let Some(t) = timeout {
205 builder = builder.timeout(t);
206 }
207 if let Some(rt) = read_timeout {
208 builder = builder.read_timeout(rt);
209 }
210 if !follow_redirects {
211 builder = builder.redirect(reqwest::redirect::Policy::none());
212 } else {
213 builder = builder.redirect(reqwest::redirect::Policy::custom(|attempt| {
217 if attempt.previous().len() >= 10 {
218 return attempt.error("too many redirects");
219 }
220 match validate_url_not_private(attempt.url().as_str()) {
221 Ok(()) => attempt.follow(),
222 Err(e) => attempt.error(e),
223 }
224 }));
225 }
226
227 #[cfg(feature = "tor")]
228 {
229 match crate::tor::transport_state() {
230 crate::tor::TorTransportState::Active(addr) => {
231 let url = format!("socks5h://{addr}");
234 let proxy = reqwest::Proxy::all(&url)
235 .map_err(|e| format!("Tor proxy URL ({url}) invalid: {e}"))?;
236 builder = builder.proxy(proxy);
237 builder = builder.connect_timeout(std::time::Duration::from_secs(45));
239 }
240 crate::tor::TorTransportState::RequiredButInactive => {
241 let url = format!("socks5h://{}", crate::tor::blackhole_proxy_addr());
244 let proxy = reqwest::Proxy::all(&url)
245 .map_err(|e| format!("blackhole proxy invalid: {e}"))?;
246 builder = builder.proxy(proxy);
247 }
248 crate::tor::TorTransportState::Disabled => {
249 }
251 }
252 }
253
254 builder
255 .build()
256 .map_err(|e| format!("Failed to build HTTP client: {}", e))
257}
258
259use std::sync::{Arc, OnceLock, RwLock};
273
274static SHARED_HTTP_CLIENT: OnceLock<RwLock<Arc<reqwest::Client>>> = OnceLock::new();
275
276const DEFAULT_SHARED_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
277
278fn shared_cell() -> &'static RwLock<Arc<reqwest::Client>> {
279 SHARED_HTTP_CLIENT.get_or_init(|| {
280 let client = build_http_client(DEFAULT_SHARED_TIMEOUT)
281 .expect("initial shared HTTP client build cannot fail");
282 RwLock::new(Arc::new(client))
283 })
284}
285
286pub fn shared_http_client() -> Arc<reqwest::Client> {
289 shared_cell().read().unwrap().clone()
290}
291
292pub fn rebuild_shared_http_client() -> Result<(), String> {
296 forget_pooled_clients();
297 let new = Arc::new(build_http_client(DEFAULT_SHARED_TIMEOUT)?);
298 *shared_cell().write().unwrap() = new;
299 Ok(())
300}
301
302fn md_group_close(bytes: &[u8], start: usize, open: u8, close: u8) -> Option<usize> {
308 let mut depth = 1usize;
309 let mut escaped = false;
310 let mut j = start;
311 loop {
312 match bytes.get(j).copied() {
313 None => return None,
314 Some(b'\\') if !escaped => escaped = true,
315 Some(b) if b == open && !escaped => depth += 1,
316 Some(b) if b == close && !escaped => {
317 depth -= 1;
318 if depth == 0 {
319 return Some(j);
320 }
321 }
322 _ => escaped = false,
323 }
324 j += 1;
325 }
326}
327
328pub fn strip_md_link_claims(text: &str) -> String {
336 let bytes = text.as_bytes();
337 let mut out = String::with_capacity(text.len());
338 let mut last = 0;
339 let mut i = 0;
340 while i < bytes.len() {
341 if bytes[i] != b'[' || (i > 0 && bytes[i - 1] == b'!') {
342 i += 1;
343 continue;
344 }
345 let Some(close) = md_group_close(bytes, i + 1, b'[', b']') else { break };
346 match bytes.get(close + 1).copied() {
347 Some(b'(') => {
349 let Some(paren) = md_group_close(bytes, close + 2, b'(', b')') else {
350 i = close + 1;
351 continue;
352 };
353 out.push_str(&text[last..i]);
354 let href = text[close + 2..paren].trim();
355 if !(href.starts_with('<') && href.ends_with('>')) {
356 out.push(' ');
357 out.push_str(href);
358 out.push(' ');
359 }
360 i = paren + 1;
361 last = i;
362 }
363 Some(b'[') => {
366 let Some(ref_close) = md_group_close(bytes, close + 2, b'[', b']') else {
367 i = close + 1;
368 continue;
369 };
370 out.push_str(&text[last..i]);
371 i = ref_close + 1;
372 last = i;
373 }
374 _ => {
375 i = close + 1;
376 }
377 }
378 }
379 out.push_str(&text[last..]);
380 out
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
392 fn md_link_claim_text_dropped_href_kept() {
393 let out = strip_md_link_claims("[https://your-bank.com](https://evil.io)");
395 assert!(!out.contains("your-bank.com"), "claimed text must not reach the scan: {out}");
396 assert!(out.contains("https://evil.io"), "real destination must reach the scan: {out}");
397 }
398
399 #[test]
400 fn md_no_preview_link_dropped_entirely() {
401 let out = strip_md_link_claims("see [docs](<https://vector.app/docs>) ok");
402 assert!(!out.contains("vector.app"), "no-preview href must not reach the scan: {out}");
403 assert!(out.contains("see ") && out.contains(" ok"));
404 }
405
406 #[test]
407 fn md_image_passes_through() {
408 let text = "";
409 assert_eq!(strip_md_link_claims(text), text);
410 }
411
412 #[test]
413 fn plain_text_and_bare_urls_untouched() {
414 let text = "check https://vector.app and [also] (spaced) brackets";
415 assert_eq!(strip_md_link_claims(text), text);
416 }
417
418 #[test]
419 fn multiple_links_keep_document_order() {
420 let out = strip_md_link_claims("[a](https://one.io) mid [b](https://two.io)");
421 let one = out.find("https://one.io").expect("first href kept");
422 let two = out.find("https://two.io").expect("second href kept");
423 assert!(one < two);
424 }
425
426 #[test]
427 fn nested_bracket_label_still_drops_claim() {
428 let out = strip_md_link_claims("[[https://trusted.com]](https://evil.io)");
429 assert!(!out.contains("trusted.com"), "nested-bracket claim must not reach the scan: {out}");
430 assert!(out.contains("https://evil.io"));
431 }
432
433 #[test]
434 fn escaped_bracket_label_still_drops_claim() {
435 let out = strip_md_link_claims(r"[https://trusted.com\]](https://evil.io)");
436 assert!(!out.contains("trusted.com"), "escaped-bracket claim must not reach the scan: {out}");
437 assert!(out.contains("https://evil.io"));
438 }
439
440 #[test]
441 fn paren_path_href_survives_whole() {
442 let out = strip_md_link_claims("[wiki](https://en.wikipedia.org/wiki/Foo_(bar))");
443 assert!(out.contains("https://en.wikipedia.org/wiki/Foo_(bar)"), "balanced-paren href kept intact: {out}");
444 }
445
446 #[test]
447 fn reference_link_label_dropped_definition_scanned() {
448 let out = strip_md_link_claims("[https://trusted.com][1]\n[1]: https://evil.io");
449 assert!(!out.contains("trusted.com"), "reflink claim must not reach the scan: {out}");
450 assert!(out.contains("https://evil.io"), "definition URL stays scannable: {out}");
451 }
452
453 #[test]
454 fn multibyte_label_no_panic() {
455 let out = strip_md_link_claims("[🔒 sécurisé — café](https://evil.io) 日本語");
456 assert!(out.contains("https://evil.io"));
457 assert!(out.contains("日本語"));
458 }
459
460 #[test]
465 fn valid_public_https_url_passes() {
466 assert!(validate_url_not_private("https://example.com/path").is_ok(),
467 "https://example.com should be allowed");
468 }
469
470 #[test]
471 fn valid_public_http_url_passes() {
472 assert!(validate_url_not_private("http://example.com").is_ok(),
473 "http://example.com should be allowed");
474 }
475
476 #[test]
477 fn valid_public_ip_8888_passes() {
478 assert!(validate_url_not_private("https://8.8.8.8/dns").is_ok(),
479 "8.8.8.8 (Google DNS) is a public IP and should be allowed");
480 }
481
482 #[test]
483 fn valid_public_ip_1111_passes() {
484 assert!(validate_url_not_private("https://1.1.1.1").is_ok(),
485 "1.1.1.1 (Cloudflare DNS) is a public IP and should be allowed");
486 }
487
488 #[test]
489 fn valid_url_with_port_passes() {
490 assert!(validate_url_not_private("https://example.com:8080/api").is_ok(),
491 "URL with port on public domain should be allowed");
492 }
493
494 #[test]
499 fn localhost_rejected() {
500 let result = validate_url_not_private("http://localhost/secret");
501 assert!(result.is_err(), "localhost should be rejected");
502 }
503
504 #[test]
505 fn ip_127_0_0_1_rejected() {
506 let result = validate_url_not_private("http://127.0.0.1/admin");
507 assert!(result.is_err(), "127.0.0.1 (loopback) should be rejected");
508 }
509
510 #[test]
511 fn ip_127_255_255_255_rejected() {
512 let result = validate_url_not_private("http://127.255.255.255");
513 assert!(result.is_err(), "127.255.255.255 (loopback range) should be rejected");
514 }
515
516 #[test]
521 fn private_class_a_10_rejected() {
522 let result = validate_url_not_private("http://10.0.0.1/internal");
523 assert!(result.is_err(), "10.0.0.1 (private class A) should be rejected");
524 }
525
526 #[test]
527 fn private_class_b_172_16_rejected() {
528 let result = validate_url_not_private("http://172.16.0.1/internal");
529 assert!(result.is_err(), "172.16.0.1 (private class B) should be rejected");
530 }
531
532 #[test]
533 fn private_class_b_172_31_rejected() {
534 let result = validate_url_not_private("http://172.31.255.255");
535 assert!(result.is_err(), "172.31.255.255 (private class B upper bound) should be rejected");
536 }
537
538 #[test]
539 fn private_class_c_192_168_rejected() {
540 let result = validate_url_not_private("http://192.168.1.1/router");
541 assert!(result.is_err(), "192.168.1.1 (private class C) should be rejected");
542 }
543
544 #[test]
549 fn link_local_169_254_rejected() {
550 let result = validate_url_not_private("http://169.254.1.1");
551 assert!(result.is_err(), "169.254.1.1 (link-local) should be rejected");
552 }
553
554 #[test]
555 fn cgn_100_64_rejected() {
556 let result = validate_url_not_private("http://100.64.0.1");
557 assert!(result.is_err(), "100.64.0.1 (CGN / shared address space) should be rejected");
558 }
559
560 #[test]
561 fn cgn_100_127_rejected() {
562 let result = validate_url_not_private("http://100.127.255.255");
563 assert!(result.is_err(), "100.127.255.255 (CGN upper bound) should be rejected");
564 }
565
566 #[test]
567 fn broadcast_255_rejected() {
568 let result = validate_url_not_private("http://255.255.255.255");
569 assert!(result.is_err(), "255.255.255.255 (broadcast) should be rejected");
570 }
571
572 #[test]
573 fn unspecified_0_0_0_0_rejected() {
574 let result = validate_url_not_private("http://0.0.0.0");
575 assert!(result.is_err(), "0.0.0.0 (unspecified) should be rejected");
576 }
577
578 #[test]
583 fn ipv6_loopback_rejected() {
584 let result = validate_url_not_private("http://[::1]/secret");
585 assert!(result.is_err(), "::1 (IPv6 loopback) should be rejected");
586 }
587
588 #[test]
589 fn ipv6_unique_local_fc00_rejected() {
590 let result = validate_url_not_private("http://[fc00::1]");
591 assert!(result.is_err(), "fc00::1 (IPv6 unique-local) should be rejected");
592 }
593
594 #[test]
595 fn ipv6_unique_local_fd00_rejected() {
596 let result = validate_url_not_private("http://[fd00::1]");
597 assert!(result.is_err(), "fd00::1 (IPv6 unique-local) should be rejected");
598 }
599
600 #[test]
601 fn ipv6_link_local_fe80_rejected() {
602 let result = validate_url_not_private("http://[fe80::1]");
603 assert!(result.is_err(), "fe80::1 (IPv6 link-local) should be rejected");
604 }
605
606 #[test]
607 fn ipv4_mapped_ipv6_loopback_rejected() {
608 let result = validate_url_not_private("http://[::ffff:127.0.0.1]");
609 assert!(result.is_err(), "::ffff:127.0.0.1 (IPv4-mapped loopback) should be rejected");
610 }
611
612 #[test]
613 fn ipv4_mapped_ipv6_private_rejected() {
614 let result = validate_url_not_private("http://[::ffff:192.168.1.1]");
615 assert!(result.is_err(), "::ffff:192.168.1.1 (IPv4-mapped private) should be rejected");
616 }
617
618 #[test]
623 fn dot_local_domain_rejected() {
624 let result = validate_url_not_private("http://mydevice.local/api");
625 assert!(result.is_err(), ".local domain should be rejected");
626 }
627
628 #[test]
629 fn dot_internal_domain_rejected() {
630 let result = validate_url_not_private("http://service.internal/health");
631 assert!(result.is_err(), ".internal domain should be rejected");
632 }
633
634 #[test]
639 fn ftp_scheme_rejected() {
640 let result = validate_url_not_private("ftp://example.com/file.txt");
641 assert!(result.is_err(), "ftp:// scheme should be rejected");
642 assert_eq!(result.unwrap_err(), "Only HTTP(S) URLs are allowed");
643 }
644
645 #[test]
646 fn file_scheme_rejected() {
647 let result = validate_url_not_private("file:///etc/passwd");
648 assert!(result.is_err(), "file:// scheme should be rejected");
649 assert_eq!(result.unwrap_err(), "Only HTTP(S) URLs are allowed");
650 }
651
652 #[test]
653 fn javascript_scheme_rejected() {
654 let result = validate_url_not_private("javascript:alert(1)");
655 assert!(result.is_err(), "javascript: scheme should be rejected");
656 }
657
658 #[test]
659 fn data_scheme_rejected() {
660 let result = validate_url_not_private("data:text/html,<h1>hi</h1>");
661 assert!(result.is_err(), "data: scheme should be rejected");
662 }
663
664 #[test]
669 fn no_host_rejected() {
670 let result = validate_url_not_private("http://");
672 assert!(result.is_err(), "URL with no host should be rejected");
673 }
674
675 #[test]
676 fn invalid_url_rejected() {
677 let result = validate_url_not_private("not a url at all");
678 assert!(result.is_err(), "invalid URL string should be rejected");
679 assert_eq!(result.unwrap_err(), "Invalid URL");
680 }
681
682 #[test]
683 fn empty_string_rejected() {
684 let result = validate_url_not_private("");
685 assert!(result.is_err(), "empty string should be rejected");
686 }
687
688 #[test]
693 fn cgn_100_63_not_rejected() {
694 assert!(validate_url_not_private("http://100.63.255.255").is_ok(),
696 "100.63.255.255 is outside CGN range and should be allowed");
697 }
698
699 #[test]
700 fn cgn_100_128_not_rejected() {
701 assert!(validate_url_not_private("http://100.128.0.1").is_ok(),
703 "100.128.0.1 is outside CGN range and should be allowed");
704 }
705
706 #[test]
707 fn private_172_15_not_rejected() {
708 assert!(validate_url_not_private("http://172.15.255.255").is_ok(),
710 "172.15.255.255 is outside private class B range and should be allowed");
711 }
712
713 #[test]
714 fn private_172_32_not_rejected() {
715 assert!(validate_url_not_private("http://172.32.0.1").is_ok(),
717 "172.32.0.1 is outside private class B range and should be allowed");
718 }
719}
720
721pub struct Egress {
729 pub url: String,
730 pub auth: Option<reqwest::header::HeaderValue>,
731}
732
733impl Egress {
734 pub fn proxied(&self) -> bool {
736 self.auth.is_some() || crate::proxy::proxy_server_of(&self.url).is_some()
737 }
738}
739
740pub async fn egress(url: &str) -> Egress {
743 match crate::proxy::proxied(url).await {
744 Some(via) => {
745 let auth = match crate::proxy::proxy_server_of(&via) {
746 Some(server) => crate::proxy::proxy_authorization(&server).await,
747 None => None,
748 };
749 Egress { url: via, auth }
750 }
751 None => Egress { url: url.to_string(), auth: None },
752 }
753}
754
755pub async fn proxied_request(client: &reqwest::Client, method: reqwest::Method, url: &str) -> reqwest::RequestBuilder {
758 let e = egress(url).await;
759 let req = client.request(method, &e.url);
760 match e.auth {
761 Some(v) => req.header(reqwest::header::AUTHORIZATION, v),
762 None => req,
763 }
764}
765
766pub async fn remote_status(url: &str, timeout: std::time::Duration) -> Option<u16> {
773 let e = egress(url).await;
774 let client = build_http_client(timeout).ok()?;
775 let with = |req: reqwest::RequestBuilder| match &e.auth {
776 Some(v) => req.header(reqwest::header::AUTHORIZATION, v.clone()),
777 None => req,
778 };
779 if e.proxied() {
780 let resp = with(client.get(&e.url)).header(reqwest::header::RANGE, "bytes=0-0").send().await.ok()?;
781 if resp.status().is_success() {
782 return Some(200);
783 }
784 let body: serde_json::Value = resp.json().await.ok()?;
785 return body.get("source_status").and_then(|v| v.as_u64()).map(|v| v as u16);
786 }
787 let head = with(client.head(&e.url)).send().await.ok()?;
788 let status = head.status();
789 if status == reqwest::StatusCode::METHOD_NOT_ALLOWED || status == reqwest::StatusCode::NOT_IMPLEMENTED {
790 let r = with(client.get(&e.url)).header(reqwest::header::RANGE, "bytes=0-0").send().await.ok()?;
791 return Some(r.status().as_u16());
792 }
793 Some(status.as_u16())
794}
795
796pub async fn get_remote_file_size(url: &str) -> Option<u64> {
803 validate_url_not_private(url).ok()?;
804 let client = build_http_client(std::time::Duration::from_secs(8)).ok()?;
805
806 if let Ok(head_res) = proxied_request(&client, reqwest::Method::HEAD, url).await.send().await {
808 if let Some(length) = head_res.content_length() {
809 if length > 0 {
810 return Some(length);
811 }
812 }
813 }
814
815 if let Ok(partial_res) = proxied_request(&client, reqwest::Method::GET, url)
817 .await
818 .header("Range", "bytes=0-1")
819 .send()
820 .await
821 {
822 if let Some(content_range) = partial_res.headers().get("content-range") {
823 if let Ok(range_str) = content_range.to_str() {
824 if let Some(size_part) = range_str.split('/').nth(1) {
825 if let Ok(size) = size_part.parse::<u64>() {
826 return Some(size);
827 }
828 }
829 }
830 }
831 if let Some(length) = partial_res.content_length() {
832 if length > 100 {
833 return Some(length);
834 }
835 }
836 }
837
838 None
839}
840
841#[allow(clippy::unnecessary_cast)] pub fn raise_fd_limit() -> Option<u64> {
850 #[cfg(not(unix))]
851 {
852 None
853 }
854 #[cfg(unix)]
855 {
856 #[cfg(target_os = "macos")]
859 const CEILING: libc::rlim_t = 10240;
860 #[cfg(not(target_os = "macos"))]
861 const CEILING: libc::rlim_t = 1 << 20;
862
863 let mut lim = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
864 if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) } != 0 {
866 return None;
867 }
868 let target = lim.rlim_max.min(CEILING);
869 if target <= lim.rlim_cur {
870 return None;
871 }
872 lim.rlim_cur = target;
873 if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &lim) } != 0 {
874 return None;
875 }
876 Some(target as u64)
877 }
878}
879
880#[cfg(all(test, unix))]
881mod fd_limit_tests {
882 #[test]
883 fn soft_limit_reaches_the_ceiling_and_is_never_lowered() {
884 let mut before = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
885 assert_eq!(unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut before) }, 0);
886
887 super::raise_fd_limit();
888
889 let mut after = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
890 assert_eq!(unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut after) }, 0);
891 #[cfg(target_os = "macos")]
892 let ceiling: libc::rlim_t = 10240;
893 #[cfg(not(target_os = "macos"))]
894 let ceiling: libc::rlim_t = 1 << 20;
895 assert!(after.rlim_cur >= before.rlim_cur);
896 assert!(after.rlim_cur >= after.rlim_max.min(ceiling));
897 }
898}
899
900#[cfg(test)]
901mod user_agent_tests {
902 use super::*;
903
904 #[test]
907 fn the_client_names_itself_as_vector() {
908 assert!(user_agent().starts_with("Vector/"), "{}", user_agent());
909 set_app_version("9.9.9-test");
910 assert!(user_agent().starts_with("Vector/"), "{}", user_agent());
913 }
914}