├── assets └── login_page_screenshot.png ├── src ├── lib.rs ├── main.rs ├── state.rs ├── utils.rs ├── config.rs ├── app.rs └── proxy.rs ├── tests ├── config_test.rs ├── stress_test.rs ├── protect_test.rs ├── integration_test.rs └── comprehensive_test.rs ├── LICENSE ├── .gitignore ├── Cargo.toml ├── example_config.toml ├── login_page.html ├── README.md └── Cargo.lock /assets/login_page_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KBluePurple/totp-gateway/HEAD/assets/login_page_screenshot.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod config; 3 | pub mod proxy; 4 | pub mod state; 5 | pub mod utils; 6 | 7 | pub use app::App; 8 | pub use utils::{ClientIp, ParseError, ProxyError, SessionId, UpstreamAddr}; 9 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use totp_gateway::App; 3 | 4 | use mimalloc::MiMalloc; 5 | 6 | #[global_allocator] 7 | static GLOBAL: MiMalloc = MiMalloc; 8 | 9 | #[derive(Parser, Debug)] 10 | #[command(version, about, long_about = None)] 11 | struct Args { 12 | #[arg(short, long, default_value = "./config.toml")] 13 | config: String, 14 | } 15 | 16 | fn main() { 17 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 18 | 19 | let args = Args::parse(); 20 | 21 | let app = App::new(args.config); 22 | app.run(); 23 | } 24 | -------------------------------------------------------------------------------- /tests/config_test.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use totp_gateway::config::load_config; 4 | 5 | #[test] 6 | fn test_config_generation() { 7 | // Create target/tmp directory if it doesn't exist 8 | let tmp_dir = Path::new("target/tmp"); 9 | fs::create_dir_all(tmp_dir).unwrap(); 10 | 11 | let config_path = "target/tmp/test_gen_config.toml"; 12 | let path = Path::new(config_path); 13 | 14 | if path.exists() { 15 | fs::remove_file(path).unwrap(); 16 | } 17 | 18 | let config = load_config(path).expect("Failed to load/generate config"); 19 | 20 | assert!(path.exists(), "Config file should have been created"); 21 | 22 | let content = fs::read_to_string(path).unwrap(); 23 | assert!(content.contains("[server]")); 24 | assert!(content.contains("[auth]")); 25 | 26 | assert_eq!(config.server.bind_addr, "0.0.0.0:25000"); 27 | assert!(config.auth.totp_secret.is_some()); 28 | 29 | fs::remove_file(path).unwrap(); 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ore U 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use arc_swap::ArcSwap; 3 | use ipnet::IpNet; 4 | use moka::sync::Cache; 5 | use regex::Regex; 6 | use std::net::IpAddr; 7 | use std::sync::Arc; 8 | use std::sync::atomic::AtomicU64; 9 | 10 | pub const MAX_BODY_SIZE: usize = 4 * 1024; 11 | pub const MAX_IP_ENTRIES: u64 = 100_000; 12 | pub const MAX_SESSION_ENTRIES: u64 = 50_000; 13 | 14 | pub const TOTP_DIGITS: usize = 6; 15 | pub const TOTP_STEP_SECS: u64 = 30; 16 | pub const TOTP_SKEW: u64 = 1; 17 | 18 | pub const DEFAULT_HTTP_PORT: u16 = 80; 19 | 20 | pub const FILE_WATCH_DEBOUNCE_MS: u64 = 100; 21 | 22 | pub struct CompiledRoute { 23 | pub host: Option, 24 | pub path: Option, 25 | pub path_prefix: Option, 26 | pub upstream_addr: String, 27 | pub protect: bool, 28 | } 29 | 30 | pub struct RuntimeState { 31 | pub config: Config, 32 | pub secret: String, 33 | pub trusted_cidrs: Vec<(IpNet, String)>, 34 | pub routes: Vec, 35 | pub login_page_html: Arc, 36 | pub login_page_len: Arc, 37 | } 38 | 39 | pub struct ProxyState { 40 | pub runtime: ArcSwap, 41 | pub sessions: Cache, 42 | pub whitelist: Cache, 43 | pub blacklist: ArcSwap>, 44 | pub ip_limits: Cache, 45 | pub last_verified_step: AtomicU64, 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust build artifacts 2 | /target 3 | **/*.rs.bk 4 | *.pdb 5 | 6 | # Test temporary files 7 | /target/tmp/ 8 | 9 | # Cargo lock for libraries (keep for applications) 10 | # Cargo.lock 11 | 12 | # Test artifacts 13 | *.profraw 14 | *.profdata 15 | 16 | # Configuration files (contain secrets) 17 | config.toml 18 | secret.txt 19 | *.key 20 | *.pem 21 | *.crt 22 | 23 | # Keep example configs 24 | !example_config.toml 25 | 26 | # Test configuration files (now in target/tmp) 27 | # test_config_*.toml 28 | # adv_test_config_*.toml 29 | 30 | # IDE and editor files 31 | .vscode/ 32 | .idea/ 33 | *.swp 34 | *.swo 35 | *~ 36 | .DS_Store 37 | 38 | # Log files 39 | *.log 40 | logs/ 41 | 42 | # Coverage reports 43 | cobertura.xml 44 | tarpaulin-report.html 45 | *.gcda 46 | *.gcno 47 | 48 | # Flamegraph output 49 | flamegraph.svg 50 | perf.data 51 | perf.data.old 52 | 53 | # Benchmark results 54 | criterion/ 55 | 56 | # Documentation build 57 | /doc 58 | 59 | # Local development 60 | .env 61 | .env.local 62 | 63 | # macOS 64 | .AppleDouble 65 | .LSOverride 66 | 67 | # Windows 68 | Thumbs.db 69 | ehthumbs.db 70 | Desktop.ini 71 | $RECYCLE.BIN/ 72 | 73 | # Linux 74 | .directory 75 | .Trash-* 76 | 77 | # Backup files 78 | *.bak 79 | *.tmp 80 | *.temp 81 | 82 | # Lock files (keep Cargo.lock but exclude others) 83 | package-lock.json 84 | yarn.lock 85 | pnpm-lock.yaml 86 | 87 | # Release artifacts (local builds) 88 | *.tar.gz 89 | *.zip 90 | dist/ 91 | release/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "totp-gateway" 3 | version = "0.1.7" 4 | edition = "2024" 5 | authors = ["Ore U "] 6 | description = "A high-performance TOTP-based authentication reverse proxy gateway built on Cloudflare's Pingora framework for securing private network access" 7 | documentation = "https://docs.rs/totp-gateway" 8 | homepage = "https://github.com/KBluePurple/totp-gateway" 9 | repository = "https://github.com/KBluePurple/totp-gateway" 10 | license = "MIT" 11 | readme = "README.md" 12 | keywords = ["totp", "gateway", "pingora", "authentication", "proxy"] 13 | categories = ["network-programming", "authentication", "web-programming", "command-line-utilities"] 14 | exclude = [ 15 | "target/*", 16 | ".github/*", 17 | "tests/*", 18 | "*.log", 19 | "config.toml", 20 | ] 21 | rust-version = "1.85" 22 | 23 | [dependencies] 24 | pingora = { git = "https://github.com/KBluePurple/pingora", features = ["proxy"] } 25 | async-trait = "0.1" 26 | uuid = { version = "1.18.1", features = ["v4", "fast-rng"] } 27 | totp-rs = { version = "5.6", features = ["gen_secret"] } 28 | log = "0.4" 29 | env_logger = "0.11.8" 30 | bytes = "1.11.0" 31 | tokio = { version = "1.48.0", features = ["full"] } 32 | moka = { version = "0.12.11", features = ["sync"] } 33 | ipnet = "2.9" 34 | serde = { version = "1.0", features = ["derive"] } 35 | toml = "0.9.8" 36 | clap = { version = "4.5", features = ["derive"] } 37 | notify = "8.2.0" 38 | arc-swap = "1.7" 39 | regex = "1.12.2" 40 | url = "2.5.7" 41 | mimalloc = "0.1" 42 | 43 | [dev-dependencies] 44 | reqwest = { version = "0.12", features = ["json", "cookies"] } 45 | 46 | [[bin]] 47 | name = "totp-gateway" 48 | path = "src/main.rs" 49 | 50 | [profile.release] 51 | codegen-units = 1 52 | lto = true 53 | strip = true 54 | panic = "abort" 55 | opt-level = 3 -------------------------------------------------------------------------------- /example_config.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | # Address to bind the gateway server 3 | bind_addr = "0.0.0.0:25000" 4 | 5 | # Default upstream server (used when no route matches) 6 | default_upstream = "127.0.0.1:25001" 7 | 8 | # Trusted proxy configuration for real IP extraction 9 | # Format: [(CIDR, Header-Name)] 10 | 11 | trusted_proxies = [] 12 | 13 | # Cloudflared (Cloudflare Tunnel) Example: 14 | # When using Cloudflare Tunnel, cloudflared typically connects from localhost 15 | # and provides the real client IP in the CF-Connecting-IP header 16 | # trusted_proxies = [ 17 | # ["127.0.0.1/32", "CF-Connecting-IP"], # IPv4 localhost 18 | # ["::1/128", "CF-Connecting-IP"] # IPv6 localhost 19 | # ] 20 | 21 | # If cloudflared runs on a different machine in your private network: 22 | # trusted_proxies = [ 23 | # ["10.0.0.0/8", "CF-Connecting-IP"], 24 | # ["172.16.0.0/12", "CF-Connecting-IP"], 25 | # ["192.168.0.0/16", "CF-Connecting-IP"] 26 | # ] 27 | 28 | # For other reverse proxies: 29 | # trusted_proxies = [ 30 | # ["10.0.0.0/8", "X-Real-IP"], 31 | # ["172.16.0.0/12", "X-Forwarded-For"] 32 | # ] 33 | 34 | # TLS (SSL/HTTPS) Configuration 35 | # Uncomment this section to enable HTTPS. 36 | # Requires valid fullchain certificate and private key files. 37 | # [tls] 38 | # cert_file = "./certs/fullchain.pem" 39 | # key_file = "./certs/privkey.pem" 40 | 41 | # Security settings for failed login attempts 42 | [security] 43 | # Enable or disable the IP blacklist feature. Default is true. 44 | enabled = true 45 | 46 | # Maximum number of unique IPs to store in the blacklist. Default is 1000. 47 | blacklist_size = 1000 48 | 49 | # Strategy to use when the blacklist is full. 50 | # "overwrite" - Remove the oldest IP to make space for the new one. (Default) 51 | # "block" - Do not add new IPs until old ones are manually removed (or app restarts). 52 | blacklist_strategy = "overwrite" 53 | 54 | # Number of failed login attempts before an IP is blacklisted. Default is 5. 55 | max_retries = 5 56 | 57 | # Duration (in seconds) to track failed login attempts for an IP. Default is 3600 (1 hour). 58 | ip_limit_duration = 3600 59 | 60 | # Duration (in seconds) for which an IP remains blacklisted. Default is 3600 (1 hour). 61 | ban_duration = 3600 62 | 63 | # Duration (in seconds) for which a whitelisted IP remains trusted. Default is 604800 (7 days). 64 | whitelist_duration = 604800 65 | 66 | [auth] 67 | # TOTP secret (base32 encoded) 68 | # You can specify the secret in three ways (in order of precedence): 69 | # 1. Directly in config (totp_secret) 70 | # 2. From a file (totp_secret_file) 71 | # 3. From environment variable (totp_secret_env) 72 | 73 | # YOU MUST CHANGE THIS TO YOUR OWN SECRET 74 | totp_secret = "N48FJHFU3YD73H2NN48FJHFU3YD73H2N" 75 | # totp_secret_file = "./key/secret.txt" 76 | # totp_secret_env = "TOTP_SECRET" 77 | 78 | # Path to a custom login page HTML file. 79 | # If commented out, a default page is used. 80 | # login_page_file = "./login_page.html" 81 | 82 | # Duration (in seconds) for which a successful login session remains valid. Default is 1800 (30 minutes). 83 | session_duration = 1800 84 | 85 | # Route Configuration 86 | # Routes are matched in order. First match wins. 87 | # Both 'host' and 'path' support glob patterns: 88 | # - '*' matches zero or more characters 89 | # - '?' matches exactly one character 90 | # URL parameters (query strings) are ignored in matching. 91 | # 92 | # Each route can set `protect`: 93 | # - true (default): Apply gateway protection (session/TOTP, blacklist, etc.) 94 | # - false: Bypass protection and just proxy to upstream 95 | # 96 | # New format (host/path with glob patterns): 97 | [[routes]] 98 | # Match all subdomains of example.com 99 | host = "*.example.com" 100 | upstream_addr = "127.0.0.1:25001" 101 | protect = true 102 | 103 | [[routes]] 104 | # Match specific path pattern under any example.com subdomain 105 | host = "*.example.com" 106 | path = "/test/*" 107 | upstream_addr = "127.0.0.1:25002" 108 | protect = false # This route bypasses authentication/security 109 | 110 | [[routes]] 111 | # Match specific host and path 112 | host = "api.example.com" 113 | path = "/v1/*" 114 | upstream_addr = "127.0.0.1:25003" 115 | protect = true 116 | 117 | [[routes]] 118 | # Match only by path (any host) 119 | path = "/admin/*" 120 | upstream_addr = "127.0.0.1:25004" 121 | protect = true 122 | 123 | [[routes]] 124 | # Match only by host (any path) 125 | host = "legacy.example.com" 126 | upstream_addr = "127.0.0.1:25005" 127 | protect = false 128 | 129 | [[routes]] 130 | # Exact match example 131 | host = "exact.example.com" 132 | path = "/api/endpoint" 133 | upstream_addr = "127.0.0.1:25006" 134 | protect = true 135 | 136 | [[routes]] 137 | # You can mix path_prefix with host if needed 138 | host = "old.example.com" 139 | path_prefix = "/api" 140 | upstream_addr = "127.0.0.1:25008" 141 | protect = true 142 | -------------------------------------------------------------------------------- /tests/stress_test.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use std::fs; 3 | use std::io::Write; 4 | use std::path::Path; 5 | use std::thread; 6 | use std::time::Duration; 7 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 8 | use tokio::net::TcpListener; 9 | use totp_gateway::App; 10 | 11 | async fn start_silent_upstream(port: u16) { 12 | let addr = format!("127.0.0.1:{}", port); 13 | let listener = TcpListener::bind(&addr).await.unwrap(); 14 | 15 | tokio::spawn(async move { 16 | loop { 17 | if let Ok((mut socket, _)) = listener.accept().await { 18 | tokio::spawn(async move { 19 | let mut buf = [0; 1024]; 20 | let _ = socket.read(&mut buf).await; 21 | let response = 22 | "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; 23 | let _ = socket.write_all(response.as_bytes()).await; 24 | }); 25 | } 26 | } 27 | }); 28 | } 29 | 30 | #[tokio::test] 31 | async fn test_brute_force_blacklist() { 32 | let proxy_port = 25010; 33 | let upstream_port = 25011; 34 | 35 | start_silent_upstream(upstream_port).await; 36 | 37 | // Create target/tmp directory if it doesn't exist 38 | let tmp_dir = Path::new("target/tmp"); 39 | fs::create_dir_all(tmp_dir).unwrap(); 40 | 41 | // Start App 42 | let config_content = format!( 43 | r#" 44 | [server] 45 | bind_addr = "127.0.0.1:{}" 46 | default_upstream = "127.0.0.1:{}" 47 | trusted_proxies = [["127.0.0.1/32", "X-Forwarded-For"]] 48 | 49 | [security] 50 | enabled = true 51 | blacklist_size = 100 52 | 53 | [auth] 54 | totp_secret = "KRSXG5CTMVRXEZLUKRSXG5CTMVRXEZLU" 55 | 56 | [[routes]] 57 | path_prefix = "/" 58 | upstream_addr = "127.0.0.1:{}" 59 | "#, 60 | proxy_port, upstream_port, upstream_port 61 | ); 62 | 63 | let config_path = format!("target/tmp/stress_config_{}.toml", proxy_port); 64 | let mut file = fs::File::create(&config_path).unwrap(); 65 | file.write_all(config_content.as_bytes()).unwrap(); 66 | 67 | let path_for_thread = config_path.clone(); 68 | thread::spawn(move || { 69 | let app = App::new(path_for_thread); 70 | app.run(); 71 | }); 72 | 73 | tokio::time::sleep(Duration::from_secs(1)).await; 74 | 75 | let client = Client::new(); 76 | let url = format!("http://127.0.0.1:{}/auth", proxy_port); 77 | 78 | for i in 1..=4 { 79 | let params = [("code", "000000")]; 80 | let resp = client 81 | .post(&url) 82 | .form(¶ms) 83 | .send() 84 | .await 85 | .expect("Failed to send request"); 86 | 87 | assert_eq!(resp.status(), 200, "Attempt {} should be allowed", i); 88 | assert!(resp.url().query().unwrap_or("").contains("error=1")); 89 | } 90 | 91 | let params = [("code", "000000")]; 92 | let resp = client 93 | .post(&url) 94 | .form(¶ms) 95 | .send() 96 | .await 97 | .expect("Failed to send request"); 98 | 99 | assert_eq!( 100 | resp.status(), 101 | 429, 102 | "IP should be blacklisted on 5th attempt" 103 | ); 104 | 105 | let _ = fs::remove_file(config_path); 106 | } 107 | 108 | #[tokio::test] 109 | async fn test_ddos_simulation() { 110 | let proxy_port = 25020; 111 | let upstream_port = 25021; 112 | 113 | start_silent_upstream(upstream_port).await; 114 | 115 | // Create target/tmp directory if it doesn't exist 116 | let tmp_dir = Path::new("target/tmp"); 117 | fs::create_dir_all(tmp_dir).unwrap(); 118 | 119 | let config_content = format!( 120 | r#" 121 | [server] 122 | bind_addr = "127.0.0.1:{}" 123 | default_upstream = "127.0.0.1:{}" 124 | trusted_proxies = [["127.0.0.1/32", "X-Forwarded-For"]] 125 | 126 | [auth] 127 | totp_secret = "KRSXG5CTMVRXEZLUKRSXG5CTMVRXEZLU" 128 | 129 | [[routes]] 130 | path_prefix = "/" 131 | upstream_addr = "127.0.0.1:{}" 132 | "#, 133 | proxy_port, upstream_port, upstream_port 134 | ); 135 | 136 | let config_path = format!("target/tmp/stress_config_{}.toml", proxy_port); 137 | let mut file = fs::File::create(&config_path).unwrap(); 138 | file.write_all(config_content.as_bytes()).unwrap(); 139 | 140 | let path_for_thread = config_path.clone(); 141 | thread::spawn(move || { 142 | let app = App::new(path_for_thread); 143 | app.run(); 144 | }); 145 | 146 | tokio::time::sleep(Duration::from_secs(1)).await; 147 | 148 | let client = Client::new(); 149 | let url = format!("http://127.0.0.1:{}", proxy_port); 150 | 151 | let mut tasks = vec![]; 152 | for i in 0..100 { 153 | let client = client.clone(); 154 | let url = url.clone(); 155 | 156 | let task = tokio::spawn(async move { 157 | let ip = format!("10.0.0.{}", i % 255); 158 | 159 | let resp = client.get(&url).header("X-Forwarded-For", ip).send().await; 160 | 161 | match resp { 162 | Ok(r) => r.status().as_u16(), 163 | Err(_) => 0, 164 | } 165 | }); 166 | tasks.push(task); 167 | } 168 | 169 | let mut success_count = 0; 170 | for task in tasks { 171 | if let Ok(status) = task.await { 172 | if status == 200 { 173 | success_count += 1; 174 | } 175 | } 176 | } 177 | 178 | println!("DDoS Simulation: {}/100 requests succeeded", success_count); 179 | 180 | assert!( 181 | success_count > 90, 182 | "Server should handle concurrent requests under load" 183 | ); 184 | 185 | let _ = fs::remove_file(config_path); 186 | } -------------------------------------------------------------------------------- /login_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Secure Gateway 7 | 88 | 89 | 90 | 102 | 118 | 119 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::fmt; 3 | use std::net::IpAddr; 4 | use std::str::FromStr; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 7 | pub struct SessionId(String); 8 | 9 | impl SessionId { 10 | pub fn new(id: String) -> Self { 11 | Self(id) 12 | } 13 | 14 | pub fn as_str(&self) -> &str { 15 | &self.0 16 | } 17 | } 18 | 19 | impl fmt::Display for SessionId { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | write!(f, "{}", self.0) 22 | } 23 | } 24 | 25 | impl From for SessionId { 26 | fn from(s: String) -> Self { 27 | Self(s) 28 | } 29 | } 30 | 31 | #[derive(Debug, Clone, PartialEq, Eq)] 32 | pub struct UpstreamAddr { 33 | pub host: String, 34 | pub port: u16, 35 | } 36 | 37 | impl UpstreamAddr { 38 | pub fn new(host: String, port: u16) -> Self { 39 | Self { host, port } 40 | } 41 | } 42 | 43 | impl FromStr for UpstreamAddr { 44 | type Err = ParseError; 45 | 46 | fn from_str(s: &str) -> Result { 47 | let parts: Vec<&str> = s.split(':').collect(); 48 | match parts.as_slice() { 49 | [host, port_str] => { 50 | let port = port_str 51 | .parse::() 52 | .map_err(|_| ParseError::InvalidPort(port_str.to_string()))?; 53 | Ok(Self { 54 | host: host.to_string(), 55 | port, 56 | }) 57 | } 58 | [host] => Ok(Self { 59 | host: host.to_string(), 60 | port: 80, 61 | }), 62 | _ => Err(ParseError::InvalidFormat(s.to_string())), 63 | } 64 | } 65 | } 66 | 67 | impl fmt::Display for UpstreamAddr { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | write!(f, "{}:{}", self.host, self.port) 70 | } 71 | } 72 | 73 | #[derive(Debug, Clone)] 74 | pub enum ParseError { 75 | InvalidFormat(String), 76 | InvalidPort(String), 77 | InvalidRegex(String), 78 | } 79 | 80 | impl fmt::Display for ParseError { 81 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 82 | match self { 83 | ParseError::InvalidFormat(s) => write!(f, "Invalid format: {}", s), 84 | ParseError::InvalidPort(s) => write!(f, "Invalid port: {}", s), 85 | ParseError::InvalidRegex(s) => write!(f, "Invalid regex: {}", s), 86 | } 87 | } 88 | } 89 | 90 | impl std::error::Error for ParseError {} 91 | 92 | #[derive(Debug, Clone)] 93 | pub enum ProxyError { 94 | MissingClientIp, 95 | InvalidUpstream(String), 96 | TotpSecretInvalid, 97 | TotpCreationFailed, 98 | SessionTableFull, 99 | IpLimitTableFull, 100 | } 101 | 102 | impl fmt::Display for ProxyError { 103 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 104 | match self { 105 | ProxyError::MissingClientIp => write!(f, "Client IP address not found"), 106 | ProxyError::InvalidUpstream(s) => write!(f, "Invalid upstream address: {}", s), 107 | ProxyError::TotpSecretInvalid => write!(f, "TOTP secret is invalid"), 108 | ProxyError::TotpCreationFailed => write!(f, "Failed to create TOTP instance"), 109 | ProxyError::SessionTableFull => write!(f, "Session table is full"), 110 | ProxyError::IpLimitTableFull => write!(f, "IP limit table is full"), 111 | } 112 | } 113 | } 114 | 115 | impl std::error::Error for ProxyError {} 116 | 117 | pub struct ClientIp(IpAddr); 118 | 119 | impl ClientIp { 120 | pub fn new(ip: IpAddr) -> Self { 121 | Self(ip) 122 | } 123 | 124 | pub fn inner(&self) -> IpAddr { 125 | self.0 126 | } 127 | } 128 | 129 | impl From for ClientIp { 130 | fn from(ip: IpAddr) -> Self { 131 | Self(ip) 132 | } 133 | } 134 | 135 | pub fn glob_to_regex(pattern: &str) -> Result { 136 | let mut regex_str = String::from("^"); 137 | for c in pattern.chars() { 138 | match c { 139 | '*' => regex_str.push_str(".*"), 140 | '?' => regex_str.push('.'), 141 | '.' | '+' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => { 142 | regex_str.push('\\'); 143 | regex_str.push(c); 144 | } 145 | _ => regex_str.push(c), 146 | } 147 | } 148 | regex_str.push('$'); 149 | Regex::new(®ex_str).map_err(|e| ParseError::InvalidRegex(e.to_string())) 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use super::*; 155 | 156 | #[test] 157 | fn test_upstream_addr_parse() { 158 | let addr: UpstreamAddr = "127.0.0.1:8080".parse().unwrap(); 159 | assert_eq!(addr.host, "127.0.0.1"); 160 | assert_eq!(addr.port, 8080); 161 | 162 | let addr: UpstreamAddr = "example.com".parse().unwrap(); 163 | assert_eq!(addr.host, "example.com"); 164 | assert_eq!(addr.port, 80); 165 | 166 | assert!("invalid:port:extra".parse::().is_err()); 167 | assert!("host:abc".parse::().is_err()); 168 | } 169 | 170 | #[test] 171 | fn test_glob_to_regex() { 172 | let re = glob_to_regex("*.example.com").unwrap(); 173 | assert!(re.is_match("sub.example.com")); 174 | assert!(re.is_match("a.example.com")); 175 | assert!(!re.is_match("example.com")); 176 | 177 | let re = glob_to_regex("/api/*").unwrap(); 178 | assert!(re.is_match("/api/users")); 179 | assert!(re.is_match("/api/")); 180 | assert!(!re.is_match("/api")); 181 | 182 | let re = glob_to_regex("test?.com").unwrap(); 183 | assert!(re.is_match("test1.com")); 184 | assert!(re.is_match("testa.com")); 185 | assert!(!re.is_match("test.com")); 186 | assert!(!re.is_match("test12.com")); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::env; 3 | use std::fs; 4 | use std::path::Path; 5 | use totp_rs::Secret; 6 | 7 | #[derive(Debug, Deserialize, Serialize, Clone, Default)] 8 | pub struct Config { 9 | pub server: ServerConfig, 10 | #[serde(default)] 11 | pub tls: Option, 12 | pub auth: AuthConfig, 13 | #[serde(default)] 14 | pub security: SecurityConfig, 15 | #[serde(default)] 16 | pub routes: Vec, 17 | } 18 | 19 | #[derive(Debug, Deserialize, Serialize, Clone)] 20 | pub struct TlsConfig { 21 | pub cert_file: String, 22 | pub key_file: String, 23 | } 24 | 25 | #[derive(Debug, Deserialize, Serialize, Clone)] 26 | pub struct ServerConfig { 27 | pub bind_addr: String, 28 | pub default_upstream: String, 29 | #[serde(default)] 30 | pub trusted_proxies: Vec<(String, String)>, 31 | } 32 | 33 | impl Default for ServerConfig { 34 | fn default() -> Self { 35 | Self { 36 | bind_addr: "0.0.0.0:25000".to_string(), 37 | default_upstream: "127.0.0.1:25001".to_string(), 38 | trusted_proxies: vec![], 39 | } 40 | } 41 | } 42 | 43 | #[derive(Debug, Deserialize, Serialize, Clone)] 44 | pub struct AuthConfig { 45 | pub totp_secret: Option, 46 | pub totp_secret_file: Option, 47 | pub totp_secret_env: Option, 48 | pub login_page_file: Option, 49 | #[serde(default = "default_session_duration")] 50 | pub session_duration: u64, 51 | } 52 | 53 | impl Default for AuthConfig { 54 | fn default() -> Self { 55 | let secret = Secret::generate_secret(); 56 | let encoded = secret.to_encoded().to_string(); 57 | Self { 58 | totp_secret: Some(encoded), 59 | totp_secret_file: None, 60 | totp_secret_env: None, 61 | login_page_file: None, 62 | session_duration: default_session_duration(), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] 68 | #[serde(rename_all = "lowercase")] 69 | pub enum BlacklistStrategy { 70 | Overwrite, 71 | Block, 72 | } 73 | 74 | #[derive(Debug, Deserialize, Serialize, Clone)] 75 | pub struct SecurityConfig { 76 | #[serde(default = "default_security_enabled")] 77 | pub enabled: bool, 78 | #[serde(default = "default_blacklist_size")] 79 | pub blacklist_size: usize, 80 | #[serde(default = "default_blacklist_strategy")] 81 | pub blacklist_strategy: BlacklistStrategy, 82 | #[serde(default = "default_max_retries")] 83 | pub max_retries: u32, 84 | #[serde(default = "default_ip_limit_duration")] 85 | pub ip_limit_duration: u64, 86 | #[serde(default = "default_ban_duration")] 87 | pub ban_duration: u64, 88 | #[serde(default = "default_whitelist_duration")] 89 | pub whitelist_duration: u64, 90 | } 91 | 92 | fn default_session_duration() -> u64 { 93 | 1800 94 | } 95 | 96 | fn default_security_enabled() -> bool { 97 | true 98 | } 99 | 100 | fn default_blacklist_size() -> usize { 101 | 1000 102 | } 103 | 104 | fn default_blacklist_strategy() -> BlacklistStrategy { 105 | BlacklistStrategy::Overwrite 106 | } 107 | 108 | fn default_max_retries() -> u32 { 109 | 5 110 | } 111 | 112 | fn default_ip_limit_duration() -> u64 { 113 | 3600 114 | } 115 | 116 | fn default_ban_duration() -> u64 { 117 | 3600 118 | } 119 | 120 | fn default_whitelist_duration() -> u64 { 121 | 604800 122 | } 123 | 124 | fn default_route_protect() -> bool { 125 | true 126 | } 127 | 128 | impl Default for SecurityConfig { 129 | fn default() -> Self { 130 | Self { 131 | enabled: default_security_enabled(), 132 | blacklist_size: default_blacklist_size(), 133 | blacklist_strategy: default_blacklist_strategy(), 134 | max_retries: default_max_retries(), 135 | ip_limit_duration: default_ip_limit_duration(), 136 | ban_duration: default_ban_duration(), 137 | whitelist_duration: default_whitelist_duration(), 138 | } 139 | } 140 | } 141 | 142 | impl AuthConfig { 143 | pub fn get_secret(&self) -> Result { 144 | let secret = if let Some(s) = &self.totp_secret { 145 | s.clone() 146 | } else if let Some(path) = &self.totp_secret_file { 147 | fs::read_to_string(path) 148 | .map_err(|e| format!("Failed to read secret file {}: {}", path, e))? 149 | .trim() 150 | .to_string() 151 | } else if let Some(env_var) = &self.totp_secret_env { 152 | env::var(env_var).map_err(|_| format!("Environment variable {} not found", env_var))? 153 | } else { 154 | return Err( 155 | "No TOTP secret configured. Provide totp_secret, totp_secret_file, or totp_secret_env" 156 | .to_string(), 157 | ); 158 | }; 159 | 160 | if secret.is_empty() { 161 | return Err("TOTP secret is empty".to_string()); 162 | } 163 | 164 | Ok(secret.chars().filter(|c| !c.is_whitespace()).collect()) 165 | } 166 | } 167 | 168 | #[derive(Debug, Deserialize, Serialize, Clone)] 169 | pub struct RouteConfig { 170 | #[serde(default)] 171 | pub host: Option, 172 | #[serde(default)] 173 | pub path: Option, 174 | #[serde(default)] 175 | pub path_prefix: Option, 176 | pub upstream_addr: String, 177 | #[serde(default = "default_route_protect")] 178 | pub protect: bool, 179 | } 180 | 181 | pub fn load_config>(path: P) -> Result> { 182 | if !path.as_ref().exists() { 183 | let example = include_str!("../example_config.toml"); 184 | fs::write(path.as_ref(), example)?; 185 | } 186 | 187 | let content = fs::read_to_string(path)?; 188 | let config: Config = toml::from_str(&content)?; 189 | Ok(config) 190 | } 191 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::config::load_config; 2 | use crate::proxy::AuthGateway; 3 | use crate::state::{ 4 | CompiledRoute, FILE_WATCH_DEBOUNCE_MS, MAX_IP_ENTRIES, MAX_SESSION_ENTRIES, ProxyState, 5 | RuntimeState, 6 | }; 7 | use crate::utils::glob_to_regex; 8 | use arc_swap::ArcSwap; 9 | use ipnet::IpNet; 10 | use log::{error, info, warn}; 11 | use moka::sync::Cache; 12 | use notify::{RecommendedWatcher, RecursiveMode, Watcher}; 13 | use pingora::prelude::*; 14 | use std::fs; 15 | use std::path::{Path, PathBuf}; 16 | use std::sync::Arc; 17 | use std::sync::atomic::AtomicU64; 18 | use std::sync::mpsc::channel; 19 | use std::time::Duration; 20 | 21 | pub struct App { 22 | config_path: PathBuf, 23 | } 24 | 25 | impl App { 26 | pub fn new>(config_path: P) -> Self { 27 | Self { 28 | config_path: config_path.as_ref().to_path_buf(), 29 | } 30 | } 31 | 32 | fn load_runtime_state(path: &Path) -> Result { 33 | let config = load_config(path).map_err(|e| e.to_string())?; 34 | let secret = config.auth.get_secret().map_err(|e| e.to_string())?; 35 | 36 | let trusted_cidrs: Vec<(IpNet, String)> = config 37 | .server 38 | .trusted_proxies 39 | .iter() 40 | .filter_map(|(s, h)| { 41 | s.parse::() 42 | .map(|cidr| (cidr, h.clone())) 43 | .map_err(|e| { 44 | warn!("Failed to parse trusted proxy CIDR '{}': {}", s, e); 45 | e 46 | }) 47 | .ok() 48 | }) 49 | .collect(); 50 | 51 | let routes = config 52 | .routes 53 | .iter() 54 | .map(|r| { 55 | let host = r.host.as_ref().and_then(|h| { 56 | glob_to_regex(h) 57 | .map_err(|e| { 58 | warn!("Failed to compile host pattern '{}': {}", h, e); 59 | e 60 | }) 61 | .ok() 62 | }); 63 | 64 | let path = r.path.as_ref().and_then(|p| { 65 | glob_to_regex(p) 66 | .map_err(|e| { 67 | warn!("Failed to compile path pattern '{}': {}", p, e); 68 | e 69 | }) 70 | .ok() 71 | }); 72 | 73 | CompiledRoute { 74 | host, 75 | path, 76 | path_prefix: r.path_prefix.clone(), 77 | upstream_addr: r.upstream_addr.clone(), 78 | protect: r.protect, 79 | } 80 | }) 81 | .collect(); 82 | 83 | let login_page_html = match &config.auth.login_page_file { 84 | Some(path) => fs::read_to_string(path) 85 | .map_err(|e| format!("Failed to read login page file {}: {}", path, e))?, 86 | None => include_str!("../login_page.html").to_string(), 87 | }; 88 | 89 | let login_page_len = login_page_html.len().to_string(); 90 | 91 | Ok(RuntimeState { 92 | config, 93 | secret, 94 | trusted_cidrs, 95 | routes, 96 | login_page_html: Arc::new(login_page_html), 97 | login_page_len: Arc::new(login_page_len), 98 | }) 99 | } 100 | 101 | fn handle_config_reload(config_path: &Path, state: &Arc) { 102 | match Self::load_runtime_state(config_path) { 103 | Ok(new_runtime) => { 104 | let new_sec = &new_runtime.config.security; 105 | let old_sec = &state.runtime.load().config.security; 106 | 107 | if new_sec.blacklist_size != old_sec.blacklist_size 108 | || new_sec.ban_duration != old_sec.ban_duration 109 | { 110 | info!( 111 | "Blacklist config changed (Size: {}, Duration: {}s). Re-creating cache.", 112 | new_sec.blacklist_size, new_sec.ban_duration 113 | ); 114 | let new_blacklist = Cache::builder() 115 | .time_to_live(Duration::from_secs(new_sec.ban_duration)) 116 | .max_capacity(new_sec.blacklist_size as u64) 117 | .build(); 118 | state.blacklist.store(Arc::new(new_blacklist)); 119 | } 120 | 121 | state.runtime.store(Arc::new(new_runtime)); 122 | info!("Configuration reloaded successfully."); 123 | } 124 | Err(e) => { 125 | error!("Failed to reload configuration: {}", e); 126 | } 127 | } 128 | } 129 | 130 | pub fn run(self) { 131 | let initial_runtime = match Self::load_runtime_state(&self.config_path) { 132 | Ok(runtime) => runtime, 133 | Err(e) => { 134 | error!("Failed to load initial configuration: {}", e); 135 | std::process::exit(1); 136 | } 137 | }; 138 | 139 | let bind_addr = initial_runtime.config.server.bind_addr.clone(); 140 | let tls_config = initial_runtime.config.tls.clone(); 141 | 142 | let security_config = &initial_runtime.config.security; 143 | let auth_config = &initial_runtime.config.auth; 144 | 145 | let blacklist_size = security_config.blacklist_size as u64; 146 | let ban_duration = Duration::from_secs(security_config.ban_duration); 147 | let whitelist_duration = Duration::from_secs(security_config.whitelist_duration); 148 | let ip_limit_duration = Duration::from_secs(security_config.ip_limit_duration); 149 | let session_duration = Duration::from_secs(auth_config.session_duration); 150 | 151 | let initial_blacklist = Arc::new( 152 | Cache::builder() 153 | .time_to_live(ban_duration) 154 | .max_capacity(blacklist_size) 155 | .build(), 156 | ); 157 | 158 | let state = Arc::new(ProxyState { 159 | runtime: ArcSwap::new(Arc::new(initial_runtime)), 160 | sessions: Cache::builder() 161 | .time_to_live(session_duration) 162 | .max_capacity(MAX_SESSION_ENTRIES) 163 | .build(), 164 | whitelist: Cache::builder() 165 | .time_to_live(whitelist_duration) 166 | .max_capacity(MAX_IP_ENTRIES) 167 | .build(), 168 | blacklist: ArcSwap::new(initial_blacklist), 169 | ip_limits: Cache::builder() 170 | .time_to_live(ip_limit_duration) 171 | .max_capacity(MAX_IP_ENTRIES) 172 | .build(), 173 | last_verified_step: AtomicU64::new(0), 174 | }); 175 | 176 | let state_for_watcher = state.clone(); 177 | let config_path = self.config_path.clone(); 178 | 179 | std::thread::spawn(move || { 180 | let (tx, rx) = channel(); 181 | let mut watcher = match RecommendedWatcher::new(tx, notify::Config::default()) { 182 | Ok(w) => w, 183 | Err(e) => { 184 | error!("Failed to create file watcher: {}", e); 185 | return; 186 | } 187 | }; 188 | 189 | if let Err(e) = watcher.watch(&config_path, RecursiveMode::NonRecursive) { 190 | error!("Failed to watch config file: {}", e); 191 | return; 192 | } 193 | 194 | info!("Watching config file: {:?}", config_path); 195 | 196 | for res in rx { 197 | match res { 198 | Ok(event) => { 199 | if event.kind.is_modify() || event.kind.is_create() { 200 | info!("Config file changed. Reloading..."); 201 | std::thread::sleep(Duration::from_millis(FILE_WATCH_DEBOUNCE_MS)); 202 | Self::handle_config_reload(&config_path, &state_for_watcher); 203 | } 204 | } 205 | Err(e) => error!("Watch error: {}", e), 206 | } 207 | } 208 | }); 209 | 210 | let mut server = match Server::new(None) { 211 | Ok(s) => s, 212 | Err(e) => { 213 | error!("Failed to create server: {}", e); 214 | std::process::exit(1); 215 | } 216 | }; 217 | 218 | server.bootstrap(); 219 | 220 | let mut my_gateway = http_proxy_service(&server.configuration, AuthGateway { state }); 221 | 222 | if let Some(tls) = tls_config { 223 | if let Err(e) = my_gateway.add_tls(&bind_addr, &tls.cert_file, &tls.key_file) { 224 | error!("Failed to add TLS: {}", e); 225 | std::process::exit(1); 226 | } 227 | info!("Gateway Server running on {} (HTTPS)", bind_addr); 228 | } else { 229 | my_gateway.add_tcp(&bind_addr); 230 | info!("Gateway Server running on {} (HTTP)", bind_addr); 231 | } 232 | 233 | server.add_service(my_gateway); 234 | server.run_forever(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /tests/protect_test.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use std::fs; 3 | use std::io::Write; 4 | use std::path::Path; 5 | use std::thread; 6 | use std::time::Duration; 7 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 8 | use tokio::net::TcpListener; 9 | use totp_gateway::App; 10 | use totp_rs::{Algorithm, Secret, TOTP}; 11 | 12 | // Reuse the same secret format as other tests 13 | const TEST_SECRET: &str = "KRSXG5CTMVRXEZLUKRSXG5CTMVRXEZLU"; 14 | const TOTP_STEP_SECS: u64 = 30; 15 | 16 | // Short durations for faster tests 17 | const SHORT_BAN_DURATION: u64 = 1; 18 | const SHORT_SESSION_DURATION: u64 = 2; 19 | 20 | struct TestFile { 21 | path: String, 22 | } 23 | 24 | impl TestFile { 25 | fn new(filename: &str, content: &str) -> Self { 26 | let tmp_dir = Path::new("target/tmp"); 27 | fs::create_dir_all(tmp_dir).expect("Failed to create target/tmp directory"); 28 | 29 | let path = format!("target/tmp/{}", filename); 30 | let mut file = fs::File::create(&path).expect("Failed to create test file"); 31 | file.write_all(content.as_bytes()) 32 | .expect("Failed to write test file content"); 33 | Self { path } 34 | } 35 | } 36 | 37 | impl Drop for TestFile { 38 | fn drop(&mut self) { 39 | let _ = fs::remove_file(&self.path); 40 | } 41 | } 42 | 43 | async fn start_mock_upstream(port: u16) { 44 | let addr = format!("127.0.0.1:{}", port); 45 | let listener = TcpListener::bind(&addr).await.unwrap(); 46 | 47 | tokio::spawn(async move { 48 | loop { 49 | if let Ok((mut socket, _)) = listener.accept().await { 50 | tokio::spawn(async move { 51 | let mut buf = [0; 1024]; 52 | let _ = socket.read(&mut buf).await; 53 | let req_str = String::from_utf8_lossy(&buf); 54 | let first_line = req_str.lines().next().unwrap_or(""); 55 | 56 | let body = if first_line.contains("GET /admin") { 57 | "Admin Area" 58 | } else { 59 | "Hello World" 60 | }; 61 | 62 | let response = format!( 63 | "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", 64 | body.len(), body 65 | ); 66 | let _ = socket.write_all(response.as_bytes()).await; 67 | tokio::time::sleep(Duration::from_millis(5)).await; 68 | }); 69 | } 70 | } 71 | }); 72 | } 73 | 74 | fn generate_totp_code() -> String { 75 | let secret_bytes = Secret::Encoded(TEST_SECRET.to_string()) 76 | .to_bytes() 77 | .expect("Failed to decode secret"); 78 | let totp = TOTP::new(Algorithm::SHA1, 6, 1, TOTP_STEP_SECS, secret_bytes) 79 | .expect("Failed to create TOTP"); 80 | totp.generate_current().expect("Failed to generate TOTP code") 81 | } 82 | 83 | fn create_test_config(proxy_port: u16, upstream_port: u16) -> String { 84 | // Configure two routes: 85 | // - /public/* with protect=false (bypass security) 86 | // - /admin/* with protect=true (default) 87 | // Also use very small limits to speed up tests 88 | format!( 89 | r#" 90 | [server] 91 | bind_addr = "127.0.0.1:{proxy_port}" 92 | default_upstream = "127.0.0.1:{upstream_port}" 93 | trusted_proxies = [["127.0.0.1/32", "X-Forwarded-For"]] 94 | 95 | [security] 96 | enabled = true 97 | blacklist_size = 100 98 | blacklist_strategy = "overwrite" 99 | max_retries = 1 100 | ip_limit_duration = 10 101 | ban_duration = {ban} 102 | whitelist_duration = 60 103 | 104 | [auth] 105 | totp_secret = "{secret}" 106 | session_duration = {session} 107 | 108 | [[routes]] 109 | path = "/public/*" 110 | upstream_addr = "127.0.0.1:{upstream_port}" 111 | protect = false 112 | 113 | [[routes]] 114 | path = "/admin/*" 115 | upstream_addr = "127.0.0.1:{upstream_port}" 116 | protect = true 117 | "#, 118 | proxy_port = proxy_port, 119 | upstream_port = upstream_port, 120 | ban = SHORT_BAN_DURATION, 121 | secret = TEST_SECRET, 122 | session = SHORT_SESSION_DURATION 123 | ) 124 | } 125 | 126 | async fn start_test_server(proxy_port: u16, upstream_port: u16) -> TestFile { 127 | let config_content = create_test_config(proxy_port, upstream_port); 128 | let config_filename = format!("protect_test_config_{}.toml", proxy_port); 129 | let config_file = TestFile::new(&config_filename, &config_content); 130 | 131 | let config_path = format!("target/tmp/{}", config_filename); 132 | thread::spawn(move || { 133 | let app = App::new(config_path); 134 | app.run(); 135 | }); 136 | 137 | // Give the server a bit of time to start 138 | std::thread::sleep(Duration::from_millis(300)); 139 | config_file 140 | } 141 | 142 | #[tokio::test] 143 | async fn test_unprotected_route_bypasses_auth() { 144 | let _ = env_logger::builder().is_test(true).try_init(); 145 | let upstream_port = 26610; 146 | start_mock_upstream(upstream_port).await; 147 | 148 | let proxy_port = 26611; 149 | let _config = start_test_server(proxy_port, upstream_port).await; 150 | 151 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 152 | let client = Client::builder().cookie_store(true).build().unwrap(); 153 | 154 | // Should not redirect to login page; should return upstream response directly 155 | let resp = client 156 | .get(format!("{}/public/anything", base_url)) 157 | .send() 158 | .await 159 | .unwrap(); 160 | assert_eq!(resp.status(), 200); 161 | let body = resp.text().await.unwrap(); 162 | assert_eq!(body, "Hello World"); 163 | } 164 | 165 | #[tokio::test] 166 | async fn test_protected_route_still_requires_auth() { 167 | let _ = env_logger::builder().is_test(true).try_init(); 168 | let upstream_port = 26612; 169 | start_mock_upstream(upstream_port).await; 170 | 171 | let proxy_port = 26613; 172 | let _config = start_test_server(proxy_port, upstream_port).await; 173 | 174 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 175 | let auth_url = format!("{}/auth", base_url); 176 | let client = Client::builder().cookie_store(true).build().unwrap(); 177 | 178 | // Access protected path should show login page 179 | let resp = client 180 | .get(format!("{}/admin/secret", base_url)) 181 | .send() 182 | .await 183 | .unwrap(); 184 | let body = resp.text().await.unwrap(); 185 | assert!(body.contains("Access Gateway")); 186 | 187 | // Authenticate, then access should succeed 188 | let code = generate_totp_code(); 189 | client 190 | .post(&auth_url) 191 | .form(&[("code", &code)]) 192 | .send() 193 | .await 194 | .unwrap(); 195 | 196 | let resp = client 197 | .get(format!("{}/admin/secret", base_url)) 198 | .send() 199 | .await 200 | .unwrap(); 201 | assert_eq!(resp.text().await.unwrap(), "Admin Area"); 202 | } 203 | 204 | #[tokio::test] 205 | async fn test_default_upstream_is_protected() { 206 | let _ = env_logger::builder().is_test(true).try_init(); 207 | let upstream_port = 26614; 208 | start_mock_upstream(upstream_port).await; 209 | 210 | let proxy_port = 26615; 211 | let _config = start_test_server(proxy_port, upstream_port).await; 212 | 213 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 214 | let client = Client::builder().cookie_store(true).build().unwrap(); 215 | 216 | let resp = client.get(format!("{}/nomatch", base_url)).send().await.unwrap(); 217 | let body = resp.text().await.unwrap(); 218 | assert!(body.contains("Access Gateway"), "default upstream must be protected"); 219 | } 220 | 221 | #[tokio::test] 222 | async fn test_unprotected_route_ignores_blacklist() { 223 | let _ = env_logger::builder().is_test(true).try_init(); 224 | let upstream_port = 26616; 225 | start_mock_upstream(upstream_port).await; 226 | 227 | let proxy_port = 26617; 228 | let _config = start_test_server(proxy_port, upstream_port).await; 229 | 230 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 231 | let auth_url = format!("{}/auth", base_url); 232 | let client = Client::builder().cookie_store(true).build().unwrap(); 233 | 234 | // Trigger blacklist quickly by sending wrong code (max_retries = 1) 235 | client 236 | .post(&auth_url) 237 | .form(&[("code", &"000000")]) 238 | .send() 239 | .await 240 | .unwrap(); 241 | 242 | // Access to protected route should now be blocked (429), confirming blacklist took effect 243 | let resp = client 244 | .get(format!("{}/admin/secret", base_url)) 245 | .send() 246 | .await 247 | .unwrap(); 248 | // Could be a login page or 429 depending on implementation order; capture status then body 249 | let status = resp.status(); 250 | let maybe_body = resp.text().await.unwrap_or_default(); 251 | let blocked = maybe_body.is_empty() || status == 429 || maybe_body.contains("Too Many Requests"); 252 | assert!(blocked, "Expected protected route to be blocked after blacklist"); 253 | 254 | // But unprotected route must still pass through regardless of blacklist 255 | let resp = client 256 | .get(format!("{}/public/free", base_url)) 257 | .send() 258 | .await 259 | .unwrap(); 260 | assert_eq!(resp.status(), 200); 261 | let body = resp.text().await.unwrap(); 262 | assert_eq!(body, "Hello World"); 263 | } 264 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use std::fs; 3 | use std::io::Write; 4 | use std::path::Path; 5 | use std::thread; 6 | use std::time::Duration; 7 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 8 | use tokio::net::TcpListener; 9 | use totp_gateway::App; 10 | use totp_rs::{Algorithm, Secret, TOTP}; 11 | 12 | const TEST_SECRET: &str = "KRSXG5CTMVRXEZLUKRSXG5CTMVRXEZLU"; 13 | const TOTP_STEP_SECS: u64 = 30; 14 | 15 | struct TestFile { 16 | path: String, 17 | } 18 | 19 | impl TestFile { 20 | fn new(filename: &str, content: &str) -> Self { 21 | // Create target/tmp directory if it doesn't exist 22 | let tmp_dir = Path::new("target/tmp"); 23 | fs::create_dir_all(tmp_dir).expect("Failed to create target/tmp directory"); 24 | 25 | let path = format!("target/tmp/{}", filename); 26 | let mut file = fs::File::create(&path).expect("Failed to create test file"); 27 | file.write_all(content.as_bytes()) 28 | .expect("Failed to write test file content"); 29 | Self { path } 30 | } 31 | } 32 | 33 | impl Drop for TestFile { 34 | fn drop(&mut self) { 35 | let _ = fs::remove_file(&self.path); 36 | } 37 | } 38 | 39 | async fn start_mock_upstream(port: u16) { 40 | let addr = format!("127.0.0.1:{}", port); 41 | let listener = TcpListener::bind(&addr).await.unwrap(); 42 | 43 | tokio::spawn(async move { 44 | loop { 45 | if let Ok((mut socket, _)) = listener.accept().await { 46 | tokio::spawn(async move { 47 | let mut buf = [0; 1024]; 48 | let _ = socket.read(&mut buf).await; 49 | let response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\nConnection: close\r\n\r\nHello, World!"; 50 | let _ = socket.write_all(response.as_bytes()).await; 51 | }); 52 | } 53 | } 54 | }); 55 | } 56 | 57 | #[tokio::test] 58 | async fn test_integration_flow() { 59 | let _ = env_logger::builder().is_test(true).try_init(); 60 | let upstream_port = 25002; 61 | start_mock_upstream(upstream_port).await; 62 | 63 | let custom_login_page_filename = "custom_login_test.html"; 64 | let _login_page_file = TestFile::new(custom_login_page_filename, "My Custom Login Page"); 65 | 66 | let proxy_port = 25003; 67 | 68 | let config_content = format!( 69 | "[server]\nbind_addr = \"127.0.0.1:{}\"\ndefault_upstream = \"127.0.0.1:{}\"\ntrusted_proxies = [[\"127.0.0.1/32\", \"X-Forwarded-For\"]] 70 | 71 | [auth]\ntotp_secret = \"{}\"\nlogin_page_file = \"target/tmp/{}\"\n 72 | [[routes]] 73 | path_prefix = \"/\" 74 | upstream_addr = \"127.0.0.1:{}\"\n", 75 | proxy_port, upstream_port, TEST_SECRET, custom_login_page_filename, upstream_port 76 | ); 77 | 78 | let config_filename = format!("test_config_{}.toml", proxy_port); 79 | let _config_file = TestFile::new(&config_filename, &config_content); 80 | 81 | let config_path = format!("target/tmp/{}", config_filename); 82 | thread::spawn(move || { 83 | let app = App::new(config_path); 84 | app.run(); 85 | }); 86 | 87 | tokio::time::sleep(Duration::from_secs(1)).await; 88 | 89 | let client = Client::builder().cookie_store(true).build().unwrap(); 90 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 91 | 92 | let resp = client 93 | .get(&base_url) 94 | .send() 95 | .await 96 | .expect("Failed to connect to proxy"); 97 | assert_eq!(resp.status(), 200); 98 | let body = resp.text().await.unwrap(); 99 | assert!(body.contains("My Custom Login Page")); 100 | 101 | let params = [("code", "000000")]; 102 | let resp = client 103 | .post(format!("{}/auth", base_url)) 104 | .form(¶ms) 105 | .send() 106 | .await 107 | .expect("Failed to send login"); 108 | 109 | assert_eq!(resp.status(), 200); 110 | assert!(resp.url().query().unwrap().contains("error=1")); 111 | 112 | let secret_bytes = Secret::Encoded(TEST_SECRET.to_string()) 113 | .to_bytes() 114 | .expect("Failed to decode secret"); 115 | let totp = TOTP::new(Algorithm::SHA1, 6, 1, TOTP_STEP_SECS, secret_bytes) 116 | .expect("Failed to create TOTP"); 117 | let code = totp 118 | .generate_current() 119 | .expect("Failed to generate TOTP code"); 120 | 121 | let params = [("code", code)]; 122 | let resp = client 123 | .post(format!("{}/auth", base_url)) 124 | .form(¶ms) 125 | .send() 126 | .await 127 | .expect("Failed to send login"); 128 | 129 | assert_eq!(resp.status(), 200); 130 | let body = resp.text().await.unwrap(); 131 | assert_eq!(body, "Hello, World!"); 132 | 133 | let resp = client 134 | .get(&base_url) 135 | .send() 136 | .await 137 | .expect("Failed to connect with session"); 138 | assert_eq!(resp.status(), 200); 139 | let body = resp.text().await.unwrap(); 140 | assert_eq!(body, "Hello, World!"); 141 | } 142 | 143 | #[tokio::test] 144 | async fn test_blacklist_flow() { 145 | let _ = env_logger::builder().is_test(true).try_init(); 146 | let upstream_port = 25004; 147 | start_mock_upstream(upstream_port).await; 148 | 149 | let proxy_port = 25005; 150 | 151 | let config_content = format!( 152 | "[server]\nbind_addr = \"127.0.0.1:{}\"\ndefault_upstream = \"127.0.0.1:{}\"\ntrusted_proxies = [[\"127.0.0.1/32\", \"X-Forwarded-For\"]] 153 | 154 | [security]\nenabled = true 155 | blacklist_size = 100 156 | blacklist_strategy = \"overwrite\" 157 | 158 | [auth]\ntotp_secret = \"{}\"\n", 159 | proxy_port, upstream_port, TEST_SECRET 160 | ); 161 | 162 | let config_filename = format!("test_config_{}.toml", proxy_port); 163 | let _config_file = TestFile::new(&config_filename, &config_content); 164 | 165 | let config_path = format!("target/tmp/{}", config_filename); 166 | thread::spawn(move || { 167 | let app = App::new(config_path); 168 | app.run(); 169 | }); 170 | 171 | tokio::time::sleep(Duration::from_secs(1)).await; 172 | 173 | let client = Client::builder().cookie_store(true).build().unwrap(); 174 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 175 | let auth_url = format!("{}/auth", base_url); 176 | 177 | for i in 1..=300 { 178 | let ip = format!("1.2.3.{}", i); 179 | for _ in 0..5 { 180 | client 181 | .post(&auth_url) 182 | .form(&[("code", "000000")]) 183 | .header("X-Forwarded-For", &ip) 184 | .send() 185 | .await 186 | .unwrap(); 187 | } 188 | } 189 | 190 | tokio::time::sleep(Duration::from_millis(1000)).await; 191 | 192 | let mut blacklisted_count = 0; 193 | for i in 1..=300 { 194 | let ip = format!("1.2.3.{}", i); 195 | let resp = client 196 | .get(&base_url) 197 | .header("X-Forwarded-For", &ip) 198 | .send() 199 | .await 200 | .unwrap(); 201 | 202 | if resp.status() == 429 { 203 | blacklisted_count += 1; 204 | } 205 | } 206 | 207 | println!( 208 | "Total blacklisted IPs found: {} (Limit: 100)", 209 | blacklisted_count 210 | ); 211 | 212 | assert!( 213 | blacklisted_count <= 250, 214 | "Blacklist size exceeded limit significantly. Found {} blacklisted IPs, limit is 100.", 215 | blacklisted_count 216 | ); 217 | assert!( 218 | blacklisted_count >= 80, 219 | "Blacklist seems too empty? Found {} blacklisted IPs.", 220 | blacklisted_count 221 | ); 222 | } 223 | 224 | #[tokio::test] 225 | async fn test_blacklist_block_strategy() { 226 | let _ = env_logger::builder().is_test(true).try_init(); 227 | let upstream_port = 25006; 228 | start_mock_upstream(upstream_port).await; 229 | 230 | let proxy_port = 25007; 231 | 232 | let config_content = format!( 233 | "[server]\nbind_addr = \"127.0.0.1:{}\"\ndefault_upstream = \"127.0.0.1:{}\"\ntrusted_proxies = [[\"127.0.0.1/32\", \"X-Forwarded-For\"]] 234 | 235 | [security]\nenabled = true 236 | blacklist_size = 1 237 | blacklist_strategy = \"block\" 238 | 239 | [auth]\ntotp_secret = \"{}\"\n", 240 | proxy_port, upstream_port, TEST_SECRET 241 | ); 242 | 243 | let config_filename = format!("test_config_{}.toml", proxy_port); 244 | let _config_file = TestFile::new(&config_filename, &config_content); 245 | 246 | let config_path = format!("target/tmp/{}", config_filename); 247 | thread::spawn(move || { 248 | let app = App::new(config_path); 249 | app.run(); 250 | }); 251 | 252 | tokio::time::sleep(Duration::from_secs(1)).await; 253 | 254 | let client = Client::new(); 255 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 256 | let auth_url = format!("{}/auth", base_url); 257 | 258 | for _ in 0..5 { 259 | client 260 | .post(&auth_url) 261 | .form(&[("code", "000000")]) 262 | .header("X-Forwarded-For", "1.1.1.1") 263 | .send() 264 | .await 265 | .unwrap(); 266 | } 267 | let resp = client 268 | .get(&base_url) 269 | .header("X-Forwarded-For", "1.1.1.1") 270 | .send() 271 | .await 272 | .unwrap(); 273 | assert_eq!(resp.status(), 429, "IP 1.1.1.1 should be blacklisted"); 274 | 275 | for _ in 0..5 { 276 | client 277 | .post(&auth_url) 278 | .form(&[("code", "000000")]) 279 | .header("X-Forwarded-For", "2.2.2.2") 280 | .send() 281 | .await 282 | .unwrap(); 283 | } 284 | let resp = client 285 | .get(&base_url) 286 | .header("X-Forwarded-For", "2.2.2.2") 287 | .send() 288 | .await 289 | .unwrap(); 290 | assert_eq!(resp.status(), 200, "IP 2.2.2.2 should NOT be blacklisted"); 291 | 292 | let resp = client 293 | .get(&base_url) 294 | .header("X-Forwarded-For", "1.1.1.1") 295 | .send() 296 | .await 297 | .unwrap(); 298 | assert_eq!(resp.status(), 429, "IP 1.1.1.1 should still be blacklisted"); 299 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TOTP Gateway 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/totp-gateway.svg)](https://crates.io/crates/totp-gateway) 4 | [![Documentation](https://docs.rs/totp-gateway/badge.svg)](https://docs.rs/totp-gateway) 5 | [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) 6 | [![Rust](https://img.shields.io/badge/rust-1.85%2B-orange)](https://www.rust-lang.org) 7 | [![Powered By](https://img.shields.io/badge/powered%20by-Pingora-F38020)](https://github.com/cloudflare/pingora) 8 | 9 | **TOTP Gateway** is a lightweight, high-performance reverse proxy authentication gateway built on 10 | Cloudflare's [Pingora](https://github.com/cloudflare/pingora) framework. 11 | 12 | It adds a **Two-Factor Authentication (TOTP)** layer in front of your internal admin panels, private tools, or sensitive 13 | API endpoints, effectively blocking unauthorized access. You can enhance security with just a single binary and a 14 | configuration file, without setting up complex authentication infrastructure. 15 | 16 | ## ✨ Key Features 17 | 18 | * **🔒 TOTP-based 2FA Enforcement**: Compatible with standard apps like Google Authenticator and Authy. 19 | * **🚀 High Performance**: Built on Pingora's asynchronous architecture for speed and stability. 20 | * **🛡️ Robust Security Policies**: 21 | * IP Blacklisting & Automatic Blocking (Brute-force protection). 22 | * Configurable login attempts and ban duration. 23 | * Session expiration control. 24 | * **🌐 Flexible Routing**: Supports Glob pattern matching for Hostnames (Subdomains) and Paths, plus per‑route protection 25 | toggle via `protect`. 26 | * **🔐 SSL/HTTPS Support**: Easily enable TLS via configuration. 27 | * **🔄 Hot Reload**: Apply configuration changes (`config.toml`) instantly without downtime. 28 | * **🎨 Custom Login Page**: Fully customizable HTML login interface. 29 | 30 | ![Login Page](./assets/login_page_screenshot.png) 31 | 32 | ## 🆕 Changelog 33 | 34 | ### v 0.1.7 35 | 36 | - Fix: Fix tokio runtime bug. 37 | 38 | ### v 0.1.6 39 | 40 | - **Note**: This version is not published to crates.io until [Pingora PR #425](https://github.com/cloudflare/pingora/pull/425) is merged. 41 | - Fix: On Windows, the program graceful shutdown immediately after startup. 42 | - Feature: Add `mimalloc` for faster, stabile memory allocation. 43 | 44 | ### v 0.1.5 45 | 46 | - Fix: Change wrong response settings. 47 | 48 | ### v 0.1.4 49 | 50 | - Change: Make can cache non-protected routes by default. 51 | 52 | ### v 0.1.3 53 | 54 | - Feature: Per‑route `protect` option to choose whether a route is secured by the gateway or proxied directly. Defaults 55 | to `true` for full backward compatibility. See the updated examples above and `example_config.toml`. 56 | 57 | ### v 0.1.2 58 | 59 | - Fix: Make don't cache any pages by default. (Issued when use with Cloudflare Tunnel) 60 | 61 | ## 📦 Installation 62 | 63 | > [!IMPORTANT] 64 | > Starting from **v0.1.6**, this crate will **not be published to crates.io** until the upstream [Pingora PR #425](https://github.com/cloudflare/pingora/pull/425) (which has been pending for over a year) is merged. 65 | > 66 | > **Reason**: To fix the immediate shutdown issue on Windows, i am currently using a forked version of Pingora. Since `crates.io` does not support dependencies specified via Git URLs (only version numbers), we cannot publish this version. Please use the **Build from Source** option below. 67 | 68 | ### Option 1: Install from crates.io (Recommended for v0.1.5 and below) 69 | 70 | ```bash 71 | cargo install totp-gateway 72 | ``` 73 | 74 | ### Option 2: Build from Source 75 | 76 | #### Prerequisites 77 | 78 | * Rust Toolchain (1.85 or later recommended) 79 | 80 | ```bash 81 | git clone https://github.com/your-username/totp-gateway.git 82 | cd totp-gateway 83 | cargo build --release 84 | ``` 85 | 86 | The compiled binary will be located at `target/release/totp-gateway` (or `totp-gateway.exe` on Windows). 87 | 88 | ## 🚀 Quick Start 89 | 90 | 1. **Generate Config**: Run the binary. If no config exists, it will automatically create `config.toml` from the default 91 | template. 92 | 93 | ```bash 94 | ./target/release/totp-gateway 95 | ``` 96 | 97 | 2. **Edit Config**: Open `config.toml` and configure `bind_addr` and `totp_secret` to match your environment. 98 | 99 | 3. **Run**: 100 | 101 | ```bash 102 | ./target/release/totp-gateway --config config.toml 103 | ``` 104 | 105 | ## ⚙️ Configuration Guide 106 | 107 | The `config.toml` file consists of the following sections: 108 | 109 | ### 1. Server Settings (`[server]`) 110 | 111 | ```toml 112 | [server] 113 | bind_addr = "0.0.0.0:25000" # Listening address and port 114 | default_upstream = "127.0.0.1:8080" # Default upstream server if no route matches 115 | 116 | # Trusted proxy configuration for real IP extraction 117 | # Essential when running behind Cloudflare Tunnel, Nginx, etc. 118 | trusted_proxies = [ 119 | ["127.0.0.1/32", "CF-Connecting-IP"], 120 | ["10.0.0.0/8", "X-Forwarded-For"] 121 | ] 122 | ``` 123 | 124 | ### 2. Authentication & Session (`[auth]`) 125 | 126 | ```toml 127 | [auth] 128 | # TOTP Secret (Base32 encoded). Register this key in your Authenticator app. 129 | totp_secret = "JBSWY3DPEHPK3PXP" 130 | # Load from file: totp_secret_file = "./secret.txt" 131 | # Load from env: totp_secret_env = "MY_TOTP_SECRET" 132 | 133 | # Path to custom login HTML file (optional) 134 | login_page_file = "./login_page.html" 135 | 136 | # Session duration in seconds. Default: 1800 (30 mins) 137 | session_duration = 3600 138 | ``` 139 | 140 | ### 3. Security Policy (`[security]`) 141 | 142 | Core settings to defend against Brute-force attacks. 143 | 144 | ```toml 145 | [security] 146 | enabled = true # Enable security features 147 | 148 | max_retries = 5 # Max failed attempts before banning 149 | ban_duration = 3600 # IP ban duration (seconds) 150 | ip_limit_duration = 3600 # Time window for tracking failures (seconds) 151 | 152 | blacklist_size = 1000 # Max IPs in blacklist 153 | blacklist_strategy = "overwrite" # "overwrite" (remove oldest) or "block" (reject new) 154 | ``` 155 | 156 | ### 4. HTTPS/TLS (`[tls]`) 157 | 158 | If this section is present, the server runs in HTTPS mode. 159 | 160 | ```toml 161 | [tls] 162 | cert_file = "./certs/fullchain.pem" # Path to certificate 163 | key_file = "./certs/privkey.pem" # Path to private key 164 | ``` 165 | 166 | ### 5. Routing Rules (`[[routes]]`) 167 | 168 | Define multiple routes. Matched in order from top to bottom. 169 | 170 | - Matching fields: 171 | - `host`: glob pattern for hostname (e.g., `*.example.com`) 172 | - `path`: glob pattern for path (e.g., `/admin/*`) 173 | - `path_prefix`: simple prefix match for path (checked before host/path globs) 174 | - Upstream target: 175 | - `upstream_addr`: target `host:port` 176 | - Protection control: 177 | - `protect` (bool, default `true`): when `false`, the route bypasses authentication, sessions, blacklist, etc. 178 | 179 | ```toml 180 | # 1. Route specific path to a different port (protected by default) 181 | [[routes]] 182 | path_prefix = "/admin" 183 | upstream_addr = "127.0.0.1:9090" 184 | protect = true 185 | 186 | # 2. Subdomain matching (unprotected example) 187 | [[routes]] 188 | host = "api.example.com" 189 | upstream_addr = "127.0.0.1:3000" 190 | protect = false # bypass login and security for this route 191 | 192 | # 3. Wildcard support 193 | [[routes]] 194 | host = "*.internal.com" 195 | path = "/legacy/*" 196 | upstream_addr = "127.0.0.1:4000" 197 | protect = true 198 | ``` 199 | 200 | Tip: 201 | 202 | - If no route matches, traffic goes to `server.default_upstream` and remains protected (equivalent to `protect = true`). 203 | - If you need a public endpoint, define an explicit route with `protect = false`. 204 | 205 | ## 🛡️ How It Works 206 | 207 | 1. User requests a resource via **TOTP Gateway**. 208 | 2. Gateway checks for a valid Session Cookie (`SID`). 209 | 3. **No Session**: 210 | * User is redirected to the Login Page. 211 | * User enters the TOTP code. 212 | * **Success**: A session cookie is issued, and the request is proxied to the upstream server. 213 | * **Failure**: Failure count increments. If it exceeds the limit, the IP is **Banned** temporarily. 214 | 4. **Valid Session**: Request is immediately proxied to the upstream server. 215 | 5. **Unprotected Route (`protect=false`)**: The gateway skips authentication and blacklist checks and proxies the 216 | request directly. 217 | 218 | ### Security Notes 219 | 220 | - The fallback route (when no `[[routes]]` matches) is always protected. To expose a public endpoint, create a specific 221 | route and set `protect = false`. 222 | - Be careful not to create a `protect = false` route that matches `/auth` unintentionally; doing so would proxy the 223 | login endpoint to your upstream instead of handling authentication. 224 | - Blacklist and rate limiting do not apply to `protect = false` routes. 225 | 226 | ### Migration 227 | 228 | - The `protect` field is optional and defaults to `true`. Existing configs without `protect` continue to work unchanged. 229 | 230 | ### Example: Mixed protected/unprotected 231 | 232 | ```toml 233 | [server] 234 | bind_addr = "0.0.0.0:25000" 235 | default_upstream = "127.0.0.1:8080" 236 | 237 | [auth] 238 | totp_secret = "JBSWY3DPEHPK3PXP" 239 | 240 | [[routes]] 241 | # Public healthcheck endpoint 242 | path = "/health" 243 | upstream_addr = "127.0.0.1:8080" 244 | protect = false 245 | 246 | [[routes]] 247 | # Protected admin area 248 | path = "/admin/*" 249 | upstream_addr = "127.0.0.1:9090" 250 | protect = true 251 | ``` 252 | 253 | ## 📊 Performance 254 | 255 | Built on Cloudflare's Pingora framework, TOTP Gateway delivers: 256 | 257 | - **Low Latency**: Sub-millisecond proxy overhead 258 | - **High Throughput**: Handles thousands of concurrent connections 259 | - **Memory Efficient**: Minimal resource footprint 260 | - **Production Ready**: Battle-tested async runtime 261 | 262 | ## 🧪 Testing 263 | 264 | Run the full test suite: 265 | 266 | ```bash 267 | # Run all tests 268 | cargo test 269 | 270 | # Run with verbose output 271 | cargo test -- --nocapture 272 | 273 | # Run specific test 274 | cargo test test_basic_auth_and_replay_protection 275 | ``` 276 | 277 | The test suite includes: 278 | 279 | - Unit tests for core components 280 | - Integration tests with mock upstream servers 281 | - Security feature tests (rate limiting, IP banning) 282 | - Session management tests 283 | - Concurrent request handling tests 284 | - Route protection tests (`protect = true/false`) validating bypass and enforcement behavior 285 | 286 | ## 🤝 Contributing 287 | 288 | Bug reports, feature suggestions, and Pull Requests are welcome! 289 | 290 | 1. Fork the repository. 291 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`). 292 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`). 293 | 4. Push to the Branch (`git push origin feature/AmazingFeature`). 294 | 5. Open a Pull Request. 295 | 296 | ## 📝 License 297 | 298 | Distributed under the [MIT License](LICENSE). 299 | -------------------------------------------------------------------------------- /tests/comprehensive_test.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use std::fs; 3 | use std::io::Write; 4 | use std::path::Path; 5 | use std::thread; 6 | use std::time::Duration; 7 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 8 | use tokio::net::TcpListener; 9 | use totp_gateway::App; 10 | use totp_rs::{Algorithm, Secret, TOTP}; 11 | 12 | const TEST_SECRET: &str = "KRSXG5CTMVRXEZLUKRSXG5CTMVRXEZLU"; 13 | const TOTP_STEP_SECS: u64 = 30; 14 | 15 | // Use shorter durations for testing 16 | const SHORT_BAN_DURATION: u64 = 1; // 1 second instead of 2 17 | const SHORT_SESSION_DURATION: u64 = 2; // 2 seconds instead of 3 18 | 19 | struct TestFile { 20 | path: String, 21 | } 22 | 23 | impl TestFile { 24 | fn new(filename: &str, content: &str) -> Self { 25 | // Create target/tmp directory if it doesn't exist 26 | let tmp_dir = Path::new("target/tmp"); 27 | fs::create_dir_all(tmp_dir).expect("Failed to create target/tmp directory"); 28 | 29 | let path = format!("target/tmp/{}", filename); 30 | let mut file = fs::File::create(&path).expect("Failed to create test file"); 31 | file.write_all(content.as_bytes()) 32 | .expect("Failed to write test file content"); 33 | Self { path } 34 | } 35 | } 36 | 37 | impl Drop for TestFile { 38 | fn drop(&mut self) { 39 | let _ = fs::remove_file(&self.path); 40 | } 41 | } 42 | 43 | async fn start_mock_upstream(port: u16) { 44 | let addr = format!("127.0.0.1:{}", port); 45 | let listener = TcpListener::bind(&addr).await.unwrap(); 46 | 47 | tokio::spawn(async move { 48 | loop { 49 | if let Ok((mut socket, _)) = listener.accept().await { 50 | tokio::spawn(async move { 51 | let mut buf = [0; 1024]; 52 | let _ = socket.read(&mut buf).await; 53 | let req_str = String::from_utf8_lossy(&buf); 54 | let first_line = req_str.lines().next().unwrap_or(""); 55 | 56 | let body = if first_line.contains("GET /admin") { 57 | "Admin Area" 58 | } else { 59 | "Hello World" 60 | }; 61 | 62 | let response = format!( 63 | "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", 64 | body.len(), 65 | body 66 | ); 67 | let _ = socket.write_all(response.as_bytes()).await; 68 | tokio::time::sleep(Duration::from_millis(5)).await; 69 | }); 70 | } 71 | } 72 | }); 73 | } 74 | 75 | fn create_test_config( 76 | proxy_port: u16, 77 | upstream_port: u16, 78 | ban_duration: u64, 79 | session_duration: u64, 80 | ) -> String { 81 | format!( 82 | "[server]\nbind_addr = \"127.0.0.1:{}\"\ndefault_upstream = \"127.0.0.1:{}\"\ntrusted_proxies = [[\"127.0.0.1/32\", \"X-Forwarded-For\"]] 83 | 84 | [security]\nenabled = true\nblacklist_size = 100\nblacklist_strategy = \"overwrite\"\nmax_retries = 3\nban_duration = {}\nsession_duration = {} 85 | 86 | [auth]\ntotp_secret = \"{}\"\nsession_duration = {} 87 | 88 | [[routes]] 89 | path_prefix = \"/admin\" 90 | upstream_addr = \"127.0.0.1:{}\"\n", 91 | proxy_port, upstream_port, ban_duration, session_duration, TEST_SECRET, session_duration, upstream_port 92 | ) 93 | } 94 | 95 | async fn start_test_server( 96 | proxy_port: u16, 97 | upstream_port: u16, 98 | ban_duration: u64, 99 | session_duration: u64, 100 | ) -> TestFile { 101 | let config_content = 102 | create_test_config(proxy_port, upstream_port, ban_duration, session_duration); 103 | let config_filename = format!("test_config_{}.toml", proxy_port); 104 | let config_file = TestFile::new(&config_filename, &config_content); 105 | 106 | let config_path = format!("target/tmp/{}", config_filename); 107 | thread::spawn(move || { 108 | let app = App::new(config_path); 109 | app.run(); 110 | }); 111 | 112 | tokio::time::sleep(Duration::from_millis(500)).await; 113 | config_file 114 | } 115 | 116 | fn generate_totp_code() -> String { 117 | let secret_bytes = Secret::Encoded(TEST_SECRET.to_string()) 118 | .to_bytes() 119 | .expect("Failed to decode secret"); 120 | let totp = TOTP::new(Algorithm::SHA1, 6, 1, TOTP_STEP_SECS, secret_bytes) 121 | .expect("Failed to create TOTP"); 122 | totp.generate_current() 123 | .expect("Failed to generate TOTP code") 124 | } 125 | 126 | #[tokio::test] 127 | async fn test_basic_auth_and_replay_protection() { 128 | let _ = env_logger::builder().is_test(true).try_init(); 129 | 130 | let upstream_port = 26100; 131 | start_mock_upstream(upstream_port).await; 132 | 133 | let proxy_port = 26101; 134 | let _config_file = start_test_server( 135 | proxy_port, 136 | upstream_port, 137 | SHORT_BAN_DURATION, 138 | SHORT_SESSION_DURATION, 139 | ) 140 | .await; 141 | 142 | let client = Client::builder().cookie_store(true).build().unwrap(); 143 | let auth_url = format!("http://127.0.0.1:{}/auth", proxy_port); 144 | 145 | let code = generate_totp_code(); 146 | 147 | // Test successful authentication 148 | let resp = client 149 | .post(&auth_url) 150 | .form(&[("code", &code)]) 151 | .header("X-Forwarded-For", "10.0.1.1") 152 | .send() 153 | .await 154 | .unwrap(); 155 | assert_eq!(resp.status(), 200, "Authentication should succeed"); 156 | 157 | // Test replay attack protection 158 | let client2 = Client::builder().cookie_store(true).build().unwrap(); 159 | let resp = client2 160 | .post(&auth_url) 161 | .form(&[("code", &code)]) 162 | .header("X-Forwarded-For", "10.0.1.2") 163 | .send() 164 | .await 165 | .unwrap(); 166 | assert!( 167 | resp.url().query().unwrap_or("").contains("error=1"), 168 | "Replay attack should fail" 169 | ); 170 | } 171 | 172 | #[tokio::test] 173 | async fn test_blacklist_and_ban_expiry() { 174 | let _ = env_logger::builder().is_test(true).try_init(); 175 | 176 | let upstream_port = 26200; 177 | start_mock_upstream(upstream_port).await; 178 | 179 | let proxy_port = 26201; 180 | let _config_file = start_test_server( 181 | proxy_port, 182 | upstream_port, 183 | SHORT_BAN_DURATION, 184 | SHORT_SESSION_DURATION, 185 | ) 186 | .await; 187 | 188 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 189 | let auth_url = format!("{}/auth", base_url); 190 | let ip = "10.0.2.1"; 191 | 192 | let client_fail = Client::builder().cookie_store(true).build().unwrap(); 193 | 194 | // Trigger blacklist with multiple failed attempts 195 | // max_retries = 3, so attempts 1-3 should work, attempt 4+ should be banned 196 | for i in 0..6 { 197 | let resp = client_fail 198 | .post(&auth_url) 199 | .form(&[("code", "000000")]) 200 | .header("X-Forwarded-For", ip) 201 | .send() 202 | .await 203 | .unwrap(); 204 | 205 | // First 3 attempts should fail auth but not be banned (status 302 or 200) 206 | // 4th attempt onwards should be banned (status 429) 207 | if i >= 3 { 208 | assert_eq!( 209 | resp.status(), 210 | 429, 211 | "Should be banned after {} attempts", 212 | i + 1 213 | ); 214 | } 215 | tokio::time::sleep(Duration::from_millis(20)).await; 216 | } 217 | 218 | // Verify IP is blacklisted 219 | let resp = client_fail 220 | .get(&base_url) 221 | .header("X-Forwarded-For", ip) 222 | .send() 223 | .await 224 | .unwrap(); 225 | assert_eq!( 226 | resp.status(), 227 | 429, 228 | "IP should be blacklisted after failures" 229 | ); 230 | 231 | // Wait for ban to expire 232 | tokio::time::sleep(Duration::from_millis(1100)).await; 233 | 234 | // Verify ban has expired 235 | let resp = client_fail 236 | .get(&base_url) 237 | .header("X-Forwarded-For", ip) 238 | .send() 239 | .await 240 | .unwrap(); 241 | assert_eq!(resp.status(), 200, "Ban should have expired"); 242 | assert!(resp.text().await.unwrap().contains("Access Gateway")); 243 | } 244 | 245 | #[tokio::test] 246 | async fn test_session_expiry() { 247 | let _ = env_logger::builder().is_test(true).try_init(); 248 | 249 | let upstream_port = 26300; 250 | start_mock_upstream(upstream_port).await; 251 | 252 | let proxy_port = 26301; 253 | let _config_file = start_test_server( 254 | proxy_port, 255 | upstream_port, 256 | SHORT_BAN_DURATION, 257 | SHORT_SESSION_DURATION, 258 | ) 259 | .await; 260 | 261 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 262 | let auth_url = format!("{}/auth", base_url); 263 | 264 | let code = generate_totp_code(); 265 | let client = Client::builder().cookie_store(true).build().unwrap(); 266 | 267 | // Authenticate 268 | let resp = client 269 | .post(&auth_url) 270 | .form(&[("code", &code)]) 271 | .header("X-Forwarded-For", "10.0.3.1") 272 | .send() 273 | .await 274 | .unwrap(); 275 | assert_eq!(resp.status(), 200); 276 | 277 | // Access should work immediately 278 | let resp = client.get(&base_url).send().await.unwrap(); 279 | assert_eq!(resp.text().await.unwrap(), "Hello World"); 280 | 281 | // Wait for session to expire (2 seconds + buffer) 282 | tokio::time::sleep(Duration::from_millis(2300)).await; 283 | 284 | // Access should redirect to login 285 | let resp = client.get(&base_url).send().await.unwrap(); 286 | let body = resp.text().await.unwrap(); 287 | assert!( 288 | body.contains("Access Gateway"), 289 | "Session should have expired" 290 | ); 291 | } 292 | 293 | #[tokio::test] 294 | async fn test_route_matching() { 295 | let _ = env_logger::builder().is_test(true).try_init(); 296 | 297 | let upstream_port = 26400; 298 | start_mock_upstream(upstream_port).await; 299 | 300 | let proxy_port = 26401; 301 | let _config_file = start_test_server( 302 | proxy_port, 303 | upstream_port, 304 | SHORT_BAN_DURATION, 305 | SHORT_SESSION_DURATION, 306 | ) 307 | .await; 308 | 309 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 310 | let auth_url = format!("{}/auth", base_url); 311 | 312 | let code = generate_totp_code(); 313 | let client = Client::builder().cookie_store(true).build().unwrap(); 314 | 315 | // Authenticate 316 | client 317 | .post(&auth_url) 318 | .form(&[("code", &code)]) 319 | .send() 320 | .await 321 | .unwrap(); 322 | 323 | // Test admin route 324 | let resp = client 325 | .get(format!("{}/admin", base_url)) 326 | .send() 327 | .await 328 | .unwrap(); 329 | assert_eq!(resp.text().await.unwrap(), "Admin Area"); 330 | 331 | // Test default route 332 | let resp = client 333 | .get(format!("{}/other", base_url)) 334 | .send() 335 | .await 336 | .unwrap(); 337 | assert_eq!(resp.text().await.unwrap(), "Hello World"); 338 | } 339 | 340 | #[tokio::test] 341 | async fn test_concurrent_requests() { 342 | let _ = env_logger::builder().is_test(true).try_init(); 343 | 344 | let upstream_port = 26500; 345 | start_mock_upstream(upstream_port).await; 346 | 347 | let proxy_port = 26501; 348 | let _config_file = start_test_server( 349 | proxy_port, 350 | upstream_port, 351 | SHORT_BAN_DURATION, 352 | SHORT_SESSION_DURATION, 353 | ) 354 | .await; 355 | 356 | let base_url = format!("http://127.0.0.1:{}", proxy_port); 357 | let auth_url = format!("{}/auth", base_url); 358 | 359 | let code = generate_totp_code(); 360 | let client = Client::builder().cookie_store(true).build().unwrap(); 361 | 362 | // Authenticate once 363 | client 364 | .post(&auth_url) 365 | .form(&[("code", &code)]) 366 | .send() 367 | .await 368 | .unwrap(); 369 | 370 | // Send multiple concurrent requests 371 | let mut handles = vec![]; 372 | for _ in 0..10 { 373 | let client_clone = client.clone(); 374 | let url = base_url.clone(); 375 | let handle = tokio::spawn(async move { 376 | client_clone 377 | .get(&url) 378 | .send() 379 | .await 380 | .unwrap() 381 | .text() 382 | .await 383 | .unwrap() 384 | }); 385 | handles.push(handle); 386 | } 387 | 388 | // All should succeed 389 | for handle in handles { 390 | let result = handle.await.unwrap(); 391 | assert_eq!(result, "Hello World"); 392 | } 393 | } -------------------------------------------------------------------------------- /src/proxy.rs: -------------------------------------------------------------------------------- 1 | use crate::config::BlacklistStrategy; 2 | use crate::state::{ 3 | CompiledRoute, DEFAULT_HTTP_PORT, MAX_BODY_SIZE, MAX_IP_ENTRIES, MAX_SESSION_ENTRIES, 4 | ProxyState, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP_SECS, 5 | }; 6 | use crate::utils::{ProxyError, SessionId, UpstreamAddr}; 7 | use async_trait::async_trait; 8 | use bytes::Bytes; 9 | use log::{info, warn}; 10 | use pingora::http::ResponseHeader; 11 | use pingora::prelude::*; 12 | use std::net::IpAddr; 13 | use std::ops::Deref; 14 | use std::str::FromStr; 15 | use std::sync::Arc; 16 | use std::sync::atomic::Ordering; 17 | use std::time::{SystemTime, UNIX_EPOCH}; 18 | use totp_rs::{Algorithm, Secret, TOTP}; 19 | use url::form_urlencoded; 20 | use uuid::Uuid; 21 | 22 | pub struct AuthGateway { 23 | pub state: Arc, 24 | } 25 | 26 | impl AuthGateway { 27 | fn get_real_ip(&self, session: &Session) -> Option { 28 | let client_addr = session 29 | .client_addr() 30 | .and_then(|addr| addr.as_inet()) 31 | .map(|inet| inet.ip())?; 32 | 33 | let runtime = self.state.runtime.load(); 34 | 35 | for (cidr, header_name) in &runtime.trusted_cidrs { 36 | if cidr.contains(&client_addr) 37 | && let Some(ip) = session 38 | .req_header() 39 | .headers 40 | .get(header_name) 41 | .and_then(|h| h.to_str().ok()) 42 | .and_then(|s| IpAddr::from_str(s).ok()) 43 | { 44 | return Some(ip); 45 | } 46 | } 47 | Some(client_addr) 48 | } 49 | 50 | fn verify_totp(&self, code: &str) -> Result { 51 | let runtime = self.state.runtime.load(); 52 | let secret = Secret::Encoded(runtime.secret.clone()); 53 | let secret_bytes = secret 54 | .to_bytes() 55 | .map_err(|_| ProxyError::TotpSecretInvalid)?; 56 | 57 | let totp = TOTP::new( 58 | Algorithm::SHA1, 59 | TOTP_DIGITS, 60 | TOTP_SKEW as u8, 61 | TOTP_STEP_SECS, 62 | secret_bytes, 63 | ) 64 | .map_err(|_| ProxyError::TotpCreationFailed)?; 65 | 66 | let now = SystemTime::now() 67 | .duration_since(UNIX_EPOCH) 68 | .unwrap_or_default() 69 | .as_secs(); 70 | 71 | let is_valid_format = totp.check(code, now) 72 | || totp.check(code, now.saturating_sub(TOTP_STEP_SECS)) 73 | || totp.check(code, now.saturating_add(TOTP_STEP_SECS)); 74 | 75 | if !is_valid_format { 76 | warn!("Invalid TOTP Format: {}", code); 77 | return Ok(false); 78 | } 79 | 80 | let current_step = now / TOTP_STEP_SECS; 81 | let last_step = self.state.last_verified_step.load(Ordering::Relaxed); 82 | 83 | if current_step > last_step { 84 | let result = self.state.last_verified_step.compare_exchange( 85 | last_step, 86 | current_step, 87 | Ordering::SeqCst, 88 | Ordering::Relaxed, 89 | ); 90 | 91 | return Ok(result.is_ok()); 92 | } 93 | 94 | warn!("Replay Attack Detected! Code used within same step."); 95 | Ok(false) 96 | } 97 | 98 | fn get_session_cookie(&self, session: &Session) -> Option { 99 | if let Some(header) = session.req_header().headers.get("Cookie") 100 | && let Ok(cookie_str) = header.to_str() 101 | { 102 | for part in cookie_str.split(';') { 103 | let part = part.trim(); 104 | if let Some(sid) = part.strip_prefix("SID=") { 105 | return Some(SessionId::new(sid.to_string())); 106 | } 107 | } 108 | } 109 | None 110 | } 111 | 112 | fn is_blacklisted(&self, ip: IpAddr) -> bool { 113 | let runtime = self.state.runtime.load(); 114 | if !runtime.config.security.enabled { 115 | return false; 116 | } 117 | self.state.blacklist.load().contains_key(&ip) 118 | } 119 | 120 | fn register_failure(&self, ip: IpAddr) { 121 | let runtime = self.state.runtime.load(); 122 | let security_config = &runtime.config.security; 123 | 124 | if !security_config.enabled { 125 | return; 126 | } 127 | 128 | if !self.state.ip_limits.contains_key(&ip) 129 | && self.state.ip_limits.entry_count() >= MAX_IP_ENTRIES 130 | { 131 | warn!("IP Limit Table Full. Dropping failure tracking for: {}", ip); 132 | return; 133 | } 134 | 135 | let entry = self.state.ip_limits.entry(ip).or_insert(0); 136 | let mut val = *entry.value(); 137 | val += 1; 138 | 139 | if val >= security_config.max_retries as u8 { 140 | let blacklist = self.state.blacklist.load(); 141 | 142 | if security_config.blacklist_strategy == BlacklistStrategy::Block 143 | && blacklist.iter().count() as u64 >= security_config.blacklist_size as u64 144 | { 145 | warn!( 146 | "Blacklist is full and strategy is 'block', not adding new IP: {}", 147 | ip 148 | ); 149 | return; 150 | } 151 | 152 | warn!("IP {} added to blacklist due to repeated failures.", ip); 153 | blacklist.insert(ip, ()); 154 | self.state.ip_limits.invalidate(&ip); 155 | return; 156 | } 157 | 158 | self.state.ip_limits.insert(ip, val); 159 | } 160 | 161 | fn reset_failure(&self, ip: IpAddr) { 162 | self.state.ip_limits.invalidate(&ip); 163 | } 164 | 165 | fn parse_upstream_addr(addr: &str) -> UpstreamAddr { 166 | addr.parse() 167 | .unwrap_or_else(|_| UpstreamAddr::new("127.0.0.1".to_string(), DEFAULT_HTTP_PORT)) 168 | } 169 | 170 | fn check_route(host: &str, path: &str, r: &&&CompiledRoute) -> bool { 171 | if let Some(prefix) = &r.path_prefix { 172 | return path.starts_with(prefix); 173 | } 174 | 175 | let host_match = r.host.as_ref().map(|re| re.is_match(host)).unwrap_or(true); 176 | let path_match = r.path.as_ref().map(|re| re.is_match(path)).unwrap_or(true); 177 | 178 | host_match && path_match 179 | } 180 | } 181 | 182 | #[async_trait] 183 | impl ProxyHttp for AuthGateway { 184 | type CTX = (); 185 | 186 | fn new_ctx(&self) -> Self::CTX {} 187 | 188 | async fn upstream_peer( 189 | &self, 190 | session: &mut Session, 191 | _ctx: &mut Self::CTX, 192 | ) -> Result> { 193 | let runtime = self.state.runtime.load(); 194 | 195 | let host = session 196 | .req_header() 197 | .headers 198 | .get("Host") 199 | .and_then(|v| v.to_str().ok()) 200 | .unwrap_or(""); 201 | 202 | let host = host.split(':').next().unwrap_or(host); 203 | let path = session.req_header().uri.path(); 204 | 205 | let upstream_addr = runtime 206 | .routes 207 | .iter() 208 | .find(|r| Self::check_route(host, path, &r)) 209 | .map(|r| &r.upstream_addr) 210 | .unwrap_or(&runtime.config.server.default_upstream); 211 | 212 | let parsed = Self::parse_upstream_addr(upstream_addr); 213 | 214 | Ok(Box::new(HttpPeer::new( 215 | (parsed.host.as_str(), parsed.port), 216 | false, 217 | parsed.host.clone(), 218 | ))) 219 | } 220 | 221 | async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result { 222 | let runtime_for_route = self.state.runtime.load(); 223 | let host_hdr = session 224 | .req_header() 225 | .headers 226 | .get("Host") 227 | .and_then(|v| v.to_str().ok()) 228 | .unwrap_or(""); 229 | let host_only = host_hdr.split(':').next().unwrap_or(host_hdr); 230 | let path = session.req_header().uri.path(); 231 | 232 | let matched_route = runtime_for_route 233 | .routes 234 | .iter() 235 | .find(|r| Self::check_route(host_only, path, &r)); 236 | 237 | if let Some(route) = matched_route { 238 | if !route.protect { 239 | return Ok(false); 240 | } 241 | } 242 | 243 | let client_ip = match self.get_real_ip(session) { 244 | Some(ip) => ip, 245 | None => { 246 | let mut header = ResponseHeader::build(400, Some(1))?; 247 | header.insert_header("Content-Length", "0")?; 248 | session 249 | .write_response_header(Box::new(header), false) 250 | .await?; 251 | return Ok(true); 252 | } 253 | }; 254 | 255 | let runtime = self.state.runtime.load(); 256 | let auth_config = &runtime.config.auth; 257 | 258 | if self.is_blacklisted(client_ip) { 259 | warn!("Blocked Request from {}", client_ip); 260 | let mut header = ResponseHeader::build(429, Some(1))?; 261 | header.insert_header("Content-Length", "0")?; 262 | session 263 | .write_response_header(Box::new(header), false) 264 | .await?; 265 | return Ok(true); 266 | } 267 | 268 | if let Some(sid) = self.get_session_cookie(session) 269 | && self.state.sessions.get(sid.as_str()).is_some() 270 | { 271 | return Ok(false); 272 | } 273 | 274 | if session.req_header().method == "POST" && session.req_header().uri.path() == "/auth" { 275 | let content_len = session.req_header().deref().headers.get("Content-Length"); 276 | let content_len = content_len 277 | .and_then(|v| v.to_str().ok()) 278 | .and_then(|v| v.parse().ok()); 279 | let content_len = content_len.unwrap_or(0); 280 | 281 | if content_len > MAX_BODY_SIZE { 282 | warn!("Payload too large: {} bytes", content_len); 283 | let mut header = ResponseHeader::build(413, Some(1))?; 284 | header.insert_header("Content-Length", "0")?; 285 | 286 | session 287 | .write_response_header(Box::new(header), true) 288 | .await?; 289 | return Ok(true); 290 | } 291 | 292 | let body_bytes = session.read_request_body().await?.unwrap_or_default(); 293 | 294 | if body_bytes.len() > MAX_BODY_SIZE { 295 | let mut header = ResponseHeader::build(413, Some(1))?; 296 | header.insert_header("Content-Length", "0")?; 297 | 298 | session 299 | .write_response_header(Box::new(header), true) 300 | .await?; 301 | return Ok(true); 302 | } 303 | 304 | let params: std::collections::HashMap = 305 | form_urlencoded::parse(&body_bytes).into_owned().collect(); 306 | 307 | if let Some(code) = params.get("code") { 308 | match self.verify_totp(code) { 309 | Ok(true) => { 310 | self.reset_failure(client_ip); 311 | 312 | if self.state.sessions.entry_count() >= MAX_SESSION_ENTRIES { 313 | warn!("Session Table Full. Rejecting login."); 314 | let mut header = ResponseHeader::build(503, Some(2))?; 315 | header.insert_header("Retry-After", "60")?; 316 | header.insert_header("Content-Length", "0")?; 317 | session 318 | .write_response_header(Box::new(header), true) 319 | .await?; 320 | return Ok(true); 321 | } 322 | 323 | let new_sid = SessionId::new(Uuid::new_v4().to_string()); 324 | info!("Login Success: (IP: {:?})", client_ip); 325 | 326 | self.state.sessions.insert(new_sid.as_str().to_string(), ()); 327 | 328 | let cookie_val = format!( 329 | "SID={}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age={}", 330 | new_sid, auth_config.session_duration 331 | ); 332 | let mut header = ResponseHeader::build(302, Some(3))?; 333 | header.insert_header("Set-Cookie", cookie_val)?; 334 | header.insert_header("Location", "/")?; 335 | header.insert_header("Content-Length", "0")?; 336 | 337 | session 338 | .write_response_header(Box::new(header), true) 339 | .await?; 340 | return Ok(true); 341 | } 342 | Ok(false) => { 343 | warn!("Login Failed (Invalid TOTP). IP: {:?}", client_ip); 344 | self.register_failure(client_ip); 345 | 346 | if self.is_blacklisted(client_ip) { 347 | let mut header = ResponseHeader::build(429, Some(1))?; 348 | header.insert_header("Content-Length", "0")?; 349 | session 350 | .write_response_header(Box::new(header), true) 351 | .await?; 352 | return Ok(true); 353 | } 354 | } 355 | Err(e) => { 356 | warn!("TOTP verification error: {}", e); 357 | self.register_failure(client_ip); 358 | } 359 | } 360 | } 361 | 362 | let mut header = ResponseHeader::build(302, Some(2))?; 363 | header.insert_header("Location", "/?error=1")?; 364 | header.insert_header("Content-Length", "0")?; 365 | session 366 | .write_response_header(Box::new(header), true) 367 | .await?; 368 | return Ok(true); 369 | } 370 | 371 | let mut header = ResponseHeader::build(200, Some(8))?; 372 | header.insert_header("Content-Type", "text/html; charset=utf-8")?; 373 | header.insert_header("Content-Length", runtime.login_page_len.as_str())?; 374 | header.insert_header("X-Content-Type-Options", "nosniff")?; 375 | header.insert_header("X-Frame-Options", "DENY")?; 376 | 377 | header.insert_header( 378 | "Cache-Control", 379 | "no-store, no-cache, must-revalidate, private", 380 | )?; 381 | header.insert_header("Pragma", "no-cache")?; 382 | header.insert_header("Expires", "0")?; 383 | header.insert_header("CDN-Cache-Control", "no-store")?; 384 | 385 | session 386 | .write_response_header(Box::new(header), false) 387 | .await?; 388 | session 389 | .write_response_body( 390 | Some(Bytes::from(runtime.login_page_html.as_bytes().to_vec())), 391 | true, 392 | ) 393 | .await?; 394 | 395 | Ok(true) 396 | } 397 | 398 | async fn response_filter( 399 | &self, 400 | session: &mut Session, 401 | upstream_response: &mut ResponseHeader, 402 | _ctx: &mut Self::CTX, 403 | ) -> Result<()> { 404 | let runtime = self.state.runtime.load(); 405 | 406 | let host_hdr = session 407 | .req_header() 408 | .headers 409 | .get("Host") 410 | .and_then(|v| v.to_str().ok()) 411 | .unwrap_or(""); 412 | let host_only = host_hdr.split(':').next().unwrap_or(host_hdr); 413 | let path = session.req_header().uri.path(); 414 | 415 | let matched_route = runtime 416 | .routes 417 | .iter() 418 | .find(|r| Self::check_route(host_only, path, &r)); 419 | 420 | let is_unprotected = matched_route.map(|r| !r.protect).unwrap_or(false); 421 | 422 | if !is_unprotected { 423 | upstream_response 424 | .insert_header( 425 | "Cache-Control", 426 | "no-store, no-cache, must-revalidate, private", 427 | ) 428 | .ok(); 429 | upstream_response.insert_header("Pragma", "no-cache").ok(); 430 | upstream_response.insert_header("Expires", "0").ok(); 431 | 432 | upstream_response 433 | .insert_header("CDN-Cache-Control", "no-store") 434 | .ok(); 435 | upstream_response 436 | .insert_header("Cloudflare-CDN-Cache-Control", "no-store") 437 | .ok(); 438 | } 439 | 440 | Ok(()) 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.25.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.12" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 25 | dependencies = [ 26 | "cfg-if", 27 | "getrandom 0.3.4", 28 | "once_cell", 29 | "version_check", 30 | "zerocopy", 31 | ] 32 | 33 | [[package]] 34 | name = "aho-corasick" 35 | version = "1.1.4" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 38 | dependencies = [ 39 | "memchr", 40 | ] 41 | 42 | [[package]] 43 | name = "alloc-no-stdlib" 44 | version = "2.0.4" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 47 | 48 | [[package]] 49 | name = "alloc-stdlib" 50 | version = "0.2.2" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 53 | dependencies = [ 54 | "alloc-no-stdlib", 55 | ] 56 | 57 | [[package]] 58 | name = "allocator-api2" 59 | version = "0.2.21" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 62 | 63 | [[package]] 64 | name = "anstream" 65 | version = "0.6.21" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 68 | dependencies = [ 69 | "anstyle", 70 | "anstyle-parse", 71 | "anstyle-query", 72 | "anstyle-wincon", 73 | "colorchoice", 74 | "is_terminal_polyfill", 75 | "utf8parse", 76 | ] 77 | 78 | [[package]] 79 | name = "anstyle" 80 | version = "1.0.13" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 83 | 84 | [[package]] 85 | name = "anstyle-parse" 86 | version = "0.2.7" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 89 | dependencies = [ 90 | "utf8parse", 91 | ] 92 | 93 | [[package]] 94 | name = "anstyle-query" 95 | version = "1.1.5" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 98 | dependencies = [ 99 | "windows-sys 0.61.2", 100 | ] 101 | 102 | [[package]] 103 | name = "anstyle-wincon" 104 | version = "3.0.11" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 107 | dependencies = [ 108 | "anstyle", 109 | "once_cell_polyfill", 110 | "windows-sys 0.61.2", 111 | ] 112 | 113 | [[package]] 114 | name = "arc-swap" 115 | version = "1.7.1" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 118 | 119 | [[package]] 120 | name = "arrayvec" 121 | version = "0.7.6" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 124 | 125 | [[package]] 126 | name = "async-stream" 127 | version = "0.3.6" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 130 | dependencies = [ 131 | "async-stream-impl", 132 | "futures-core", 133 | "pin-project-lite", 134 | ] 135 | 136 | [[package]] 137 | name = "async-stream-impl" 138 | version = "0.3.6" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 141 | dependencies = [ 142 | "proc-macro2", 143 | "quote", 144 | "syn 2.0.111", 145 | ] 146 | 147 | [[package]] 148 | name = "async-trait" 149 | version = "0.1.89" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 152 | dependencies = [ 153 | "proc-macro2", 154 | "quote", 155 | "syn 2.0.111", 156 | ] 157 | 158 | [[package]] 159 | name = "atomic-waker" 160 | version = "1.1.2" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 163 | 164 | [[package]] 165 | name = "atty" 166 | version = "0.2.14" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 169 | dependencies = [ 170 | "hermit-abi", 171 | "libc", 172 | "winapi", 173 | ] 174 | 175 | [[package]] 176 | name = "autocfg" 177 | version = "1.5.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 180 | 181 | [[package]] 182 | name = "backtrace" 183 | version = "0.3.76" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" 186 | dependencies = [ 187 | "addr2line", 188 | "cfg-if", 189 | "libc", 190 | "miniz_oxide", 191 | "object", 192 | "rustc-demangle", 193 | "windows-link 0.2.1", 194 | ] 195 | 196 | [[package]] 197 | name = "base32" 198 | version = "0.5.1" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" 201 | 202 | [[package]] 203 | name = "base64" 204 | version = "0.22.1" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 207 | 208 | [[package]] 209 | name = "bitflags" 210 | version = "1.3.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 213 | 214 | [[package]] 215 | name = "bitflags" 216 | version = "2.10.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 219 | 220 | [[package]] 221 | name = "blake2" 222 | version = "0.10.6" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" 225 | dependencies = [ 226 | "digest", 227 | ] 228 | 229 | [[package]] 230 | name = "block-buffer" 231 | version = "0.10.4" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 234 | dependencies = [ 235 | "generic-array", 236 | ] 237 | 238 | [[package]] 239 | name = "brotli" 240 | version = "3.5.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" 243 | dependencies = [ 244 | "alloc-no-stdlib", 245 | "alloc-stdlib", 246 | "brotli-decompressor", 247 | ] 248 | 249 | [[package]] 250 | name = "brotli-decompressor" 251 | version = "2.5.1" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" 254 | dependencies = [ 255 | "alloc-no-stdlib", 256 | "alloc-stdlib", 257 | ] 258 | 259 | [[package]] 260 | name = "bstr" 261 | version = "1.12.1" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" 264 | dependencies = [ 265 | "memchr", 266 | "regex-automata", 267 | "serde", 268 | ] 269 | 270 | [[package]] 271 | name = "bumpalo" 272 | version = "3.19.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 275 | 276 | [[package]] 277 | name = "byteorder" 278 | version = "1.5.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 281 | 282 | [[package]] 283 | name = "bytes" 284 | version = "1.11.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 287 | 288 | [[package]] 289 | name = "cc" 290 | version = "1.2.47" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" 293 | dependencies = [ 294 | "find-msvc-tools", 295 | "jobserver", 296 | "libc", 297 | "shlex", 298 | ] 299 | 300 | [[package]] 301 | name = "cf-rustracing" 302 | version = "1.2.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "93f85c3824e4191621dec0551e3cef3d511f329da9a8990bf3e450a85651d97e" 305 | dependencies = [ 306 | "backtrace", 307 | "rand 0.8.5", 308 | "tokio", 309 | "trackable", 310 | ] 311 | 312 | [[package]] 313 | name = "cf-rustracing-jaeger" 314 | version = "1.2.2" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "a6a5f80d44c257c3300a7f45ada676c211e64bbbac591bbec19344a8f61fbcab" 317 | dependencies = [ 318 | "cf-rustracing", 319 | "hostname", 320 | "local-ip-address", 321 | "percent-encoding", 322 | "rand 0.9.2", 323 | "thrift_codec", 324 | "tokio", 325 | "trackable", 326 | ] 327 | 328 | [[package]] 329 | name = "cfg-if" 330 | version = "1.0.4" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 333 | 334 | [[package]] 335 | name = "chrono" 336 | version = "0.4.42" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 339 | dependencies = [ 340 | "num-traits", 341 | ] 342 | 343 | [[package]] 344 | name = "clap" 345 | version = "3.2.25" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 348 | dependencies = [ 349 | "atty", 350 | "bitflags 1.3.2", 351 | "clap_derive 3.2.25", 352 | "clap_lex 0.2.4", 353 | "indexmap 1.9.3", 354 | "once_cell", 355 | "strsim 0.10.0", 356 | "termcolor", 357 | "textwrap", 358 | ] 359 | 360 | [[package]] 361 | name = "clap" 362 | version = "4.5.53" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" 365 | dependencies = [ 366 | "clap_builder", 367 | "clap_derive 4.5.49", 368 | ] 369 | 370 | [[package]] 371 | name = "clap_builder" 372 | version = "4.5.53" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" 375 | dependencies = [ 376 | "anstream", 377 | "anstyle", 378 | "clap_lex 0.7.6", 379 | "strsim 0.11.1", 380 | ] 381 | 382 | [[package]] 383 | name = "clap_derive" 384 | version = "3.2.25" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" 387 | dependencies = [ 388 | "heck 0.4.1", 389 | "proc-macro-error", 390 | "proc-macro2", 391 | "quote", 392 | "syn 1.0.109", 393 | ] 394 | 395 | [[package]] 396 | name = "clap_derive" 397 | version = "4.5.49" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 400 | dependencies = [ 401 | "heck 0.5.0", 402 | "proc-macro2", 403 | "quote", 404 | "syn 2.0.111", 405 | ] 406 | 407 | [[package]] 408 | name = "clap_lex" 409 | version = "0.2.4" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 412 | dependencies = [ 413 | "os_str_bytes", 414 | ] 415 | 416 | [[package]] 417 | name = "clap_lex" 418 | version = "0.7.6" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 421 | 422 | [[package]] 423 | name = "cmake" 424 | version = "0.1.54" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 427 | dependencies = [ 428 | "cc", 429 | ] 430 | 431 | [[package]] 432 | name = "colorchoice" 433 | version = "1.0.4" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 436 | 437 | [[package]] 438 | name = "constant_time_eq" 439 | version = "0.3.1" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" 442 | 443 | [[package]] 444 | name = "cookie" 445 | version = "0.18.1" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 448 | dependencies = [ 449 | "percent-encoding", 450 | "time", 451 | "version_check", 452 | ] 453 | 454 | [[package]] 455 | name = "cookie_store" 456 | version = "0.21.1" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" 459 | dependencies = [ 460 | "cookie", 461 | "document-features", 462 | "idna", 463 | "log", 464 | "publicsuffix", 465 | "serde", 466 | "serde_derive", 467 | "serde_json", 468 | "time", 469 | "url", 470 | ] 471 | 472 | [[package]] 473 | name = "core-foundation" 474 | version = "0.9.4" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 477 | dependencies = [ 478 | "core-foundation-sys", 479 | "libc", 480 | ] 481 | 482 | [[package]] 483 | name = "core-foundation-sys" 484 | version = "0.8.7" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 487 | 488 | [[package]] 489 | name = "cpufeatures" 490 | version = "0.2.17" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 493 | dependencies = [ 494 | "libc", 495 | ] 496 | 497 | [[package]] 498 | name = "crc32fast" 499 | version = "1.5.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 502 | dependencies = [ 503 | "cfg-if", 504 | ] 505 | 506 | [[package]] 507 | name = "crossbeam-channel" 508 | version = "0.5.15" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 511 | dependencies = [ 512 | "crossbeam-utils", 513 | ] 514 | 515 | [[package]] 516 | name = "crossbeam-epoch" 517 | version = "0.9.18" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 520 | dependencies = [ 521 | "crossbeam-utils", 522 | ] 523 | 524 | [[package]] 525 | name = "crossbeam-queue" 526 | version = "0.3.12" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 529 | dependencies = [ 530 | "crossbeam-utils", 531 | ] 532 | 533 | [[package]] 534 | name = "crossbeam-utils" 535 | version = "0.8.21" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 538 | 539 | [[package]] 540 | name = "crypto-common" 541 | version = "0.1.7" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 544 | dependencies = [ 545 | "generic-array", 546 | "typenum", 547 | ] 548 | 549 | [[package]] 550 | name = "daemonize" 551 | version = "0.5.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" 554 | dependencies = [ 555 | "libc", 556 | ] 557 | 558 | [[package]] 559 | name = "deranged" 560 | version = "0.5.5" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 563 | dependencies = [ 564 | "powerfmt", 565 | ] 566 | 567 | [[package]] 568 | name = "derivative" 569 | version = "2.2.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 572 | dependencies = [ 573 | "proc-macro2", 574 | "quote", 575 | "syn 1.0.109", 576 | ] 577 | 578 | [[package]] 579 | name = "digest" 580 | version = "0.10.7" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 583 | dependencies = [ 584 | "block-buffer", 585 | "crypto-common", 586 | "subtle", 587 | ] 588 | 589 | [[package]] 590 | name = "displaydoc" 591 | version = "0.2.5" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 594 | dependencies = [ 595 | "proc-macro2", 596 | "quote", 597 | "syn 2.0.111", 598 | ] 599 | 600 | [[package]] 601 | name = "document-features" 602 | version = "0.2.12" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" 605 | dependencies = [ 606 | "litrs", 607 | ] 608 | 609 | [[package]] 610 | name = "either" 611 | version = "1.15.0" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 614 | 615 | [[package]] 616 | name = "encoding_rs" 617 | version = "0.8.35" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 620 | dependencies = [ 621 | "cfg-if", 622 | ] 623 | 624 | [[package]] 625 | name = "env_filter" 626 | version = "0.1.4" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" 629 | dependencies = [ 630 | "log", 631 | "regex", 632 | ] 633 | 634 | [[package]] 635 | name = "env_logger" 636 | version = "0.11.8" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 639 | dependencies = [ 640 | "anstream", 641 | "anstyle", 642 | "env_filter", 643 | "jiff", 644 | "log", 645 | ] 646 | 647 | [[package]] 648 | name = "equivalent" 649 | version = "1.0.2" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 652 | 653 | [[package]] 654 | name = "errno" 655 | version = "0.3.14" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 658 | dependencies = [ 659 | "libc", 660 | "windows-sys 0.61.2", 661 | ] 662 | 663 | [[package]] 664 | name = "fastrand" 665 | version = "2.3.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 668 | 669 | [[package]] 670 | name = "find-msvc-tools" 671 | version = "0.1.5" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 674 | 675 | [[package]] 676 | name = "flate2" 677 | version = "1.1.5" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" 680 | dependencies = [ 681 | "crc32fast", 682 | "libz-ng-sys", 683 | "miniz_oxide", 684 | ] 685 | 686 | [[package]] 687 | name = "fnv" 688 | version = "1.0.7" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 691 | 692 | [[package]] 693 | name = "foldhash" 694 | version = "0.1.5" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 697 | 698 | [[package]] 699 | name = "foldhash" 700 | version = "0.2.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 703 | 704 | [[package]] 705 | name = "foreign-types" 706 | version = "0.3.2" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 709 | dependencies = [ 710 | "foreign-types-shared", 711 | ] 712 | 713 | [[package]] 714 | name = "foreign-types-shared" 715 | version = "0.1.1" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 718 | 719 | [[package]] 720 | name = "form_urlencoded" 721 | version = "1.2.2" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 724 | dependencies = [ 725 | "percent-encoding", 726 | ] 727 | 728 | [[package]] 729 | name = "fsevent-sys" 730 | version = "4.1.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" 733 | dependencies = [ 734 | "libc", 735 | ] 736 | 737 | [[package]] 738 | name = "futures" 739 | version = "0.3.31" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 742 | dependencies = [ 743 | "futures-channel", 744 | "futures-core", 745 | "futures-executor", 746 | "futures-io", 747 | "futures-sink", 748 | "futures-task", 749 | "futures-util", 750 | ] 751 | 752 | [[package]] 753 | name = "futures-channel" 754 | version = "0.3.31" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 757 | dependencies = [ 758 | "futures-core", 759 | "futures-sink", 760 | ] 761 | 762 | [[package]] 763 | name = "futures-core" 764 | version = "0.3.31" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 767 | 768 | [[package]] 769 | name = "futures-executor" 770 | version = "0.3.31" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 773 | dependencies = [ 774 | "futures-core", 775 | "futures-task", 776 | "futures-util", 777 | ] 778 | 779 | [[package]] 780 | name = "futures-io" 781 | version = "0.3.31" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 784 | 785 | [[package]] 786 | name = "futures-macro" 787 | version = "0.3.31" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 790 | dependencies = [ 791 | "proc-macro2", 792 | "quote", 793 | "syn 2.0.111", 794 | ] 795 | 796 | [[package]] 797 | name = "futures-sink" 798 | version = "0.3.31" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 801 | 802 | [[package]] 803 | name = "futures-task" 804 | version = "0.3.31" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 807 | 808 | [[package]] 809 | name = "futures-util" 810 | version = "0.3.31" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 813 | dependencies = [ 814 | "futures-channel", 815 | "futures-core", 816 | "futures-io", 817 | "futures-macro", 818 | "futures-sink", 819 | "futures-task", 820 | "memchr", 821 | "pin-project-lite", 822 | "pin-utils", 823 | "slab", 824 | ] 825 | 826 | [[package]] 827 | name = "generic-array" 828 | version = "0.14.7" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 831 | dependencies = [ 832 | "typenum", 833 | "version_check", 834 | ] 835 | 836 | [[package]] 837 | name = "getrandom" 838 | version = "0.2.16" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 841 | dependencies = [ 842 | "cfg-if", 843 | "libc", 844 | "wasi", 845 | ] 846 | 847 | [[package]] 848 | name = "getrandom" 849 | version = "0.3.4" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 852 | dependencies = [ 853 | "cfg-if", 854 | "libc", 855 | "r-efi", 856 | "wasip2", 857 | ] 858 | 859 | [[package]] 860 | name = "gimli" 861 | version = "0.32.3" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 864 | 865 | [[package]] 866 | name = "h2" 867 | version = "0.4.12" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 870 | dependencies = [ 871 | "atomic-waker", 872 | "bytes", 873 | "fnv", 874 | "futures-core", 875 | "futures-sink", 876 | "http", 877 | "indexmap 2.12.1", 878 | "slab", 879 | "tokio", 880 | "tokio-util", 881 | "tracing", 882 | ] 883 | 884 | [[package]] 885 | name = "hashbrown" 886 | version = "0.12.3" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 889 | 890 | [[package]] 891 | name = "hashbrown" 892 | version = "0.15.5" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 895 | dependencies = [ 896 | "allocator-api2", 897 | "equivalent", 898 | "foldhash 0.1.5", 899 | ] 900 | 901 | [[package]] 902 | name = "hashbrown" 903 | version = "0.16.1" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 906 | dependencies = [ 907 | "allocator-api2", 908 | "equivalent", 909 | "foldhash 0.2.0", 910 | ] 911 | 912 | [[package]] 913 | name = "heck" 914 | version = "0.4.1" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 917 | 918 | [[package]] 919 | name = "heck" 920 | version = "0.5.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 923 | 924 | [[package]] 925 | name = "hermit-abi" 926 | version = "0.1.19" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 929 | dependencies = [ 930 | "libc", 931 | ] 932 | 933 | [[package]] 934 | name = "hex" 935 | version = "0.4.3" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 938 | 939 | [[package]] 940 | name = "hmac" 941 | version = "0.12.1" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 944 | dependencies = [ 945 | "digest", 946 | ] 947 | 948 | [[package]] 949 | name = "hostname" 950 | version = "0.4.1" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" 953 | dependencies = [ 954 | "cfg-if", 955 | "libc", 956 | "windows-link 0.1.3", 957 | ] 958 | 959 | [[package]] 960 | name = "http" 961 | version = "1.3.1" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 964 | dependencies = [ 965 | "bytes", 966 | "fnv", 967 | "itoa", 968 | ] 969 | 970 | [[package]] 971 | name = "http-body" 972 | version = "1.0.1" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 975 | dependencies = [ 976 | "bytes", 977 | "http", 978 | ] 979 | 980 | [[package]] 981 | name = "http-body-util" 982 | version = "0.1.3" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 985 | dependencies = [ 986 | "bytes", 987 | "futures-core", 988 | "http", 989 | "http-body", 990 | "pin-project-lite", 991 | ] 992 | 993 | [[package]] 994 | name = "httparse" 995 | version = "1.10.1" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 998 | 999 | [[package]] 1000 | name = "httpdate" 1001 | version = "1.0.3" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1004 | 1005 | [[package]] 1006 | name = "hyper" 1007 | version = "1.8.1" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 1010 | dependencies = [ 1011 | "atomic-waker", 1012 | "bytes", 1013 | "futures-channel", 1014 | "futures-core", 1015 | "h2", 1016 | "http", 1017 | "http-body", 1018 | "httparse", 1019 | "itoa", 1020 | "pin-project-lite", 1021 | "pin-utils", 1022 | "smallvec", 1023 | "tokio", 1024 | "want", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "hyper-rustls" 1029 | version = "0.27.7" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1032 | dependencies = [ 1033 | "http", 1034 | "hyper", 1035 | "hyper-util", 1036 | "rustls", 1037 | "rustls-pki-types", 1038 | "tokio", 1039 | "tokio-rustls", 1040 | "tower-service", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "hyper-tls" 1045 | version = "0.6.0" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1048 | dependencies = [ 1049 | "bytes", 1050 | "http-body-util", 1051 | "hyper", 1052 | "hyper-util", 1053 | "native-tls", 1054 | "tokio", 1055 | "tokio-native-tls", 1056 | "tower-service", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "hyper-util" 1061 | version = "0.1.18" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" 1064 | dependencies = [ 1065 | "base64", 1066 | "bytes", 1067 | "futures-channel", 1068 | "futures-core", 1069 | "futures-util", 1070 | "http", 1071 | "http-body", 1072 | "hyper", 1073 | "ipnet", 1074 | "libc", 1075 | "percent-encoding", 1076 | "pin-project-lite", 1077 | "socket2", 1078 | "system-configuration", 1079 | "tokio", 1080 | "tower-service", 1081 | "tracing", 1082 | "windows-registry", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "icu_collections" 1087 | version = "2.1.1" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 1090 | dependencies = [ 1091 | "displaydoc", 1092 | "potential_utf", 1093 | "yoke", 1094 | "zerofrom", 1095 | "zerovec", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "icu_locale_core" 1100 | version = "2.1.1" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 1103 | dependencies = [ 1104 | "displaydoc", 1105 | "litemap", 1106 | "tinystr", 1107 | "writeable", 1108 | "zerovec", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "icu_normalizer" 1113 | version = "2.1.1" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 1116 | dependencies = [ 1117 | "icu_collections", 1118 | "icu_normalizer_data", 1119 | "icu_properties", 1120 | "icu_provider", 1121 | "smallvec", 1122 | "zerovec", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "icu_normalizer_data" 1127 | version = "2.1.1" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 1130 | 1131 | [[package]] 1132 | name = "icu_properties" 1133 | version = "2.1.1" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" 1136 | dependencies = [ 1137 | "icu_collections", 1138 | "icu_locale_core", 1139 | "icu_properties_data", 1140 | "icu_provider", 1141 | "zerotrie", 1142 | "zerovec", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "icu_properties_data" 1147 | version = "2.1.1" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" 1150 | 1151 | [[package]] 1152 | name = "icu_provider" 1153 | version = "2.1.1" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 1156 | dependencies = [ 1157 | "displaydoc", 1158 | "icu_locale_core", 1159 | "writeable", 1160 | "yoke", 1161 | "zerofrom", 1162 | "zerotrie", 1163 | "zerovec", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "idna" 1168 | version = "1.1.0" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 1171 | dependencies = [ 1172 | "idna_adapter", 1173 | "smallvec", 1174 | "utf8_iter", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "idna_adapter" 1179 | version = "1.2.1" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1182 | dependencies = [ 1183 | "icu_normalizer", 1184 | "icu_properties", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "indexmap" 1189 | version = "1.9.3" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 1192 | dependencies = [ 1193 | "autocfg", 1194 | "hashbrown 0.12.3", 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "indexmap" 1199 | version = "2.12.1" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 1202 | dependencies = [ 1203 | "equivalent", 1204 | "hashbrown 0.16.1", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "inotify" 1209 | version = "0.11.0" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" 1212 | dependencies = [ 1213 | "bitflags 2.10.0", 1214 | "inotify-sys", 1215 | "libc", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "inotify-sys" 1220 | version = "0.1.5" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 1223 | dependencies = [ 1224 | "libc", 1225 | ] 1226 | 1227 | [[package]] 1228 | name = "ipnet" 1229 | version = "2.11.0" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1232 | 1233 | [[package]] 1234 | name = "iri-string" 1235 | version = "0.7.9" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" 1238 | dependencies = [ 1239 | "memchr", 1240 | "serde", 1241 | ] 1242 | 1243 | [[package]] 1244 | name = "is_terminal_polyfill" 1245 | version = "1.70.2" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 1248 | 1249 | [[package]] 1250 | name = "itoa" 1251 | version = "1.0.15" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1254 | 1255 | [[package]] 1256 | name = "jiff" 1257 | version = "0.2.16" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" 1260 | dependencies = [ 1261 | "jiff-static", 1262 | "log", 1263 | "portable-atomic", 1264 | "portable-atomic-util", 1265 | "serde_core", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "jiff-static" 1270 | version = "0.2.16" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" 1273 | dependencies = [ 1274 | "proc-macro2", 1275 | "quote", 1276 | "syn 2.0.111", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "jobserver" 1281 | version = "0.1.34" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 1284 | dependencies = [ 1285 | "getrandom 0.3.4", 1286 | "libc", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "js-sys" 1291 | version = "0.3.82" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" 1294 | dependencies = [ 1295 | "once_cell", 1296 | "wasm-bindgen", 1297 | ] 1298 | 1299 | [[package]] 1300 | name = "kqueue" 1301 | version = "1.1.1" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" 1304 | dependencies = [ 1305 | "kqueue-sys", 1306 | "libc", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "kqueue-sys" 1311 | version = "1.0.4" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" 1314 | dependencies = [ 1315 | "bitflags 1.3.2", 1316 | "libc", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "lazy_static" 1321 | version = "1.5.0" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1324 | 1325 | [[package]] 1326 | name = "libc" 1327 | version = "0.2.177" 1328 | source = "registry+https://github.com/rust-lang/crates.io-index" 1329 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 1330 | 1331 | [[package]] 1332 | name = "libmimalloc-sys" 1333 | version = "0.1.44" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" 1336 | dependencies = [ 1337 | "cc", 1338 | "libc", 1339 | ] 1340 | 1341 | [[package]] 1342 | name = "libz-ng-sys" 1343 | version = "1.1.23" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "7bf914b7dd154ca9193afec311d8e39345c1bd93b48b3faa77329f0db8f553c0" 1346 | dependencies = [ 1347 | "cmake", 1348 | "libc", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "linked-hash-map" 1353 | version = "0.5.6" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 1356 | 1357 | [[package]] 1358 | name = "linux-raw-sys" 1359 | version = "0.11.0" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 1362 | 1363 | [[package]] 1364 | name = "litemap" 1365 | version = "0.8.1" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 1368 | 1369 | [[package]] 1370 | name = "litrs" 1371 | version = "1.0.0" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" 1374 | 1375 | [[package]] 1376 | name = "local-ip-address" 1377 | version = "0.6.5" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" 1380 | dependencies = [ 1381 | "libc", 1382 | "neli", 1383 | "thiserror 2.0.17", 1384 | "windows-sys 0.59.0", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "lock_api" 1389 | version = "0.4.14" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 1392 | dependencies = [ 1393 | "scopeguard", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "log" 1398 | version = "0.4.28" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 1401 | 1402 | [[package]] 1403 | name = "lru" 1404 | version = "0.14.0" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" 1407 | dependencies = [ 1408 | "hashbrown 0.15.5", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "memchr" 1413 | version = "2.7.6" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 1416 | 1417 | [[package]] 1418 | name = "memoffset" 1419 | version = "0.6.5" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 1422 | dependencies = [ 1423 | "autocfg", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "mimalloc" 1428 | version = "0.1.48" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" 1431 | dependencies = [ 1432 | "libmimalloc-sys", 1433 | ] 1434 | 1435 | [[package]] 1436 | name = "mime" 1437 | version = "0.3.17" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1440 | 1441 | [[package]] 1442 | name = "miniz_oxide" 1443 | version = "0.8.9" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1446 | dependencies = [ 1447 | "adler2", 1448 | "simd-adler32", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "mio" 1453 | version = "1.1.0" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 1456 | dependencies = [ 1457 | "libc", 1458 | "log", 1459 | "wasi", 1460 | "windows-sys 0.61.2", 1461 | ] 1462 | 1463 | [[package]] 1464 | name = "moka" 1465 | version = "0.12.11" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" 1468 | dependencies = [ 1469 | "crossbeam-channel", 1470 | "crossbeam-epoch", 1471 | "crossbeam-utils", 1472 | "equivalent", 1473 | "parking_lot", 1474 | "portable-atomic", 1475 | "rustc_version", 1476 | "smallvec", 1477 | "tagptr", 1478 | "uuid", 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "native-tls" 1483 | version = "0.2.14" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1486 | dependencies = [ 1487 | "libc", 1488 | "log", 1489 | "openssl", 1490 | "openssl-probe", 1491 | "openssl-sys", 1492 | "schannel", 1493 | "security-framework", 1494 | "security-framework-sys", 1495 | "tempfile", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "neli" 1500 | version = "0.6.5" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9" 1503 | dependencies = [ 1504 | "byteorder", 1505 | "libc", 1506 | "log", 1507 | "neli-proc-macros", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "neli-proc-macros" 1512 | version = "0.1.4" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe" 1515 | dependencies = [ 1516 | "either", 1517 | "proc-macro2", 1518 | "quote", 1519 | "serde", 1520 | "syn 1.0.109", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "nix" 1525 | version = "0.24.3" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" 1528 | dependencies = [ 1529 | "bitflags 1.3.2", 1530 | "cfg-if", 1531 | "libc", 1532 | "memoffset", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "notify" 1537 | version = "8.2.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" 1540 | dependencies = [ 1541 | "bitflags 2.10.0", 1542 | "fsevent-sys", 1543 | "inotify", 1544 | "kqueue", 1545 | "libc", 1546 | "log", 1547 | "mio", 1548 | "notify-types", 1549 | "walkdir", 1550 | "windows-sys 0.60.2", 1551 | ] 1552 | 1553 | [[package]] 1554 | name = "notify-types" 1555 | version = "2.0.0" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" 1558 | 1559 | [[package]] 1560 | name = "num-conv" 1561 | version = "0.1.0" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1564 | 1565 | [[package]] 1566 | name = "num-traits" 1567 | version = "0.2.19" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1570 | dependencies = [ 1571 | "autocfg", 1572 | ] 1573 | 1574 | [[package]] 1575 | name = "object" 1576 | version = "0.37.3" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" 1579 | dependencies = [ 1580 | "memchr", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "once_cell" 1585 | version = "1.21.3" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1588 | 1589 | [[package]] 1590 | name = "once_cell_polyfill" 1591 | version = "1.70.2" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 1594 | 1595 | [[package]] 1596 | name = "openssl" 1597 | version = "0.10.75" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 1600 | dependencies = [ 1601 | "bitflags 2.10.0", 1602 | "cfg-if", 1603 | "foreign-types", 1604 | "libc", 1605 | "once_cell", 1606 | "openssl-macros", 1607 | "openssl-sys", 1608 | ] 1609 | 1610 | [[package]] 1611 | name = "openssl-macros" 1612 | version = "0.1.1" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1615 | dependencies = [ 1616 | "proc-macro2", 1617 | "quote", 1618 | "syn 2.0.111", 1619 | ] 1620 | 1621 | [[package]] 1622 | name = "openssl-probe" 1623 | version = "0.1.6" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1626 | 1627 | [[package]] 1628 | name = "openssl-sys" 1629 | version = "0.9.111" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" 1632 | dependencies = [ 1633 | "cc", 1634 | "libc", 1635 | "pkg-config", 1636 | "vcpkg", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "os_str_bytes" 1641 | version = "6.6.1" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 1644 | 1645 | [[package]] 1646 | name = "parking_lot" 1647 | version = "0.12.5" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 1650 | dependencies = [ 1651 | "lock_api", 1652 | "parking_lot_core", 1653 | ] 1654 | 1655 | [[package]] 1656 | name = "parking_lot_core" 1657 | version = "0.9.12" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 1660 | dependencies = [ 1661 | "cfg-if", 1662 | "libc", 1663 | "redox_syscall", 1664 | "smallvec", 1665 | "windows-link 0.2.1", 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "paste" 1670 | version = "1.0.15" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1673 | 1674 | [[package]] 1675 | name = "percent-encoding" 1676 | version = "2.3.2" 1677 | source = "registry+https://github.com/rust-lang/crates.io-index" 1678 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1679 | 1680 | [[package]] 1681 | name = "pin-project-lite" 1682 | version = "0.2.16" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1685 | 1686 | [[package]] 1687 | name = "pin-utils" 1688 | version = "0.1.0" 1689 | source = "registry+https://github.com/rust-lang/crates.io-index" 1690 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1691 | 1692 | [[package]] 1693 | name = "pingora" 1694 | version = "0.6.0" 1695 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1696 | dependencies = [ 1697 | "pingora-core", 1698 | "pingora-http", 1699 | "pingora-proxy", 1700 | "pingora-timeout", 1701 | ] 1702 | 1703 | [[package]] 1704 | name = "pingora-cache" 1705 | version = "0.6.0" 1706 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1707 | dependencies = [ 1708 | "ahash", 1709 | "async-trait", 1710 | "blake2", 1711 | "bstr", 1712 | "bytes", 1713 | "cf-rustracing", 1714 | "cf-rustracing-jaeger", 1715 | "hex", 1716 | "http", 1717 | "httparse", 1718 | "httpdate", 1719 | "indexmap 1.9.3", 1720 | "log", 1721 | "lru", 1722 | "once_cell", 1723 | "parking_lot", 1724 | "pingora-core", 1725 | "pingora-error", 1726 | "pingora-header-serde", 1727 | "pingora-http", 1728 | "pingora-lru", 1729 | "pingora-timeout", 1730 | "rand 0.8.5", 1731 | "regex", 1732 | "rmp", 1733 | "rmp-serde", 1734 | "serde", 1735 | "strum", 1736 | "tokio", 1737 | ] 1738 | 1739 | [[package]] 1740 | name = "pingora-core" 1741 | version = "0.6.0" 1742 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1743 | dependencies = [ 1744 | "ahash", 1745 | "async-trait", 1746 | "brotli", 1747 | "bstr", 1748 | "bytes", 1749 | "chrono", 1750 | "clap 3.2.25", 1751 | "daemonize", 1752 | "derivative", 1753 | "flate2", 1754 | "futures", 1755 | "h2", 1756 | "http", 1757 | "httparse", 1758 | "httpdate", 1759 | "libc", 1760 | "log", 1761 | "nix", 1762 | "once_cell", 1763 | "openssl-probe", 1764 | "parking_lot", 1765 | "percent-encoding", 1766 | "pingora-error", 1767 | "pingora-http", 1768 | "pingora-pool", 1769 | "pingora-runtime", 1770 | "pingora-timeout", 1771 | "prometheus", 1772 | "rand 0.8.5", 1773 | "regex", 1774 | "serde", 1775 | "serde_yaml", 1776 | "sfv", 1777 | "socket2", 1778 | "strum", 1779 | "strum_macros", 1780 | "tokio", 1781 | "tokio-stream", 1782 | "tokio-test", 1783 | "unicase", 1784 | "windows-sys 0.59.0", 1785 | "zstd", 1786 | ] 1787 | 1788 | [[package]] 1789 | name = "pingora-error" 1790 | version = "0.6.0" 1791 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1792 | 1793 | [[package]] 1794 | name = "pingora-header-serde" 1795 | version = "0.6.0" 1796 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1797 | dependencies = [ 1798 | "bytes", 1799 | "http", 1800 | "httparse", 1801 | "pingora-error", 1802 | "pingora-http", 1803 | "thread_local", 1804 | "zstd", 1805 | "zstd-safe", 1806 | ] 1807 | 1808 | [[package]] 1809 | name = "pingora-http" 1810 | version = "0.6.0" 1811 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1812 | dependencies = [ 1813 | "bytes", 1814 | "http", 1815 | "pingora-error", 1816 | ] 1817 | 1818 | [[package]] 1819 | name = "pingora-lru" 1820 | version = "0.6.0" 1821 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1822 | dependencies = [ 1823 | "arrayvec", 1824 | "hashbrown 0.16.1", 1825 | "parking_lot", 1826 | "rand 0.8.5", 1827 | ] 1828 | 1829 | [[package]] 1830 | name = "pingora-pool" 1831 | version = "0.6.0" 1832 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1833 | dependencies = [ 1834 | "crossbeam-queue", 1835 | "log", 1836 | "lru", 1837 | "parking_lot", 1838 | "pingora-timeout", 1839 | "thread_local", 1840 | "tokio", 1841 | ] 1842 | 1843 | [[package]] 1844 | name = "pingora-proxy" 1845 | version = "0.6.0" 1846 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1847 | dependencies = [ 1848 | "async-trait", 1849 | "bytes", 1850 | "clap 3.2.25", 1851 | "futures", 1852 | "h2", 1853 | "http", 1854 | "log", 1855 | "once_cell", 1856 | "pingora-cache", 1857 | "pingora-core", 1858 | "pingora-error", 1859 | "pingora-http", 1860 | "rand 0.8.5", 1861 | "regex", 1862 | "tokio", 1863 | ] 1864 | 1865 | [[package]] 1866 | name = "pingora-runtime" 1867 | version = "0.6.0" 1868 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1869 | dependencies = [ 1870 | "once_cell", 1871 | "rand 0.8.5", 1872 | "thread_local", 1873 | "tokio", 1874 | ] 1875 | 1876 | [[package]] 1877 | name = "pingora-timeout" 1878 | version = "0.6.0" 1879 | source = "git+https://github.com/KBluePurple/pingora#9faa813af8f7c0ede63b8d69936b2640cf2158fb" 1880 | dependencies = [ 1881 | "once_cell", 1882 | "parking_lot", 1883 | "pin-project-lite", 1884 | "thread_local", 1885 | "tokio", 1886 | ] 1887 | 1888 | [[package]] 1889 | name = "pkg-config" 1890 | version = "0.3.32" 1891 | source = "registry+https://github.com/rust-lang/crates.io-index" 1892 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1893 | 1894 | [[package]] 1895 | name = "portable-atomic" 1896 | version = "1.11.1" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 1899 | 1900 | [[package]] 1901 | name = "portable-atomic-util" 1902 | version = "0.2.4" 1903 | source = "registry+https://github.com/rust-lang/crates.io-index" 1904 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 1905 | dependencies = [ 1906 | "portable-atomic", 1907 | ] 1908 | 1909 | [[package]] 1910 | name = "potential_utf" 1911 | version = "0.1.4" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 1914 | dependencies = [ 1915 | "zerovec", 1916 | ] 1917 | 1918 | [[package]] 1919 | name = "powerfmt" 1920 | version = "0.2.0" 1921 | source = "registry+https://github.com/rust-lang/crates.io-index" 1922 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1923 | 1924 | [[package]] 1925 | name = "ppv-lite86" 1926 | version = "0.2.21" 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" 1928 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1929 | dependencies = [ 1930 | "zerocopy", 1931 | ] 1932 | 1933 | [[package]] 1934 | name = "proc-macro-error" 1935 | version = "1.0.4" 1936 | source = "registry+https://github.com/rust-lang/crates.io-index" 1937 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1938 | dependencies = [ 1939 | "proc-macro-error-attr", 1940 | "proc-macro2", 1941 | "quote", 1942 | "syn 1.0.109", 1943 | "version_check", 1944 | ] 1945 | 1946 | [[package]] 1947 | name = "proc-macro-error-attr" 1948 | version = "1.0.4" 1949 | source = "registry+https://github.com/rust-lang/crates.io-index" 1950 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1951 | dependencies = [ 1952 | "proc-macro2", 1953 | "quote", 1954 | "version_check", 1955 | ] 1956 | 1957 | [[package]] 1958 | name = "proc-macro2" 1959 | version = "1.0.103" 1960 | source = "registry+https://github.com/rust-lang/crates.io-index" 1961 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 1962 | dependencies = [ 1963 | "unicode-ident", 1964 | ] 1965 | 1966 | [[package]] 1967 | name = "prometheus" 1968 | version = "0.13.4" 1969 | source = "registry+https://github.com/rust-lang/crates.io-index" 1970 | checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" 1971 | dependencies = [ 1972 | "cfg-if", 1973 | "fnv", 1974 | "lazy_static", 1975 | "memchr", 1976 | "parking_lot", 1977 | "protobuf", 1978 | "thiserror 1.0.69", 1979 | ] 1980 | 1981 | [[package]] 1982 | name = "protobuf" 1983 | version = "2.28.0" 1984 | source = "registry+https://github.com/rust-lang/crates.io-index" 1985 | checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" 1986 | 1987 | [[package]] 1988 | name = "psl-types" 1989 | version = "2.0.11" 1990 | source = "registry+https://github.com/rust-lang/crates.io-index" 1991 | checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" 1992 | 1993 | [[package]] 1994 | name = "publicsuffix" 1995 | version = "2.3.0" 1996 | source = "registry+https://github.com/rust-lang/crates.io-index" 1997 | checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" 1998 | dependencies = [ 1999 | "idna", 2000 | "psl-types", 2001 | ] 2002 | 2003 | [[package]] 2004 | name = "quote" 2005 | version = "1.0.42" 2006 | source = "registry+https://github.com/rust-lang/crates.io-index" 2007 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 2008 | dependencies = [ 2009 | "proc-macro2", 2010 | ] 2011 | 2012 | [[package]] 2013 | name = "r-efi" 2014 | version = "5.3.0" 2015 | source = "registry+https://github.com/rust-lang/crates.io-index" 2016 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 2017 | 2018 | [[package]] 2019 | name = "rand" 2020 | version = "0.8.5" 2021 | source = "registry+https://github.com/rust-lang/crates.io-index" 2022 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 2023 | dependencies = [ 2024 | "libc", 2025 | "rand_chacha 0.3.1", 2026 | "rand_core 0.6.4", 2027 | ] 2028 | 2029 | [[package]] 2030 | name = "rand" 2031 | version = "0.9.2" 2032 | source = "registry+https://github.com/rust-lang/crates.io-index" 2033 | checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 2034 | dependencies = [ 2035 | "rand_chacha 0.9.0", 2036 | "rand_core 0.9.3", 2037 | ] 2038 | 2039 | [[package]] 2040 | name = "rand_chacha" 2041 | version = "0.3.1" 2042 | source = "registry+https://github.com/rust-lang/crates.io-index" 2043 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 2044 | dependencies = [ 2045 | "ppv-lite86", 2046 | "rand_core 0.6.4", 2047 | ] 2048 | 2049 | [[package]] 2050 | name = "rand_chacha" 2051 | version = "0.9.0" 2052 | source = "registry+https://github.com/rust-lang/crates.io-index" 2053 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2054 | dependencies = [ 2055 | "ppv-lite86", 2056 | "rand_core 0.9.3", 2057 | ] 2058 | 2059 | [[package]] 2060 | name = "rand_core" 2061 | version = "0.6.4" 2062 | source = "registry+https://github.com/rust-lang/crates.io-index" 2063 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 2064 | dependencies = [ 2065 | "getrandom 0.2.16", 2066 | ] 2067 | 2068 | [[package]] 2069 | name = "rand_core" 2070 | version = "0.9.3" 2071 | source = "registry+https://github.com/rust-lang/crates.io-index" 2072 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 2073 | dependencies = [ 2074 | "getrandom 0.3.4", 2075 | ] 2076 | 2077 | [[package]] 2078 | name = "redox_syscall" 2079 | version = "0.5.18" 2080 | source = "registry+https://github.com/rust-lang/crates.io-index" 2081 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 2082 | dependencies = [ 2083 | "bitflags 2.10.0", 2084 | ] 2085 | 2086 | [[package]] 2087 | name = "regex" 2088 | version = "1.12.2" 2089 | source = "registry+https://github.com/rust-lang/crates.io-index" 2090 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 2091 | dependencies = [ 2092 | "aho-corasick", 2093 | "memchr", 2094 | "regex-automata", 2095 | "regex-syntax", 2096 | ] 2097 | 2098 | [[package]] 2099 | name = "regex-automata" 2100 | version = "0.4.13" 2101 | source = "registry+https://github.com/rust-lang/crates.io-index" 2102 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 2103 | dependencies = [ 2104 | "aho-corasick", 2105 | "memchr", 2106 | "regex-syntax", 2107 | ] 2108 | 2109 | [[package]] 2110 | name = "regex-syntax" 2111 | version = "0.8.8" 2112 | source = "registry+https://github.com/rust-lang/crates.io-index" 2113 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 2114 | 2115 | [[package]] 2116 | name = "reqwest" 2117 | version = "0.12.24" 2118 | source = "registry+https://github.com/rust-lang/crates.io-index" 2119 | checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" 2120 | dependencies = [ 2121 | "base64", 2122 | "bytes", 2123 | "cookie", 2124 | "cookie_store", 2125 | "encoding_rs", 2126 | "futures-core", 2127 | "h2", 2128 | "http", 2129 | "http-body", 2130 | "http-body-util", 2131 | "hyper", 2132 | "hyper-rustls", 2133 | "hyper-tls", 2134 | "hyper-util", 2135 | "js-sys", 2136 | "log", 2137 | "mime", 2138 | "native-tls", 2139 | "percent-encoding", 2140 | "pin-project-lite", 2141 | "rustls-pki-types", 2142 | "serde", 2143 | "serde_json", 2144 | "serde_urlencoded", 2145 | "sync_wrapper", 2146 | "tokio", 2147 | "tokio-native-tls", 2148 | "tower", 2149 | "tower-http", 2150 | "tower-service", 2151 | "url", 2152 | "wasm-bindgen", 2153 | "wasm-bindgen-futures", 2154 | "web-sys", 2155 | ] 2156 | 2157 | [[package]] 2158 | name = "ring" 2159 | version = "0.17.14" 2160 | source = "registry+https://github.com/rust-lang/crates.io-index" 2161 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 2162 | dependencies = [ 2163 | "cc", 2164 | "cfg-if", 2165 | "getrandom 0.2.16", 2166 | "libc", 2167 | "untrusted", 2168 | "windows-sys 0.52.0", 2169 | ] 2170 | 2171 | [[package]] 2172 | name = "rmp" 2173 | version = "0.8.14" 2174 | source = "registry+https://github.com/rust-lang/crates.io-index" 2175 | checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" 2176 | dependencies = [ 2177 | "byteorder", 2178 | "num-traits", 2179 | "paste", 2180 | ] 2181 | 2182 | [[package]] 2183 | name = "rmp-serde" 2184 | version = "1.3.0" 2185 | source = "registry+https://github.com/rust-lang/crates.io-index" 2186 | checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" 2187 | dependencies = [ 2188 | "byteorder", 2189 | "rmp", 2190 | "serde", 2191 | ] 2192 | 2193 | [[package]] 2194 | name = "rust_decimal" 2195 | version = "1.39.0" 2196 | source = "registry+https://github.com/rust-lang/crates.io-index" 2197 | checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" 2198 | dependencies = [ 2199 | "arrayvec", 2200 | "num-traits", 2201 | ] 2202 | 2203 | [[package]] 2204 | name = "rustc-demangle" 2205 | version = "0.1.26" 2206 | source = "registry+https://github.com/rust-lang/crates.io-index" 2207 | checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 2208 | 2209 | [[package]] 2210 | name = "rustc_version" 2211 | version = "0.4.1" 2212 | source = "registry+https://github.com/rust-lang/crates.io-index" 2213 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 2214 | dependencies = [ 2215 | "semver", 2216 | ] 2217 | 2218 | [[package]] 2219 | name = "rustix" 2220 | version = "1.1.2" 2221 | source = "registry+https://github.com/rust-lang/crates.io-index" 2222 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 2223 | dependencies = [ 2224 | "bitflags 2.10.0", 2225 | "errno", 2226 | "libc", 2227 | "linux-raw-sys", 2228 | "windows-sys 0.61.2", 2229 | ] 2230 | 2231 | [[package]] 2232 | name = "rustls" 2233 | version = "0.23.35" 2234 | source = "registry+https://github.com/rust-lang/crates.io-index" 2235 | checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 2236 | dependencies = [ 2237 | "once_cell", 2238 | "rustls-pki-types", 2239 | "rustls-webpki", 2240 | "subtle", 2241 | "zeroize", 2242 | ] 2243 | 2244 | [[package]] 2245 | name = "rustls-pki-types" 2246 | version = "1.13.0" 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" 2248 | checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" 2249 | dependencies = [ 2250 | "zeroize", 2251 | ] 2252 | 2253 | [[package]] 2254 | name = "rustls-webpki" 2255 | version = "0.103.8" 2256 | source = "registry+https://github.com/rust-lang/crates.io-index" 2257 | checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" 2258 | dependencies = [ 2259 | "ring", 2260 | "rustls-pki-types", 2261 | "untrusted", 2262 | ] 2263 | 2264 | [[package]] 2265 | name = "rustversion" 2266 | version = "1.0.22" 2267 | source = "registry+https://github.com/rust-lang/crates.io-index" 2268 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 2269 | 2270 | [[package]] 2271 | name = "ryu" 2272 | version = "1.0.20" 2273 | source = "registry+https://github.com/rust-lang/crates.io-index" 2274 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 2275 | 2276 | [[package]] 2277 | name = "same-file" 2278 | version = "1.0.6" 2279 | source = "registry+https://github.com/rust-lang/crates.io-index" 2280 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 2281 | dependencies = [ 2282 | "winapi-util", 2283 | ] 2284 | 2285 | [[package]] 2286 | name = "schannel" 2287 | version = "0.1.28" 2288 | source = "registry+https://github.com/rust-lang/crates.io-index" 2289 | checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 2290 | dependencies = [ 2291 | "windows-sys 0.61.2", 2292 | ] 2293 | 2294 | [[package]] 2295 | name = "scopeguard" 2296 | version = "1.2.0" 2297 | source = "registry+https://github.com/rust-lang/crates.io-index" 2298 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2299 | 2300 | [[package]] 2301 | name = "security-framework" 2302 | version = "2.11.1" 2303 | source = "registry+https://github.com/rust-lang/crates.io-index" 2304 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2305 | dependencies = [ 2306 | "bitflags 2.10.0", 2307 | "core-foundation", 2308 | "core-foundation-sys", 2309 | "libc", 2310 | "security-framework-sys", 2311 | ] 2312 | 2313 | [[package]] 2314 | name = "security-framework-sys" 2315 | version = "2.15.0" 2316 | source = "registry+https://github.com/rust-lang/crates.io-index" 2317 | checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 2318 | dependencies = [ 2319 | "core-foundation-sys", 2320 | "libc", 2321 | ] 2322 | 2323 | [[package]] 2324 | name = "semver" 2325 | version = "1.0.27" 2326 | source = "registry+https://github.com/rust-lang/crates.io-index" 2327 | checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 2328 | 2329 | [[package]] 2330 | name = "serde" 2331 | version = "1.0.228" 2332 | source = "registry+https://github.com/rust-lang/crates.io-index" 2333 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 2334 | dependencies = [ 2335 | "serde_core", 2336 | "serde_derive", 2337 | ] 2338 | 2339 | [[package]] 2340 | name = "serde_core" 2341 | version = "1.0.228" 2342 | source = "registry+https://github.com/rust-lang/crates.io-index" 2343 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 2344 | dependencies = [ 2345 | "serde_derive", 2346 | ] 2347 | 2348 | [[package]] 2349 | name = "serde_derive" 2350 | version = "1.0.228" 2351 | source = "registry+https://github.com/rust-lang/crates.io-index" 2352 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 2353 | dependencies = [ 2354 | "proc-macro2", 2355 | "quote", 2356 | "syn 2.0.111", 2357 | ] 2358 | 2359 | [[package]] 2360 | name = "serde_json" 2361 | version = "1.0.145" 2362 | source = "registry+https://github.com/rust-lang/crates.io-index" 2363 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 2364 | dependencies = [ 2365 | "itoa", 2366 | "memchr", 2367 | "ryu", 2368 | "serde", 2369 | "serde_core", 2370 | ] 2371 | 2372 | [[package]] 2373 | name = "serde_spanned" 2374 | version = "1.0.3" 2375 | source = "registry+https://github.com/rust-lang/crates.io-index" 2376 | checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" 2377 | dependencies = [ 2378 | "serde_core", 2379 | ] 2380 | 2381 | [[package]] 2382 | name = "serde_urlencoded" 2383 | version = "0.7.1" 2384 | source = "registry+https://github.com/rust-lang/crates.io-index" 2385 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 2386 | dependencies = [ 2387 | "form_urlencoded", 2388 | "itoa", 2389 | "ryu", 2390 | "serde", 2391 | ] 2392 | 2393 | [[package]] 2394 | name = "serde_yaml" 2395 | version = "0.8.26" 2396 | source = "registry+https://github.com/rust-lang/crates.io-index" 2397 | checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" 2398 | dependencies = [ 2399 | "indexmap 1.9.3", 2400 | "ryu", 2401 | "serde", 2402 | "yaml-rust", 2403 | ] 2404 | 2405 | [[package]] 2406 | name = "sfv" 2407 | version = "0.10.4" 2408 | source = "registry+https://github.com/rust-lang/crates.io-index" 2409 | checksum = "3fa1f336066b758b7c9df34ed049c0e693a426afe2b27ff7d5b14f410ab1a132" 2410 | dependencies = [ 2411 | "base64", 2412 | "indexmap 2.12.1", 2413 | "rust_decimal", 2414 | ] 2415 | 2416 | [[package]] 2417 | name = "sha1" 2418 | version = "0.10.6" 2419 | source = "registry+https://github.com/rust-lang/crates.io-index" 2420 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 2421 | dependencies = [ 2422 | "cfg-if", 2423 | "cpufeatures", 2424 | "digest", 2425 | ] 2426 | 2427 | [[package]] 2428 | name = "sha2" 2429 | version = "0.10.9" 2430 | source = "registry+https://github.com/rust-lang/crates.io-index" 2431 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 2432 | dependencies = [ 2433 | "cfg-if", 2434 | "cpufeatures", 2435 | "digest", 2436 | ] 2437 | 2438 | [[package]] 2439 | name = "shlex" 2440 | version = "1.3.0" 2441 | source = "registry+https://github.com/rust-lang/crates.io-index" 2442 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2443 | 2444 | [[package]] 2445 | name = "signal-hook-registry" 2446 | version = "1.4.7" 2447 | source = "registry+https://github.com/rust-lang/crates.io-index" 2448 | checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" 2449 | dependencies = [ 2450 | "libc", 2451 | ] 2452 | 2453 | [[package]] 2454 | name = "simd-adler32" 2455 | version = "0.3.7" 2456 | source = "registry+https://github.com/rust-lang/crates.io-index" 2457 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 2458 | 2459 | [[package]] 2460 | name = "slab" 2461 | version = "0.4.11" 2462 | source = "registry+https://github.com/rust-lang/crates.io-index" 2463 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 2464 | 2465 | [[package]] 2466 | name = "smallvec" 2467 | version = "1.15.1" 2468 | source = "registry+https://github.com/rust-lang/crates.io-index" 2469 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 2470 | 2471 | [[package]] 2472 | name = "socket2" 2473 | version = "0.6.1" 2474 | source = "registry+https://github.com/rust-lang/crates.io-index" 2475 | checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 2476 | dependencies = [ 2477 | "libc", 2478 | "windows-sys 0.60.2", 2479 | ] 2480 | 2481 | [[package]] 2482 | name = "stable_deref_trait" 2483 | version = "1.2.1" 2484 | source = "registry+https://github.com/rust-lang/crates.io-index" 2485 | checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 2486 | 2487 | [[package]] 2488 | name = "strsim" 2489 | version = "0.10.0" 2490 | source = "registry+https://github.com/rust-lang/crates.io-index" 2491 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 2492 | 2493 | [[package]] 2494 | name = "strsim" 2495 | version = "0.11.1" 2496 | source = "registry+https://github.com/rust-lang/crates.io-index" 2497 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2498 | 2499 | [[package]] 2500 | name = "strum" 2501 | version = "0.26.3" 2502 | source = "registry+https://github.com/rust-lang/crates.io-index" 2503 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 2504 | dependencies = [ 2505 | "strum_macros", 2506 | ] 2507 | 2508 | [[package]] 2509 | name = "strum_macros" 2510 | version = "0.26.4" 2511 | source = "registry+https://github.com/rust-lang/crates.io-index" 2512 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 2513 | dependencies = [ 2514 | "heck 0.5.0", 2515 | "proc-macro2", 2516 | "quote", 2517 | "rustversion", 2518 | "syn 2.0.111", 2519 | ] 2520 | 2521 | [[package]] 2522 | name = "subtle" 2523 | version = "2.6.1" 2524 | source = "registry+https://github.com/rust-lang/crates.io-index" 2525 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2526 | 2527 | [[package]] 2528 | name = "syn" 2529 | version = "1.0.109" 2530 | source = "registry+https://github.com/rust-lang/crates.io-index" 2531 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 2532 | dependencies = [ 2533 | "proc-macro2", 2534 | "quote", 2535 | "unicode-ident", 2536 | ] 2537 | 2538 | [[package]] 2539 | name = "syn" 2540 | version = "2.0.111" 2541 | source = "registry+https://github.com/rust-lang/crates.io-index" 2542 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 2543 | dependencies = [ 2544 | "proc-macro2", 2545 | "quote", 2546 | "unicode-ident", 2547 | ] 2548 | 2549 | [[package]] 2550 | name = "sync_wrapper" 2551 | version = "1.0.2" 2552 | source = "registry+https://github.com/rust-lang/crates.io-index" 2553 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2554 | dependencies = [ 2555 | "futures-core", 2556 | ] 2557 | 2558 | [[package]] 2559 | name = "synstructure" 2560 | version = "0.13.2" 2561 | source = "registry+https://github.com/rust-lang/crates.io-index" 2562 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 2563 | dependencies = [ 2564 | "proc-macro2", 2565 | "quote", 2566 | "syn 2.0.111", 2567 | ] 2568 | 2569 | [[package]] 2570 | name = "system-configuration" 2571 | version = "0.6.1" 2572 | source = "registry+https://github.com/rust-lang/crates.io-index" 2573 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2574 | dependencies = [ 2575 | "bitflags 2.10.0", 2576 | "core-foundation", 2577 | "system-configuration-sys", 2578 | ] 2579 | 2580 | [[package]] 2581 | name = "system-configuration-sys" 2582 | version = "0.6.0" 2583 | source = "registry+https://github.com/rust-lang/crates.io-index" 2584 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 2585 | dependencies = [ 2586 | "core-foundation-sys", 2587 | "libc", 2588 | ] 2589 | 2590 | [[package]] 2591 | name = "tagptr" 2592 | version = "0.2.0" 2593 | source = "registry+https://github.com/rust-lang/crates.io-index" 2594 | checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 2595 | 2596 | [[package]] 2597 | name = "tempfile" 2598 | version = "3.23.0" 2599 | source = "registry+https://github.com/rust-lang/crates.io-index" 2600 | checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 2601 | dependencies = [ 2602 | "fastrand", 2603 | "getrandom 0.3.4", 2604 | "once_cell", 2605 | "rustix", 2606 | "windows-sys 0.61.2", 2607 | ] 2608 | 2609 | [[package]] 2610 | name = "termcolor" 2611 | version = "1.4.1" 2612 | source = "registry+https://github.com/rust-lang/crates.io-index" 2613 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 2614 | dependencies = [ 2615 | "winapi-util", 2616 | ] 2617 | 2618 | [[package]] 2619 | name = "textwrap" 2620 | version = "0.16.2" 2621 | source = "registry+https://github.com/rust-lang/crates.io-index" 2622 | checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 2623 | 2624 | [[package]] 2625 | name = "thiserror" 2626 | version = "1.0.69" 2627 | source = "registry+https://github.com/rust-lang/crates.io-index" 2628 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2629 | dependencies = [ 2630 | "thiserror-impl 1.0.69", 2631 | ] 2632 | 2633 | [[package]] 2634 | name = "thiserror" 2635 | version = "2.0.17" 2636 | source = "registry+https://github.com/rust-lang/crates.io-index" 2637 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 2638 | dependencies = [ 2639 | "thiserror-impl 2.0.17", 2640 | ] 2641 | 2642 | [[package]] 2643 | name = "thiserror-impl" 2644 | version = "1.0.69" 2645 | source = "registry+https://github.com/rust-lang/crates.io-index" 2646 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2647 | dependencies = [ 2648 | "proc-macro2", 2649 | "quote", 2650 | "syn 2.0.111", 2651 | ] 2652 | 2653 | [[package]] 2654 | name = "thiserror-impl" 2655 | version = "2.0.17" 2656 | source = "registry+https://github.com/rust-lang/crates.io-index" 2657 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 2658 | dependencies = [ 2659 | "proc-macro2", 2660 | "quote", 2661 | "syn 2.0.111", 2662 | ] 2663 | 2664 | [[package]] 2665 | name = "thread_local" 2666 | version = "1.1.9" 2667 | source = "registry+https://github.com/rust-lang/crates.io-index" 2668 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 2669 | dependencies = [ 2670 | "cfg-if", 2671 | ] 2672 | 2673 | [[package]] 2674 | name = "thrift_codec" 2675 | version = "0.3.2" 2676 | source = "registry+https://github.com/rust-lang/crates.io-index" 2677 | checksum = "83d957f535b242b91aa9f47bde08080f9a6fef276477e55b0079979d002759d5" 2678 | dependencies = [ 2679 | "byteorder", 2680 | "trackable", 2681 | ] 2682 | 2683 | [[package]] 2684 | name = "time" 2685 | version = "0.3.44" 2686 | source = "registry+https://github.com/rust-lang/crates.io-index" 2687 | checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 2688 | dependencies = [ 2689 | "deranged", 2690 | "itoa", 2691 | "num-conv", 2692 | "powerfmt", 2693 | "serde", 2694 | "time-core", 2695 | "time-macros", 2696 | ] 2697 | 2698 | [[package]] 2699 | name = "time-core" 2700 | version = "0.1.6" 2701 | source = "registry+https://github.com/rust-lang/crates.io-index" 2702 | checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 2703 | 2704 | [[package]] 2705 | name = "time-macros" 2706 | version = "0.2.24" 2707 | source = "registry+https://github.com/rust-lang/crates.io-index" 2708 | checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 2709 | dependencies = [ 2710 | "num-conv", 2711 | "time-core", 2712 | ] 2713 | 2714 | [[package]] 2715 | name = "tinystr" 2716 | version = "0.8.2" 2717 | source = "registry+https://github.com/rust-lang/crates.io-index" 2718 | checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 2719 | dependencies = [ 2720 | "displaydoc", 2721 | "zerovec", 2722 | ] 2723 | 2724 | [[package]] 2725 | name = "tokio" 2726 | version = "1.48.0" 2727 | source = "registry+https://github.com/rust-lang/crates.io-index" 2728 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 2729 | dependencies = [ 2730 | "bytes", 2731 | "libc", 2732 | "mio", 2733 | "parking_lot", 2734 | "pin-project-lite", 2735 | "signal-hook-registry", 2736 | "socket2", 2737 | "tokio-macros", 2738 | "windows-sys 0.61.2", 2739 | ] 2740 | 2741 | [[package]] 2742 | name = "tokio-macros" 2743 | version = "2.6.0" 2744 | source = "registry+https://github.com/rust-lang/crates.io-index" 2745 | checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 2746 | dependencies = [ 2747 | "proc-macro2", 2748 | "quote", 2749 | "syn 2.0.111", 2750 | ] 2751 | 2752 | [[package]] 2753 | name = "tokio-native-tls" 2754 | version = "0.3.1" 2755 | source = "registry+https://github.com/rust-lang/crates.io-index" 2756 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2757 | dependencies = [ 2758 | "native-tls", 2759 | "tokio", 2760 | ] 2761 | 2762 | [[package]] 2763 | name = "tokio-rustls" 2764 | version = "0.26.4" 2765 | source = "registry+https://github.com/rust-lang/crates.io-index" 2766 | checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 2767 | dependencies = [ 2768 | "rustls", 2769 | "tokio", 2770 | ] 2771 | 2772 | [[package]] 2773 | name = "tokio-stream" 2774 | version = "0.1.17" 2775 | source = "registry+https://github.com/rust-lang/crates.io-index" 2776 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 2777 | dependencies = [ 2778 | "futures-core", 2779 | "pin-project-lite", 2780 | "tokio", 2781 | ] 2782 | 2783 | [[package]] 2784 | name = "tokio-test" 2785 | version = "0.4.4" 2786 | source = "registry+https://github.com/rust-lang/crates.io-index" 2787 | checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" 2788 | dependencies = [ 2789 | "async-stream", 2790 | "bytes", 2791 | "futures-core", 2792 | "tokio", 2793 | "tokio-stream", 2794 | ] 2795 | 2796 | [[package]] 2797 | name = "tokio-util" 2798 | version = "0.7.17" 2799 | source = "registry+https://github.com/rust-lang/crates.io-index" 2800 | checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 2801 | dependencies = [ 2802 | "bytes", 2803 | "futures-core", 2804 | "futures-sink", 2805 | "pin-project-lite", 2806 | "tokio", 2807 | ] 2808 | 2809 | [[package]] 2810 | name = "toml" 2811 | version = "0.9.8" 2812 | source = "registry+https://github.com/rust-lang/crates.io-index" 2813 | checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" 2814 | dependencies = [ 2815 | "indexmap 2.12.1", 2816 | "serde_core", 2817 | "serde_spanned", 2818 | "toml_datetime", 2819 | "toml_parser", 2820 | "toml_writer", 2821 | "winnow", 2822 | ] 2823 | 2824 | [[package]] 2825 | name = "toml_datetime" 2826 | version = "0.7.3" 2827 | source = "registry+https://github.com/rust-lang/crates.io-index" 2828 | checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 2829 | dependencies = [ 2830 | "serde_core", 2831 | ] 2832 | 2833 | [[package]] 2834 | name = "toml_parser" 2835 | version = "1.0.4" 2836 | source = "registry+https://github.com/rust-lang/crates.io-index" 2837 | checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 2838 | dependencies = [ 2839 | "winnow", 2840 | ] 2841 | 2842 | [[package]] 2843 | name = "toml_writer" 2844 | version = "1.0.4" 2845 | source = "registry+https://github.com/rust-lang/crates.io-index" 2846 | checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" 2847 | 2848 | [[package]] 2849 | name = "totp-gateway" 2850 | version = "0.1.7" 2851 | dependencies = [ 2852 | "arc-swap", 2853 | "async-trait", 2854 | "bytes", 2855 | "clap 4.5.53", 2856 | "env_logger", 2857 | "ipnet", 2858 | "log", 2859 | "mimalloc", 2860 | "moka", 2861 | "notify", 2862 | "pingora", 2863 | "regex", 2864 | "reqwest", 2865 | "serde", 2866 | "tokio", 2867 | "toml", 2868 | "totp-rs", 2869 | "url", 2870 | "uuid", 2871 | ] 2872 | 2873 | [[package]] 2874 | name = "totp-rs" 2875 | version = "5.7.0" 2876 | source = "registry+https://github.com/rust-lang/crates.io-index" 2877 | checksum = "f124352108f58ef88299e909f6e9470f1cdc8d2a1397963901b4a6366206bf72" 2878 | dependencies = [ 2879 | "base32", 2880 | "constant_time_eq", 2881 | "hmac", 2882 | "rand 0.9.2", 2883 | "sha1", 2884 | "sha2", 2885 | ] 2886 | 2887 | [[package]] 2888 | name = "tower" 2889 | version = "0.5.2" 2890 | source = "registry+https://github.com/rust-lang/crates.io-index" 2891 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2892 | dependencies = [ 2893 | "futures-core", 2894 | "futures-util", 2895 | "pin-project-lite", 2896 | "sync_wrapper", 2897 | "tokio", 2898 | "tower-layer", 2899 | "tower-service", 2900 | ] 2901 | 2902 | [[package]] 2903 | name = "tower-http" 2904 | version = "0.6.6" 2905 | source = "registry+https://github.com/rust-lang/crates.io-index" 2906 | checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 2907 | dependencies = [ 2908 | "bitflags 2.10.0", 2909 | "bytes", 2910 | "futures-util", 2911 | "http", 2912 | "http-body", 2913 | "iri-string", 2914 | "pin-project-lite", 2915 | "tower", 2916 | "tower-layer", 2917 | "tower-service", 2918 | ] 2919 | 2920 | [[package]] 2921 | name = "tower-layer" 2922 | version = "0.3.3" 2923 | source = "registry+https://github.com/rust-lang/crates.io-index" 2924 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2925 | 2926 | [[package]] 2927 | name = "tower-service" 2928 | version = "0.3.3" 2929 | source = "registry+https://github.com/rust-lang/crates.io-index" 2930 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2931 | 2932 | [[package]] 2933 | name = "tracing" 2934 | version = "0.1.41" 2935 | source = "registry+https://github.com/rust-lang/crates.io-index" 2936 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2937 | dependencies = [ 2938 | "pin-project-lite", 2939 | "tracing-core", 2940 | ] 2941 | 2942 | [[package]] 2943 | name = "tracing-core" 2944 | version = "0.1.34" 2945 | source = "registry+https://github.com/rust-lang/crates.io-index" 2946 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 2947 | dependencies = [ 2948 | "once_cell", 2949 | ] 2950 | 2951 | [[package]] 2952 | name = "trackable" 2953 | version = "1.3.0" 2954 | source = "registry+https://github.com/rust-lang/crates.io-index" 2955 | checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" 2956 | dependencies = [ 2957 | "trackable_derive", 2958 | ] 2959 | 2960 | [[package]] 2961 | name = "trackable_derive" 2962 | version = "1.0.0" 2963 | source = "registry+https://github.com/rust-lang/crates.io-index" 2964 | checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" 2965 | dependencies = [ 2966 | "quote", 2967 | "syn 1.0.109", 2968 | ] 2969 | 2970 | [[package]] 2971 | name = "try-lock" 2972 | version = "0.2.5" 2973 | source = "registry+https://github.com/rust-lang/crates.io-index" 2974 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2975 | 2976 | [[package]] 2977 | name = "typenum" 2978 | version = "1.19.0" 2979 | source = "registry+https://github.com/rust-lang/crates.io-index" 2980 | checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 2981 | 2982 | [[package]] 2983 | name = "unicase" 2984 | version = "2.8.1" 2985 | source = "registry+https://github.com/rust-lang/crates.io-index" 2986 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 2987 | 2988 | [[package]] 2989 | name = "unicode-ident" 2990 | version = "1.0.22" 2991 | source = "registry+https://github.com/rust-lang/crates.io-index" 2992 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 2993 | 2994 | [[package]] 2995 | name = "untrusted" 2996 | version = "0.9.0" 2997 | source = "registry+https://github.com/rust-lang/crates.io-index" 2998 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2999 | 3000 | [[package]] 3001 | name = "url" 3002 | version = "2.5.7" 3003 | source = "registry+https://github.com/rust-lang/crates.io-index" 3004 | checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 3005 | dependencies = [ 3006 | "form_urlencoded", 3007 | "idna", 3008 | "percent-encoding", 3009 | "serde", 3010 | ] 3011 | 3012 | [[package]] 3013 | name = "utf8_iter" 3014 | version = "1.0.4" 3015 | source = "registry+https://github.com/rust-lang/crates.io-index" 3016 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 3017 | 3018 | [[package]] 3019 | name = "utf8parse" 3020 | version = "0.2.2" 3021 | source = "registry+https://github.com/rust-lang/crates.io-index" 3022 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 3023 | 3024 | [[package]] 3025 | name = "uuid" 3026 | version = "1.18.1" 3027 | source = "registry+https://github.com/rust-lang/crates.io-index" 3028 | checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 3029 | dependencies = [ 3030 | "getrandom 0.3.4", 3031 | "js-sys", 3032 | "rand 0.9.2", 3033 | "wasm-bindgen", 3034 | ] 3035 | 3036 | [[package]] 3037 | name = "vcpkg" 3038 | version = "0.2.15" 3039 | source = "registry+https://github.com/rust-lang/crates.io-index" 3040 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 3041 | 3042 | [[package]] 3043 | name = "version_check" 3044 | version = "0.9.5" 3045 | source = "registry+https://github.com/rust-lang/crates.io-index" 3046 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3047 | 3048 | [[package]] 3049 | name = "walkdir" 3050 | version = "2.5.0" 3051 | source = "registry+https://github.com/rust-lang/crates.io-index" 3052 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 3053 | dependencies = [ 3054 | "same-file", 3055 | "winapi-util", 3056 | ] 3057 | 3058 | [[package]] 3059 | name = "want" 3060 | version = "0.3.1" 3061 | source = "registry+https://github.com/rust-lang/crates.io-index" 3062 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 3063 | dependencies = [ 3064 | "try-lock", 3065 | ] 3066 | 3067 | [[package]] 3068 | name = "wasi" 3069 | version = "0.11.1+wasi-snapshot-preview1" 3070 | source = "registry+https://github.com/rust-lang/crates.io-index" 3071 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 3072 | 3073 | [[package]] 3074 | name = "wasip2" 3075 | version = "1.0.1+wasi-0.2.4" 3076 | source = "registry+https://github.com/rust-lang/crates.io-index" 3077 | checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 3078 | dependencies = [ 3079 | "wit-bindgen", 3080 | ] 3081 | 3082 | [[package]] 3083 | name = "wasm-bindgen" 3084 | version = "0.2.105" 3085 | source = "registry+https://github.com/rust-lang/crates.io-index" 3086 | checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" 3087 | dependencies = [ 3088 | "cfg-if", 3089 | "once_cell", 3090 | "rustversion", 3091 | "wasm-bindgen-macro", 3092 | "wasm-bindgen-shared", 3093 | ] 3094 | 3095 | [[package]] 3096 | name = "wasm-bindgen-futures" 3097 | version = "0.4.55" 3098 | source = "registry+https://github.com/rust-lang/crates.io-index" 3099 | checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" 3100 | dependencies = [ 3101 | "cfg-if", 3102 | "js-sys", 3103 | "once_cell", 3104 | "wasm-bindgen", 3105 | "web-sys", 3106 | ] 3107 | 3108 | [[package]] 3109 | name = "wasm-bindgen-macro" 3110 | version = "0.2.105" 3111 | source = "registry+https://github.com/rust-lang/crates.io-index" 3112 | checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" 3113 | dependencies = [ 3114 | "quote", 3115 | "wasm-bindgen-macro-support", 3116 | ] 3117 | 3118 | [[package]] 3119 | name = "wasm-bindgen-macro-support" 3120 | version = "0.2.105" 3121 | source = "registry+https://github.com/rust-lang/crates.io-index" 3122 | checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" 3123 | dependencies = [ 3124 | "bumpalo", 3125 | "proc-macro2", 3126 | "quote", 3127 | "syn 2.0.111", 3128 | "wasm-bindgen-shared", 3129 | ] 3130 | 3131 | [[package]] 3132 | name = "wasm-bindgen-shared" 3133 | version = "0.2.105" 3134 | source = "registry+https://github.com/rust-lang/crates.io-index" 3135 | checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" 3136 | dependencies = [ 3137 | "unicode-ident", 3138 | ] 3139 | 3140 | [[package]] 3141 | name = "web-sys" 3142 | version = "0.3.82" 3143 | source = "registry+https://github.com/rust-lang/crates.io-index" 3144 | checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" 3145 | dependencies = [ 3146 | "js-sys", 3147 | "wasm-bindgen", 3148 | ] 3149 | 3150 | [[package]] 3151 | name = "winapi" 3152 | version = "0.3.9" 3153 | source = "registry+https://github.com/rust-lang/crates.io-index" 3154 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 3155 | dependencies = [ 3156 | "winapi-i686-pc-windows-gnu", 3157 | "winapi-x86_64-pc-windows-gnu", 3158 | ] 3159 | 3160 | [[package]] 3161 | name = "winapi-i686-pc-windows-gnu" 3162 | version = "0.4.0" 3163 | source = "registry+https://github.com/rust-lang/crates.io-index" 3164 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 3165 | 3166 | [[package]] 3167 | name = "winapi-util" 3168 | version = "0.1.11" 3169 | source = "registry+https://github.com/rust-lang/crates.io-index" 3170 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 3171 | dependencies = [ 3172 | "windows-sys 0.61.2", 3173 | ] 3174 | 3175 | [[package]] 3176 | name = "winapi-x86_64-pc-windows-gnu" 3177 | version = "0.4.0" 3178 | source = "registry+https://github.com/rust-lang/crates.io-index" 3179 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 3180 | 3181 | [[package]] 3182 | name = "windows-link" 3183 | version = "0.1.3" 3184 | source = "registry+https://github.com/rust-lang/crates.io-index" 3185 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 3186 | 3187 | [[package]] 3188 | name = "windows-link" 3189 | version = "0.2.1" 3190 | source = "registry+https://github.com/rust-lang/crates.io-index" 3191 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 3192 | 3193 | [[package]] 3194 | name = "windows-registry" 3195 | version = "0.6.1" 3196 | source = "registry+https://github.com/rust-lang/crates.io-index" 3197 | checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 3198 | dependencies = [ 3199 | "windows-link 0.2.1", 3200 | "windows-result", 3201 | "windows-strings", 3202 | ] 3203 | 3204 | [[package]] 3205 | name = "windows-result" 3206 | version = "0.4.1" 3207 | source = "registry+https://github.com/rust-lang/crates.io-index" 3208 | checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 3209 | dependencies = [ 3210 | "windows-link 0.2.1", 3211 | ] 3212 | 3213 | [[package]] 3214 | name = "windows-strings" 3215 | version = "0.5.1" 3216 | source = "registry+https://github.com/rust-lang/crates.io-index" 3217 | checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 3218 | dependencies = [ 3219 | "windows-link 0.2.1", 3220 | ] 3221 | 3222 | [[package]] 3223 | name = "windows-sys" 3224 | version = "0.52.0" 3225 | source = "registry+https://github.com/rust-lang/crates.io-index" 3226 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3227 | dependencies = [ 3228 | "windows-targets 0.52.6", 3229 | ] 3230 | 3231 | [[package]] 3232 | name = "windows-sys" 3233 | version = "0.59.0" 3234 | source = "registry+https://github.com/rust-lang/crates.io-index" 3235 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3236 | dependencies = [ 3237 | "windows-targets 0.52.6", 3238 | ] 3239 | 3240 | [[package]] 3241 | name = "windows-sys" 3242 | version = "0.60.2" 3243 | source = "registry+https://github.com/rust-lang/crates.io-index" 3244 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3245 | dependencies = [ 3246 | "windows-targets 0.53.5", 3247 | ] 3248 | 3249 | [[package]] 3250 | name = "windows-sys" 3251 | version = "0.61.2" 3252 | source = "registry+https://github.com/rust-lang/crates.io-index" 3253 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 3254 | dependencies = [ 3255 | "windows-link 0.2.1", 3256 | ] 3257 | 3258 | [[package]] 3259 | name = "windows-targets" 3260 | version = "0.52.6" 3261 | source = "registry+https://github.com/rust-lang/crates.io-index" 3262 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3263 | dependencies = [ 3264 | "windows_aarch64_gnullvm 0.52.6", 3265 | "windows_aarch64_msvc 0.52.6", 3266 | "windows_i686_gnu 0.52.6", 3267 | "windows_i686_gnullvm 0.52.6", 3268 | "windows_i686_msvc 0.52.6", 3269 | "windows_x86_64_gnu 0.52.6", 3270 | "windows_x86_64_gnullvm 0.52.6", 3271 | "windows_x86_64_msvc 0.52.6", 3272 | ] 3273 | 3274 | [[package]] 3275 | name = "windows-targets" 3276 | version = "0.53.5" 3277 | source = "registry+https://github.com/rust-lang/crates.io-index" 3278 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 3279 | dependencies = [ 3280 | "windows-link 0.2.1", 3281 | "windows_aarch64_gnullvm 0.53.1", 3282 | "windows_aarch64_msvc 0.53.1", 3283 | "windows_i686_gnu 0.53.1", 3284 | "windows_i686_gnullvm 0.53.1", 3285 | "windows_i686_msvc 0.53.1", 3286 | "windows_x86_64_gnu 0.53.1", 3287 | "windows_x86_64_gnullvm 0.53.1", 3288 | "windows_x86_64_msvc 0.53.1", 3289 | ] 3290 | 3291 | [[package]] 3292 | name = "windows_aarch64_gnullvm" 3293 | version = "0.52.6" 3294 | source = "registry+https://github.com/rust-lang/crates.io-index" 3295 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3296 | 3297 | [[package]] 3298 | name = "windows_aarch64_gnullvm" 3299 | version = "0.53.1" 3300 | source = "registry+https://github.com/rust-lang/crates.io-index" 3301 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 3302 | 3303 | [[package]] 3304 | name = "windows_aarch64_msvc" 3305 | version = "0.52.6" 3306 | source = "registry+https://github.com/rust-lang/crates.io-index" 3307 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3308 | 3309 | [[package]] 3310 | name = "windows_aarch64_msvc" 3311 | version = "0.53.1" 3312 | source = "registry+https://github.com/rust-lang/crates.io-index" 3313 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 3314 | 3315 | [[package]] 3316 | name = "windows_i686_gnu" 3317 | version = "0.52.6" 3318 | source = "registry+https://github.com/rust-lang/crates.io-index" 3319 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3320 | 3321 | [[package]] 3322 | name = "windows_i686_gnu" 3323 | version = "0.53.1" 3324 | source = "registry+https://github.com/rust-lang/crates.io-index" 3325 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 3326 | 3327 | [[package]] 3328 | name = "windows_i686_gnullvm" 3329 | version = "0.52.6" 3330 | source = "registry+https://github.com/rust-lang/crates.io-index" 3331 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 3332 | 3333 | [[package]] 3334 | name = "windows_i686_gnullvm" 3335 | version = "0.53.1" 3336 | source = "registry+https://github.com/rust-lang/crates.io-index" 3337 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 3338 | 3339 | [[package]] 3340 | name = "windows_i686_msvc" 3341 | version = "0.52.6" 3342 | source = "registry+https://github.com/rust-lang/crates.io-index" 3343 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3344 | 3345 | [[package]] 3346 | name = "windows_i686_msvc" 3347 | version = "0.53.1" 3348 | source = "registry+https://github.com/rust-lang/crates.io-index" 3349 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 3350 | 3351 | [[package]] 3352 | name = "windows_x86_64_gnu" 3353 | version = "0.52.6" 3354 | source = "registry+https://github.com/rust-lang/crates.io-index" 3355 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3356 | 3357 | [[package]] 3358 | name = "windows_x86_64_gnu" 3359 | version = "0.53.1" 3360 | source = "registry+https://github.com/rust-lang/crates.io-index" 3361 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 3362 | 3363 | [[package]] 3364 | name = "windows_x86_64_gnullvm" 3365 | version = "0.52.6" 3366 | source = "registry+https://github.com/rust-lang/crates.io-index" 3367 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3368 | 3369 | [[package]] 3370 | name = "windows_x86_64_gnullvm" 3371 | version = "0.53.1" 3372 | source = "registry+https://github.com/rust-lang/crates.io-index" 3373 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 3374 | 3375 | [[package]] 3376 | name = "windows_x86_64_msvc" 3377 | version = "0.52.6" 3378 | source = "registry+https://github.com/rust-lang/crates.io-index" 3379 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3380 | 3381 | [[package]] 3382 | name = "windows_x86_64_msvc" 3383 | version = "0.53.1" 3384 | source = "registry+https://github.com/rust-lang/crates.io-index" 3385 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 3386 | 3387 | [[package]] 3388 | name = "winnow" 3389 | version = "0.7.13" 3390 | source = "registry+https://github.com/rust-lang/crates.io-index" 3391 | checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 3392 | 3393 | [[package]] 3394 | name = "wit-bindgen" 3395 | version = "0.46.0" 3396 | source = "registry+https://github.com/rust-lang/crates.io-index" 3397 | checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 3398 | 3399 | [[package]] 3400 | name = "writeable" 3401 | version = "0.6.2" 3402 | source = "registry+https://github.com/rust-lang/crates.io-index" 3403 | checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 3404 | 3405 | [[package]] 3406 | name = "yaml-rust" 3407 | version = "0.4.5" 3408 | source = "registry+https://github.com/rust-lang/crates.io-index" 3409 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 3410 | dependencies = [ 3411 | "linked-hash-map", 3412 | ] 3413 | 3414 | [[package]] 3415 | name = "yoke" 3416 | version = "0.8.1" 3417 | source = "registry+https://github.com/rust-lang/crates.io-index" 3418 | checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 3419 | dependencies = [ 3420 | "stable_deref_trait", 3421 | "yoke-derive", 3422 | "zerofrom", 3423 | ] 3424 | 3425 | [[package]] 3426 | name = "yoke-derive" 3427 | version = "0.8.1" 3428 | source = "registry+https://github.com/rust-lang/crates.io-index" 3429 | checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 3430 | dependencies = [ 3431 | "proc-macro2", 3432 | "quote", 3433 | "syn 2.0.111", 3434 | "synstructure", 3435 | ] 3436 | 3437 | [[package]] 3438 | name = "zerocopy" 3439 | version = "0.8.28" 3440 | source = "registry+https://github.com/rust-lang/crates.io-index" 3441 | checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" 3442 | dependencies = [ 3443 | "zerocopy-derive", 3444 | ] 3445 | 3446 | [[package]] 3447 | name = "zerocopy-derive" 3448 | version = "0.8.28" 3449 | source = "registry+https://github.com/rust-lang/crates.io-index" 3450 | checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" 3451 | dependencies = [ 3452 | "proc-macro2", 3453 | "quote", 3454 | "syn 2.0.111", 3455 | ] 3456 | 3457 | [[package]] 3458 | name = "zerofrom" 3459 | version = "0.1.6" 3460 | source = "registry+https://github.com/rust-lang/crates.io-index" 3461 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 3462 | dependencies = [ 3463 | "zerofrom-derive", 3464 | ] 3465 | 3466 | [[package]] 3467 | name = "zerofrom-derive" 3468 | version = "0.1.6" 3469 | source = "registry+https://github.com/rust-lang/crates.io-index" 3470 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 3471 | dependencies = [ 3472 | "proc-macro2", 3473 | "quote", 3474 | "syn 2.0.111", 3475 | "synstructure", 3476 | ] 3477 | 3478 | [[package]] 3479 | name = "zeroize" 3480 | version = "1.8.2" 3481 | source = "registry+https://github.com/rust-lang/crates.io-index" 3482 | checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 3483 | 3484 | [[package]] 3485 | name = "zerotrie" 3486 | version = "0.2.3" 3487 | source = "registry+https://github.com/rust-lang/crates.io-index" 3488 | checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 3489 | dependencies = [ 3490 | "displaydoc", 3491 | "yoke", 3492 | "zerofrom", 3493 | ] 3494 | 3495 | [[package]] 3496 | name = "zerovec" 3497 | version = "0.11.5" 3498 | source = "registry+https://github.com/rust-lang/crates.io-index" 3499 | checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 3500 | dependencies = [ 3501 | "yoke", 3502 | "zerofrom", 3503 | "zerovec-derive", 3504 | ] 3505 | 3506 | [[package]] 3507 | name = "zerovec-derive" 3508 | version = "0.11.2" 3509 | source = "registry+https://github.com/rust-lang/crates.io-index" 3510 | checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 3511 | dependencies = [ 3512 | "proc-macro2", 3513 | "quote", 3514 | "syn 2.0.111", 3515 | ] 3516 | 3517 | [[package]] 3518 | name = "zstd" 3519 | version = "0.13.3" 3520 | source = "registry+https://github.com/rust-lang/crates.io-index" 3521 | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 3522 | dependencies = [ 3523 | "zstd-safe", 3524 | ] 3525 | 3526 | [[package]] 3527 | name = "zstd-safe" 3528 | version = "7.2.4" 3529 | source = "registry+https://github.com/rust-lang/crates.io-index" 3530 | checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 3531 | dependencies = [ 3532 | "zstd-sys", 3533 | ] 3534 | 3535 | [[package]] 3536 | name = "zstd-sys" 3537 | version = "2.0.16+zstd.1.5.7" 3538 | source = "registry+https://github.com/rust-lang/crates.io-index" 3539 | checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" 3540 | dependencies = [ 3541 | "cc", 3542 | "pkg-config", 3543 | ] 3544 | --------------------------------------------------------------------------------