pub mod activity;
pub mod dns;
pub mod doctor;
pub mod hostname;
pub mod hosts;
pub mod lan_ip;
pub mod mdns;
pub mod pac;
pub mod server;
pub mod setup;
pub mod sni;
pub mod trust;
pub mod worktree;
pub(crate) struct LogThrottle {
last: std::sync::Mutex<Option<std::time::Instant>>,
suppressed: std::sync::atomic::AtomicU64,
}
impl LogThrottle {
pub(crate) const fn new() -> Self {
Self {
last: std::sync::Mutex::new(None),
suppressed: std::sync::atomic::AtomicU64::new(0),
}
}
pub(crate) fn allow(&self, every: std::time::Duration) -> Option<u64> {
use std::sync::atomic::Ordering;
let now = std::time::Instant::now();
let mut last = match self.last.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
match *last {
Some(t) if now.duration_since(t) < every => {
self.suppressed.fetch_add(1, Ordering::Relaxed);
None
}
_ => {
*last = Some(now);
Some(self.suppressed.swap(0, Ordering::Relaxed))
}
}
}
}
pub(crate) fn owns_name(tld: &str, name: &str) -> bool {
let name = name.trim_end_matches('.');
let tld = tld.trim_matches('.');
if tld.is_empty() || name.is_empty() {
return false;
}
if name.eq_ignore_ascii_case(tld) {
return true;
}
let (name, tld) = (name.as_bytes(), tld.as_bytes());
name.len() > tld.len() + 1
&& name[name.len() - tld.len() - 1] == b'.'
&& name[name.len() - tld.len()..].eq_ignore_ascii_case(tld)
}
pub(crate) fn is_strictly_under_tld(tld: &str, name: &str) -> bool {
!name
.trim_end_matches('.')
.eq_ignore_ascii_case(tld.trim_matches('.'))
&& owns_name(tld, name)
}
pub(crate) fn ascii_case_collisions<'a>(
keys: impl Iterator<Item = &'a str>,
) -> std::collections::HashSet<String> {
let mut seen = std::collections::HashSet::new();
let mut collisions = std::collections::HashSet::new();
for key in keys {
let folded = key.to_ascii_lowercase();
if !seen.insert(folded.clone()) {
collisions.insert(folded);
}
}
collisions
}
pub fn effective_tld(s: &crate::settings::Settings) -> &str {
if s.proxy.lan || !s.proxy.lan_ip.is_empty() {
"local"
} else {
&s.proxy.tld
}
}
pub fn build_proxy_url(host: Option<&str>, s: &crate::settings::Settings) -> Option<String> {
if !s.proxy.enable {
return None;
}
let host = host?;
let scheme = if s.proxy.https { "https" } else { "http" };
let tld = effective_tld(s);
let standard_port = if s.proxy.https { 443u16 } else { 80u16 };
let effective_port = u16::try_from(s.proxy.port).ok().filter(|&p| p > 0)?;
let authority = format!("{host}.{tld}");
Some(if effective_port == standard_port {
format!("{scheme}://{authority}")
} else {
format!("{scheme}://{authority}:{effective_port}")
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_throttled_message_reports_what_it_suppressed() {
use std::time::Duration;
let throttle = LogThrottle::new();
assert_eq!(throttle.allow(Duration::from_secs(60)), Some(0));
for _ in 0..5 {
assert_eq!(throttle.allow(Duration::from_secs(60)), None);
}
assert_eq!(throttle.allow(Duration::ZERO), Some(5));
assert_eq!(throttle.allow(Duration::ZERO), Some(0));
}
#[test]
fn owns_name_matches_the_apex_and_names_beneath_it() {
assert!(owns_name("localhost", "localhost"));
assert!(owns_name("localhost", "api.localhost"));
assert!(owns_name("localhost", "core.fix-refs.proj.localhost"));
assert!(owns_name("localhost", "API.LocalHost"));
assert!(owns_name("localhost", "api.localhost."));
assert!(owns_name("dev.internal", "api.dev.internal"));
assert!(!owns_name("localhost", "example.com"));
assert!(!owns_name("localhost", "notlocalhost"));
assert!(!owns_name("localhost", "localhost.evil.com"));
assert!(!owns_name("localhost", ""));
assert!(!owns_name("", "api.localhost"));
}
#[test]
fn is_strictly_under_tld_excludes_the_apex() {
assert!(is_strictly_under_tld("localhost", "api.localhost"));
assert!(!is_strictly_under_tld("localhost", "localhost"));
assert!(!is_strictly_under_tld("dev.internal", "dev.internal"));
assert!(is_strictly_under_tld("dev.internal", "a.dev.internal"));
assert!(!is_strictly_under_tld("localhost", "example.com"));
}
#[test]
fn test_ascii_case_collisions() {
let none = ascii_case_collisions(["myapp", "other", "third"].into_iter());
assert!(none.is_empty());
let folded = ascii_case_collisions(["MyApp", "myapp", "other"].into_iter());
assert_eq!(folded.len(), 1);
assert!(folded.contains("myapp"));
let exact = ascii_case_collisions(["dup", "dup"].into_iter());
assert!(exact.contains("dup"));
let unicode = ascii_case_collisions(["café", "CAFÉ"].into_iter());
assert!(unicode.is_empty());
}
fn proxy_settings() -> crate::settings::Settings {
let mut s = crate::settings::Settings::default();
s.proxy.enable = true;
s.proxy.https = true;
s.proxy.port = 443;
s.proxy.tld = "localhost".to_string();
s
}
#[test]
fn test_build_proxy_url_port_suffix() {
let mut s = proxy_settings();
assert_eq!(
build_proxy_url(Some("api.myproj"), &s).as_deref(),
Some("https://api.myproj.localhost")
);
s.proxy.port = 8088;
assert_eq!(
build_proxy_url(Some("api.myproj"), &s).as_deref(),
Some("https://api.myproj.localhost:8088")
);
s.proxy.https = false;
s.proxy.port = 80;
assert_eq!(
build_proxy_url(Some("api.myproj"), &s).as_deref(),
Some("http://api.myproj.localhost")
);
}
#[test]
fn test_build_proxy_url_requires_enabled_proxy_and_host() {
let mut s = proxy_settings();
assert_eq!(build_proxy_url(None, &s), None);
s.proxy.enable = false;
assert_eq!(build_proxy_url(Some("api.myproj"), &s), None);
}
#[test]
fn test_build_proxy_url_uses_lan_tld() {
let mut s = proxy_settings();
s.proxy.tld = "test".to_string();
assert_eq!(effective_tld(&s), "test");
s.proxy.lan = true;
assert_eq!(effective_tld(&s), "local");
assert_eq!(
build_proxy_url(Some("api.myproj"), &s).as_deref(),
Some("https://api.myproj.local")
);
s.proxy.lan = false;
s.proxy.lan_ip = "192.168.1.42".to_string();
assert_eq!(
build_proxy_url(Some("api.myproj"), &s).as_deref(),
Some("https://api.myproj.local")
);
}
#[test]
fn test_build_proxy_url_rejects_invalid_port() {
let mut s = proxy_settings();
s.proxy.port = 0;
assert_eq!(build_proxy_url(Some("api.myproj"), &s), None);
s.proxy.port = 70000;
assert_eq!(build_proxy_url(Some("api.myproj"), &s), None);
}
}