diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index f369695..606ea3f 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -4,14 +4,11 @@ on: paths: - '**/Cargo.toml' - '**/Cargo.lock' - push: - paths: - - '**/Cargo.toml' - - '**/Cargo.lock' schedule: - cron: '3 3 3 * *' jobs: security_audit: + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9b0c03f..604228e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,8 +12,6 @@ name: "CodeQL" on: - push: - branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] @@ -23,6 +21,7 @@ on: jobs: analyze: name: Analyze + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest permissions: actions: read diff --git a/.github/workflows/committed.yml b/.github/workflows/committed.yml index 8375557..7193b9c 100644 --- a/.github/workflows/committed.yml +++ b/.github/workflows/committed.yml @@ -6,6 +6,7 @@ on: [pull_request] jobs: committed: name: Lint Commits + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: - name: Checkout Actions Repository diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index e3c61b6..6518ee5 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -1,6 +1,6 @@ name: rustdoc on: - push: + pull_request: branches: - main @@ -11,7 +11,7 @@ env: jobs: rustdoc: - if: github.repository == 'emo-cat/rust-template' + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 4ba21a3..25026c0 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,10 +1,10 @@ name: pre-commit on: pull_request: - push: branches: [main] jobs: pre-commit: + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1deff8f..cb798fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,26 +6,18 @@ on: - '!/*.md' - '!/docs/**' - "!/LICENSE-*" - push: - branches: - - main - paths: - - '**' - - '!/*.md' - - '!/docs/**' - - "!/LICENSE-*" jobs: ci: name: CI + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' needs: [ test,rustfmt,clippy ] - if: github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: - name: Done run: exit 0 test: name: Test - if: github.actor != 'dependabot[bot]' + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' strategy: matrix: os: [ "ubuntu-latest", "windows-latest", "macos-latest" ] @@ -48,7 +40,7 @@ jobs: run: cargo test --workspace rustfmt: name: rustfmt - if: github.actor != 'dependabot[bot]' + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: - name: Checkout repository @@ -67,7 +59,7 @@ jobs: run: cargo fmt --all -- --check clippy: name: clippy - if: github.actor != 'dependabot[bot]' + if: startsWith(github.repository, 'emo-crab/') && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.gitignore b/.gitignore index d0da6e5..4c00f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,8 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk -.idea/ \ No newline at end of file +.idea/ + +# MITM proxy CA certificate storage +.slinger-mitm*/ +*.pem \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b4795de..922d91d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,30 @@ -[package] -name = "slinger" #改这个 -version = "0.2.9" +[workspace] +resolver = "2" +members = [ + "slinger", + "slinger-mitm", +] + +[workspace.package] edition = "2021" -description = "An HTTP Client for Rust designed for hackers." -homepage = "https://github.com/emo-crab/slinger" -repository = "https://github.com/emo-crab/slinger" authors = ["Kali-Team "] -readme = "README.md" license = "GPL-3.0-only" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +repository = "https://github.com/emo-crab/slinger" +homepage = "https://github.com/emo-crab/slinger" + +[workspace.dependencies] +# Shared dependencies +thiserror = "2" +http = "1.3.1" +socket2 = "0.6.1" +bytes = "1.6.1" +tracing = "0.1" +tokio = { version = "1", default-features = false } +tokio-rustls = { version = "0.26.4", features = ["ring", "tls12"], default-features = false } +rustls-pemfile = "2" +rustls-pki-types = { version = "1.1.0", features = ["alloc"] } +rustls-native-certs = "0.8" -#https://github.com/johnthagen/min-sized-rust [profile.release] opt-level = "z" # Optimize for size. lto = true # Enable Link Time Optimization @@ -20,6 +34,7 @@ strip = true # Automatically strip symbols from the binary. [profile.dev.package."*"] opt-level = 3 + [profile.test] opt-level = 3 lto = "thin" @@ -28,45 +43,3 @@ lto = "thin" lto = true codegen-units = 1 opt-level = 3 - - -[dependencies] -thiserror = "2" -http = { version = "1.3.1" } -percent-encoding = "2.1" -socket2 = "0.6.1" -bytes = "1.6.1" -base64 = "0.22" -flate2 = { version = "1.0", default-features = false, features = ["default"], optional = true } -serde = { version = "1", features = ["derive"], optional = true } -http-serde = { version = "2.1.0", optional = true } -encoding_rs = { version = "0.8", optional = true } -mime = { version = "0.3.17", optional = true } -cookie = { version = "0.18.1", optional = true } -tokio-rustls = { version = "0.26.2", optional = true, features = ["ring", "tls12"], default-features = false } -tokio = { version = "1", default-features = false, features = ["net", "io-util", "time"] } -rustls-pemfile = { version = "2", optional = true } -rustls-pki-types = { version = "1.1.0", features = ["alloc"], optional = true } -rustls-native-certs = { version = "0.8", optional = true } -h2 = { version = "0.4", optional = true } -schemars = { version = "1.0.4", optional = true, features = ["bytes1"] } -[dev-dependencies] -tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread"] } -futures = { version = "0.3.31" } -serde_json = "1" -tokio-native-tls = "0.3" -openssl = { version = "0.10.71", features = ["vendored"] } -[features] -default = [] -# TLS feature hierarchy: -# - `tls`: Base TLS feature flag (enables TLS types and interfaces, but no backend) -# - `rustls`: Rustls TLS backend (requires `tls`) -# Note: If `tls` is enabled without a backend, you must provide a custom TLS connector callback. -tls = [] -rustls = ["tls", "tokio-rustls", "rustls-pemfile", "rustls-pki-types", "rustls-native-certs"] -http2 = ["dep:h2"] -cookie = ["dep:cookie"] -charset = ["dep:encoding_rs", "dep:mime"] -serde = ["dep:serde", "http-serde"] -gzip = ["flate2"] -schema = ["dep:schemars", "serde"] \ No newline at end of file diff --git a/README.md b/README.md index 39a85bd..f9c5c18 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ *** that would make this better, please fork the repo and create a pull request *** or simply open an issue with the tag "enhancement". *** Don't forget to give the project a star! -*** Thanks again! Now go create something AMAZING! :D +*** Thanks again! Now go create something AMAZING! --> @@ -57,12 +57,28 @@ ![Product Name Screen Shot][product-screenshot] +**Slinger** is a workspace containing: + +### slinger +The core HTTP client library for Rust designed for hackers. + - customizable redirect policy - http/https and socks5/socks5h proxies - cookie store - raw socket request - HTTPS via tls +### slinger-mitm +A Man-in-the-Middle (MITM) proxy with transparent HTTPS traffic interception, similar to Burp Suite. + +- Automatic CA certificate generation with improved certificate management (inspired by [hudsucker](https://github.com/omjadas/hudsucker)) +- Certificate caching for high performance +- Transparent HTTPS interception using rustls backend +- Traffic interception and modification interfaces +- Random serial numbers and clock skew handling +- Reuses slinger's Socket implementation +- Minimal external dependencies +

(back to top)

@@ -71,11 +87,13 @@ ## Getting Started +### Using slinger (HTTP Client) + This example enables some optional features, so your `Cargo.toml` could look like this: ```toml [dependencies] -slinger = { version = "0.2.2", features = ["serde", "cookie", "charset", "tls", "rustls", "gzip"] } +slinger = { version = "0.2.9", features = ["serde", "cookie", "charset", "tls", "rustls", "gzip"] } ``` And then the code: @@ -89,6 +107,40 @@ async fn main() -> Result<(), Box> { } ``` +### Using slinger-mitm (MITM Proxy) + +Add to your `Cargo.toml`: + +```toml +[dependencies] +slinger-mitm = { version = "0.2.9" } +tokio = { version = "1", features = ["full"] } +``` + +Example code: + +```rust,no_run +use slinger_mitm::{MitmConfig, MitmProxy, Interceptor}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let config = MitmConfig::default(); + let proxy = MitmProxy::new(config).await?; + + // Add logging interceptor + let handler = proxy.interceptor_handler(); + let mut h = handler.write().await; + h.add_request_interceptor(Arc::new(Interceptor::logging())); + drop(h); + + proxy.start("127.0.0.1:8080").await?; + Ok(()) +} +``` + +See [slinger-mitm/README.md](slinger-mitm/README.md) for more details on MITM proxy usage. + ## Features diff --git a/slinger-mitm/Cargo.toml b/slinger-mitm/Cargo.toml new file mode 100644 index 0000000..5212b63 --- /dev/null +++ b/slinger-mitm/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "slinger-mitm" +version = "0.0.1" +edition.workspace = true +description = "MITM proxy with transparent traffic interception using rustls backend for slinger" +homepage.workspace = true +repository.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +# Core slinger library +slinger = { path = "../slinger", features = ["tls", "rustls", "http2"] } +# Workspace dependencies +thiserror.workspace = true +http.workspace = true +bytes.workspace = true +tracing.workspace = true +tokio = { workspace = true, features = ["net", "io-util", "rt", "sync", "fs", "macros"] } +tokio-rustls = { workspace = true } +rustls-pemfile.workspace = true + +# MITM-specific dependencies +rcgen = { version = "0.14", features = ["x509-parser"] } +time = "0.3" +async-trait = "0.1" +moka = { version = "0.12", features = ["future"] } +rand = "0.8" + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "test-util"] } + +[features] +default = [] diff --git a/slinger-mitm/README.md b/slinger-mitm/README.md new file mode 100644 index 0000000..2a69447 --- /dev/null +++ b/slinger-mitm/README.md @@ -0,0 +1,240 @@ +# Slinger MITM + +A Man-in-the-Middle (MITM) proxy library with transparent HTTPS traffic interception using rustls backend, similar to Burp Suite. + +## Features + +- **Automatic CA Certificate Generation** - Automatically generates and manages root CA certificates +- **Transparent HTTPS Interception** - Intercepts HTTPS traffic using rustls backend +- **Traffic Modification Interface** - Provides interfaces to intercept and modify HTTP/HTTPS requests and responses +- **Built on Slinger** - Reuses the robust Socket implementation from the slinger HTTP client +- **Multi-Protocol Support** - Supports both HTTP proxy and SOCKS5 protocols on the same port +- **Automatic Protocol Detection** - Automatically detects and handles HTTP or SOCKS5 connections +- **Minimal Dependencies** - Uses only essential libraries to keep the footprint small +- **High Performance** - Certificate caching and optimized certificate generation +- **Clock Skew Handling** - Built-in NOT_BEFORE_OFFSET to handle client clock differences +- **Unique Certificate Serial Numbers** - Each generated certificate has a unique random serial number + +### CA Certificate Generation + +The MITM proxy uses an advanced certificate generation approach inspired by [hudsucker](https://github.com/omjadas/hudsucker): + +- **Issuer Pattern**: Uses rcgen 0.14's `Issuer` for clean separation between root CA and server certificates +- **Certificate Caching**: Generated server certificates are cached using moka for improved performance +- **Random Serial Numbers**: Each certificate gets a unique random serial number to avoid conflicts +- **Clock Skew Tolerance**: NOT_BEFORE_OFFSET of 60 seconds handles client clock differences +- **Long Validity**: CA certificates valid for 10 years, server certificates for 1 year + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +slinger-mitm = { path = "../slinger-mitm" } +tokio = { version = "1", features = ["full"] } +``` + +## Quick Start + +### Basic Proxy + +```rust +use slinger_mitm::{MitmConfig, MitmProxy, Interceptor}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create proxy with default configuration + let config = MitmConfig::default(); + let proxy = MitmProxy::new(config).await?; + + // Add logging interceptor + let interceptor_handler = proxy.interceptor_handler(); + let mut handler = interceptor_handler.write().await; + handler.add_request_interceptor(Arc::new(Interceptor::logging())); + handler.add_response_interceptor(Arc::new(Interceptor::logging())); + drop(handler); + + // Start the proxy + proxy.start("127.0.0.1:8080").await?; + Ok(()) +} +``` + +### Custom Interceptors + +```rust +use async_trait::async_trait; +use bytes::Bytes; +use http::{Request, Response, HeaderValue}; +use slinger_mitm::{RequestInterceptor, ResponseInterceptor, Result}; + +struct CustomInterceptor; + +#[async_trait] +impl RequestInterceptor for CustomInterceptor { + async fn intercept_request(&self, mut request: Request) -> Result>> { + // Modify request headers + request.headers_mut().insert( + "X-Custom-Header", + HeaderValue::from_static("value"), + ); + Ok(Some(request)) + } +} + +#[async_trait] +impl ResponseInterceptor for CustomInterceptor { + async fn intercept_response(&self, mut response: Response) -> Result>> { + // Modify response + response.headers_mut().insert( + "X-Modified", + HeaderValue::from_static("true"), + ); + Ok(Some(response)) + } +} +``` + +## Usage + +### 1. Start the Proxy + +Run your proxy application: + +```bash +cargo run --example simple_proxy +``` + +### 2. Install CA Certificate + +The proxy will automatically generate a CA certificate at `.slinger-mitm/ca_cert.pem`. + +**For Firefox:** +1. Go to Settings → Privacy & Security → Certificates → View Certificates +2. Click "Import" and select the CA certificate +3. Trust it for identifying websites + +**For Chrome/System:** +- **Linux:** Copy to `/usr/local/share/ca-certificates/` and run `sudo update-ca-certificates` +- **macOS:** Add to Keychain Access and trust it +- **Windows:** Double-click the certificate and install it to "Trusted Root Certification Authorities" + +### 3. Configure Browser/Client Proxy + +The MITM proxy supports both HTTP and SOCKS5 protocols: + +**HTTP Proxy Mode:** +Set your browser to use HTTP proxy at `127.0.0.1:8080` (or whatever port you configured). + +**SOCKS5 Proxy Mode:** +Set your browser/application to use SOCKS5 proxy at `127.0.0.1:8080`. The proxy automatically detects the protocol and handles both HTTP and SOCKS5 connections on the same port. + +**Protocol Support:** +- HTTP proxy (CONNECT method for HTTPS) +- SOCKS5 protocol +- Automatic protocol detection based on connection handshake +- Support for non-HTTP protocols through SOCKS5 tunneling + +## Configuration + +```rust +use slinger_mitm::MitmConfig; +use std::path::PathBuf; + +let config = MitmConfig { + // Directory to store CA certificates + ca_storage_path: PathBuf::from(".slinger-mitm"), + + // Enable HTTPS interception (requires CA cert installation) + enable_https_interception: true, + + // Maximum concurrent connections + max_connections: 1000, + + // Connection timeout in seconds + connection_timeout: 30, + + // Optional upstream proxy (supports HTTP, HTTPS, SOCKS5, SOCKS5h) + // Example: Some(slinger::Proxy::parse("socks5h://127.0.0.1:1080")?) + upstream_proxy: None, +}; +``` + +### Upstream Proxy Support + +You can configure slinger-mitm to forward all traffic through an upstream proxy. This is useful for: +- Chaining with other proxies (like Tor, Burp Suite, or corporate proxies) +- Using remote DNS resolution with SOCKS5h +- Adding authentication to upstream proxies + +```rust +use slinger_mitm::MitmConfig; + +// Configure with SOCKS5h proxy (remote DNS resolution) +let proxy = slinger::Proxy::parse("socks5h://127.0.0.1:9050")?; +let config = MitmConfig { + upstream_proxy: Some(proxy), + ..Default::default() +}; + +// Supported proxy types: +// - HTTP: "http://proxy.example.com:8080" +// - HTTPS: "https://proxy.example.com:8443" +// - SOCKS5: "socks5://127.0.0.1:1080" +// - SOCKS5h: "socks5h://127.0.0.1:1080" (with remote DNS) +// - With auth: "socks5h://user:pass@127.0.0.1:1080" +``` + + +## Architecture + +``` +Client → MITM Proxy → Target Server + ↓ + Request Interceptor + ↓ + Slinger HTTP Client + ↓ + Response Interceptor + ↓ + Client +``` + +The proxy works by: +1. Accepting client connections +2. For HTTPS: Generating on-the-fly certificates signed by the root CA +3. Establishing TLS connection with client using the generated certificate +4. Parsing the HTTP request +5. Running it through request interceptors +6. Forwarding to the target server using slinger client +7. Running the response through response interceptors +8. Sending the modified response back to the client + +## Examples + +See the `examples/` directory for more examples: + +- `simple_proxy.rs` - Basic logging proxy +- `custom_interceptor.rs` - Custom request/response modification +- `proxy_chain.rs` - MITM proxy with upstream proxy support (SOCKS5h, HTTP, etc.) + +Run an example: + +```bash +cargo run --example simple_proxy +cargo run --example proxy_chain +``` + +## Security Note + +WARNING: This is a MITM proxy tool designed for security testing and debugging. Only use it on networks and systems you have permission to test. Installing the CA certificate allows the proxy to decrypt all HTTPS traffic, so protect it accordingly. + +## License + +GPL-3.0-only + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/slinger-mitm/examples/custom_interceptor.rs b/slinger-mitm/examples/custom_interceptor.rs new file mode 100644 index 0000000..60119f2 --- /dev/null +++ b/slinger-mitm/examples/custom_interceptor.rs @@ -0,0 +1,101 @@ +//! MITM proxy with custom traffic modification +//! +//! This example demonstrates how to create custom interceptors +//! to modify HTTP requests and responses. +//! +//! To run: +//! ```bash +//! cargo run --example custom_interceptor +//! ``` + +use async_trait::async_trait; +use http::HeaderValue; +use slinger::{Request, Response}; +use slinger_mitm::{MitmConfig, MitmProxy, RequestInterceptor, ResponseInterceptor, Result}; +use std::sync::Arc; + +/// Custom request interceptor that adds a custom header +struct CustomHeaderInterceptor; + +#[async_trait] +impl RequestInterceptor for CustomHeaderInterceptor { + async fn intercept_request(&self, mut request: Request) -> Result> { + println!("[CUSTOM] Intercepting request to: {}", request.uri()); + + // Add a custom header + request + .headers_mut() + .insert("X-Slinger-MITM", HeaderValue::from_static("true")); + + // Modify User-Agent + if request.headers().contains_key("User-Agent") { + request.headers_mut().insert( + "User-Agent", + HeaderValue::from_static("Slinger-MITM-Proxy/1.0"), + ); + } + + Ok(Some(request)) + } +} + +/// Custom response interceptor that modifies response headers +struct ResponseModifierInterceptor; + +#[async_trait] +impl ResponseInterceptor for ResponseModifierInterceptor { + async fn intercept_response(&self, mut response: Response) -> Result> { + println!("[CUSTOM] Intercepting response: {}", response.status_code()); + + // Add a custom header to the response + response.headers_mut().insert( + "X-Slinger-MITM-Response", + HeaderValue::from_static("modified"), + ); + + // You could also modify the body here if needed + // let body = response.body(); + // let modified_body = modify_body(body); + // *response.body_mut() = modified_body; + + Ok(Some(response)) + } +} + +#[tokio::main] +async fn main() -> std::result::Result<(), Box> { + println!("=== Slinger MITM Proxy with Custom Interceptors ===\n"); + + // Create proxy with custom storage path + let config = MitmConfig { + ca_storage_path: std::path::PathBuf::from(".slinger-mitm-custom"), + enable_https_interception: true, + max_connections: 1000, + connection_timeout: 30, + upstream_proxy: None, + }; + + let proxy = MitmProxy::new(config).await?; + + // Add custom interceptors + let interceptor_handler = proxy.interceptor_handler(); + let mut handler = interceptor_handler.write().await; + + // Add our custom interceptors + handler.add_request_interceptor(Arc::new(CustomHeaderInterceptor)); + handler.add_response_interceptor(Arc::new(ResponseModifierInterceptor)); + + drop(handler); // Release write lock + + // Start the proxy + println!("Starting MITM proxy on 127.0.0.1:8888"); + println!("CA certificate: {}\n", proxy.ca_cert_path().display()); + println!("This proxy will:"); + println!(" - Add X-Slinger-MITM header to all requests"); + println!(" - Modify User-Agent header"); + println!(" - Add X-Slinger-MITM-Response header to all responses\n"); + + proxy.start("127.0.0.1:8888").await?; + + Ok(()) +} diff --git a/slinger-mitm/examples/proxy_chain.rs b/slinger-mitm/examples/proxy_chain.rs new file mode 100644 index 0000000..7661703 --- /dev/null +++ b/slinger-mitm/examples/proxy_chain.rs @@ -0,0 +1,93 @@ +//! MITM proxy with upstream proxy support +//! +//! This example demonstrates how to configure slinger-mitm to forward +//! traffic through an upstream proxy (HTTP, HTTPS, SOCKS5, or SOCKS5h). +//! +//! This is useful for chaining proxies or using slinger-mitm with tools +//! like Tor, corporate proxies, or other SOCKS5 proxies. +//! +//! To run: +//! ```bash +//! cargo run --example proxy_chain +//! ``` +//! +//! Then configure your browser to use the proxy at 127.0.0.1:8080 +//! Install the CA certificate from .slinger-mitm/ca_cert.pem +//! +//! ## Example proxy types: +//! - SOCKS5: socks5://127.0.0.1:1080 +//! - SOCKS5h (with remote DNS): socks5h://127.0.0.1:1080 +//! - HTTP: http://proxy.example.com:8080 +//! - HTTPS: https://proxy.example.com:8443 +//! - With authentication: socks5h://user:pass@127.0.0.1:1080 + +use slinger_mitm::{Interceptor, MitmConfig, MitmProxy}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== Slinger MITM Proxy with Upstream Proxy ===\n"); + + // Configure upstream proxy + // Change this to your actual proxy address + // Examples: + // - For Tor: "socks5h://127.0.0.1:9050" + // - For corporate proxy: "http://proxy.company.com:8080" + // - For another SOCKS5 proxy: "socks5h://127.0.0.1:1080" + // + // Set to None to run without upstream proxy: + // let upstream_proxy_url = None; + let upstream_proxy_url = Some("http://127.0.0.1:8080"); + + // Parse the proxy URL + let proxy = match upstream_proxy_url { + Some(url) => { + let parsed = slinger::Proxy::parse(url)?; + println!("✓ Configured upstream proxy: {}", url); + Some(parsed) + } + None => { + println!("ℹ Running without upstream proxy"); + None + } + }; + + // Create MITM proxy configuration with upstream proxy + let config = MitmConfig { + ca_storage_path: std::path::PathBuf::from(".slinger-mitm"), + enable_https_interception: true, + max_connections: 1000, + connection_timeout: 30, + upstream_proxy: proxy, + }; + + let mitm_proxy = MitmProxy::new(config).await?; + + // Add a logging interceptor to see traffic + let interceptor_handler = mitm_proxy.interceptor_handler(); + let mut handler = interceptor_handler.write().await; + handler.add_request_interceptor(Arc::new(Interceptor::logging())); + handler.add_response_interceptor(Arc::new(Interceptor::logging())); + drop(handler); // Release write lock + + // Start the proxy + println!("\nStarting MITM proxy on 127.0.0.1:8080"); + println!("CA certificate: {}\n", mitm_proxy.ca_cert_path().display()); + println!("Traffic flow:"); + if let Some(proxy_url) = upstream_proxy_url { + println!( + " Browser → MITM Proxy (127.0.0.1:8080) → Upstream Proxy ({}) → Internet\n", + proxy_url + ); + } else { + println!(" Browser → MITM Proxy (127.0.0.1:8080) → Internet\n"); + } + println!("To use this proxy:"); + println!("1. Configure your browser to use HTTP proxy: 127.0.0.1:8080"); + println!("2. Install the CA certificate in your browser/system"); + println!("3. Visit any HTTP/HTTPS website\n"); + + mitm_proxy.start("127.0.0.1:2008").await?; + + Ok(()) +} diff --git a/slinger-mitm/examples/simple_proxy.rs b/slinger-mitm/examples/simple_proxy.rs new file mode 100644 index 0000000..4d16e36 --- /dev/null +++ b/slinger-mitm/examples/simple_proxy.rs @@ -0,0 +1,43 @@ +//! Simple MITM proxy example +//! +//! This example demonstrates how to start a basic MITM proxy server +//! with logging interceptor. +//! +//! To run: +//! ```bash +//! cargo run --example simple_proxy +//! ``` +//! +//! Then configure your browser to use the proxy at 127.0.0.1:8080 +//! Install the CA certificate from .slinger-mitm/ca_cert.pem + +use slinger_mitm::{Interceptor, MitmConfig, MitmProxy}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== Slinger MITM Proxy ===\n"); + + // Create proxy with default configuration + let config = MitmConfig::default(); + let proxy = MitmProxy::new(config).await?; + + // Add a logging interceptor to see traffic + let interceptor_handler = proxy.interceptor_handler(); + let mut handler = interceptor_handler.write().await; + handler.add_request_interceptor(Arc::new(Interceptor::logging())); + handler.add_response_interceptor(Arc::new(Interceptor::logging())); + drop(handler); // Release write lock + + // Start the proxy + println!("Starting MITM proxy on 127.0.0.1:8080"); + println!("CA certificate: {}\n", proxy.ca_cert_path().display()); + println!("To use this proxy:"); + println!("1. Configure your browser to use HTTP proxy: 127.0.0.1:8080"); + println!("2. Install the CA certificate in your browser/system"); + println!("3. Visit any HTTP/HTTPS website\n"); + + proxy.start("127.0.0.1:2008").await?; + + Ok(()) +} diff --git a/slinger-mitm/examples/socks5_proxy.rs b/slinger-mitm/examples/socks5_proxy.rs new file mode 100644 index 0000000..72763a1 --- /dev/null +++ b/slinger-mitm/examples/socks5_proxy.rs @@ -0,0 +1,47 @@ +//! SOCKS5 proxy example +//! +//! This example demonstrates how the MITM proxy can work as a SOCKS5 server +//! in addition to HTTP proxy. The server automatically detects the protocol. +//! +//! To run: +//! ```bash +//! cargo run --example socks5_proxy +//! ``` +//! +//! Then configure your application to use SOCKS5 proxy at 127.0.0.1:1080 +//! The proxy will handle both SOCKS5 and HTTP proxy connections on the same port + +use slinger_mitm::{Interceptor, MitmConfig, MitmProxy}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== Slinger MITM Proxy (HTTP + SOCKS5) ===\n"); + + // Create proxy with default configuration + let config = MitmConfig::default(); + let proxy = MitmProxy::new(config).await?; + + // Add a logging interceptor to see traffic + let interceptor_handler = proxy.interceptor_handler(); + let mut handler = interceptor_handler.write().await; + handler.add_request_interceptor(Arc::new(Interceptor::logging())); + handler.add_response_interceptor(Arc::new(Interceptor::logging())); + drop(handler); // Release write lock + + // Start the proxy + println!("Starting MITM proxy on 127.0.0.1:1080"); + println!("CA certificate: {}\n", proxy.ca_cert_path().display()); + println!("This proxy supports:"); + println!(" - HTTP proxy protocol (CONNECT method)"); + println!(" - SOCKS5 protocol"); + println!(" - Automatic protocol detection\n"); + println!("To use this proxy:"); + println!("1. Configure your application to use SOCKS5 proxy: 127.0.0.1:1080"); + println!(" OR HTTP proxy: 127.0.0.1:1080"); + println!("2. For HTTPS interception, install the CA certificate\n"); + + proxy.start("127.0.0.1:1080").await?; + + Ok(()) +} diff --git a/slinger-mitm/examples/test_ca_caching.rs b/slinger-mitm/examples/test_ca_caching.rs new file mode 100644 index 0000000..01371d1 --- /dev/null +++ b/slinger-mitm/examples/test_ca_caching.rs @@ -0,0 +1,72 @@ +//! Test CA certificate caching functionality +//! +//! This example demonstrates the certificate caching mechanism. +//! It generates certificates for multiple domains and shows that +//! subsequent requests for the same domain use cached certificates. + +use slinger_mitm::CertificateManager; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let temp_dir = std::env::temp_dir().join("test_slinger_mitm_cache"); + std::fs::create_dir_all(&temp_dir)?; + + println!("=== Testing CA Certificate Caching ===\n"); + + println!("Creating certificate manager..."); + let cert_manager = CertificateManager::new(&temp_dir).await?; + println!( + "CA certificate path: {}\n", + cert_manager.ca_cert_path().display() + ); + + // First request - should generate new certificate + println!("1. Generating certificate for example.com..."); + let start = std::time::Instant::now(); + let (cert_chain1, _key1) = cert_manager.get_server_cert("example.com").await?; + let duration1 = start.elapsed(); + println!( + " Generated: {} certs in chain (took {:?})", + cert_chain1.len(), + duration1 + ); + + // Second request for same domain - should use cache + println!("\n2. Getting certificate for example.com again (should be cached)..."); + let start = std::time::Instant::now(); + let (cert_chain2, _key2) = cert_manager.get_server_cert("example.com").await?; + let duration2 = start.elapsed(); + println!( + " Retrieved: {} certs in chain (took {:?})", + cert_chain2.len(), + duration2 + ); + + if duration2 < duration1 / 2 { + println!(" ✓ Cache is working! Second request was much faster."); + } + + // Third request for different domain - should generate new certificate + println!("\n3. Generating certificate for google.com..."); + let start = std::time::Instant::now(); + let (cert_chain3, _key3) = cert_manager.get_server_cert("google.com").await?; + let duration3 = start.elapsed(); + println!( + " Generated: {} certs in chain (took {:?})", + cert_chain3.len(), + duration3 + ); + + // Fourth request for another different domain + println!("\n4. Generating certificate for github.com..."); + let (cert_chain4, _key4) = cert_manager.get_server_cert("github.com").await?; + println!(" Generated: {} certs in chain", cert_chain4.len()); + + println!("\n=== All tests passed! ==="); + println!("The certificate manager successfully:"); + println!(" - Generated unique certificates for different domains"); + println!(" - Cached certificates for improved performance"); + println!(" - Each certificate chain includes the server cert and CA cert"); + + Ok(()) +} diff --git a/slinger-mitm/src/ca.rs b/slinger-mitm/src/ca.rs new file mode 100644 index 0000000..79d6313 --- /dev/null +++ b/slinger-mitm/src/ca.rs @@ -0,0 +1,300 @@ +//! Certificate Authority (CA) management for MITM proxy +//! +//! This module handles automatic generation and management of CA certificates +//! for intercepting HTTPS traffic. +//! + +use crate::error::{Error, Result}; +use moka::future::Cache; +use rand::Rng; +use rcgen::{ + BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, Issuer, KeyPair, + KeyUsagePurpose, SanType, +}; +use std::net::IpAddr; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use time::{Duration, OffsetDateTime}; +use tokio::fs; +use tokio::io::AsyncWriteExt; +use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; + +/// Certificate validity period in seconds (1 year) +const TTL_SECS: i64 = 365 * 24 * 60 * 60; +/// Cache time-to-live in seconds (6 months) +const CACHE_TTL: u64 = (TTL_SECS / 2) as u64; +/// Offset for not_before timestamp to handle clock skew (60 seconds) +const NOT_BEFORE_OFFSET: i64 = 60; + +/// Certificate Authority for generating certificates +pub struct CertificateAuthority { + /// Root CA issuer + issuer: Issuer<'static, KeyPair>, + /// Root CA certificate in DER format + ca_cert_der: CertificateDer<'static>, + /// Root CA private key + #[allow(dead_code)] + ca_key_der: PrivateKeyDer<'static>, + /// Storage path for certificates + storage_path: PathBuf, +} + +impl CertificateAuthority { + /// Create a new Certificate Authority + /// + /// If a CA already exists at the storage path, it will be loaded. + /// Otherwise, a new CA will be generated. + pub async fn new(storage_path: impl AsRef) -> Result { + let storage_path = storage_path.as_ref().to_path_buf(); + + // Create storage directory if it doesn't exist + if !storage_path.exists() { + fs::create_dir_all(&storage_path).await?; + } + + let ca_cert_path = storage_path.join("ca_cert.pem"); + let ca_key_path = storage_path.join("ca_key.pem"); + + // Check if CA already exists + let (issuer, ca_cert_der, ca_key_der) = if ca_cert_path.exists() && ca_key_path.exists() { + Self::load_ca(&ca_cert_path, &ca_key_path).await? + } else { + Self::generate_ca(&ca_cert_path, &ca_key_path).await? + }; + + Ok(Self { + issuer, + ca_cert_der, + ca_key_der, + storage_path, + }) + } + + /// Load existing CA certificate and key + async fn load_ca( + cert_path: &Path, + key_path: &Path, + ) -> Result<( + Issuer<'static, KeyPair>, + CertificateDer<'static>, + PrivateKeyDer<'static>, + )> { + let cert_pem = fs::read_to_string(cert_path).await?; + let key_pem = fs::read_to_string(key_path).await?; + + let key_pair = KeyPair::from_pem(&key_pem) + .map_err(|e| Error::certificate_error(format!("Failed to parse CA key: {}", e)))?; + + let issuer = Issuer::from_ca_cert_pem(&cert_pem, key_pair).map_err(|e| { + Error::certificate_error(format!("Failed to create issuer from CA cert: {}", e)) + })?; + + // Parse PEM to DER for rustls + let cert_der = rustls_pemfile::certs(&mut cert_pem.as_bytes()) + .next() + .ok_or_else(|| Error::certificate_error("No certificate found in PEM"))? + .map_err(|e| Error::certificate_error(format!("Failed to parse PEM: {}", e)))?; + + let key_der = PrivateKeyDer::try_from(issuer.key().serialize_der()) + .map_err(|_| Error::certificate_error("Failed to serialize CA key"))?; + + Ok((issuer, cert_der, key_der)) + } + + /// Generate a new CA certificate and key + async fn generate_ca( + cert_path: &Path, + key_path: &Path, + ) -> Result<( + Issuer<'static, KeyPair>, + CertificateDer<'static>, + PrivateKeyDer<'static>, + )> { + let mut params = CertificateParams::default(); + + // Set up distinguished name + let mut dn = DistinguishedName::new(); + dn.push(DnType::CommonName, "Slinger MITM Proxy CA"); + dn.push(DnType::OrganizationName, "Emo-Crab"); + dn.push(DnType::CountryName, "CN"); + dn.push(DnType::LocalityName, "Internet"); + dn.push(DnType::StateOrProvinceName, "World"); + params.distinguished_name = dn; + // Configure as CA + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]; + + // Set validity period (10 years) + let now = OffsetDateTime::now_utc(); + params.not_before = now; + params.not_after = now + Duration::days(3650); + + let key_pair = KeyPair::generate() + .map_err(|e| Error::certificate_error(format!("Failed to generate key pair: {}", e)))?; + + let cert = params + .self_signed(&key_pair) + .map_err(|e| Error::certificate_error(format!("Failed to generate CA: {}", e)))?; + + // Serialize and save + let cert_pem = cert.pem(); + let key_pem = key_pair.serialize_pem(); + + let mut cert_file = fs::File::create(cert_path).await?; + cert_file.write_all(cert_pem.as_bytes()).await?; + + let mut key_file = fs::File::create(key_path).await?; + key_file.write_all(key_pem.as_bytes()).await?; + + let cert_der = CertificateDer::from(cert.der().to_vec()); + let key_der = PrivateKeyDer::try_from(key_pair.serialize_der()) + .map_err(|_| Error::certificate_error("Failed to serialize CA key DER"))?; + + // Create issuer from the certificate and key pair + let issuer = Issuer::from_ca_cert_pem(&cert_pem, key_pair) + .map_err(|e| Error::certificate_error(format!("Failed to create issuer: {}", e)))?; + + Ok((issuer, cert_der, key_der)) + } + + /// Generate a server certificate signed by this CA + pub fn generate_server_cert( + &self, + domain: &str, + ) -> Result<(Vec>, PrivateKeyDer<'static>)> { + let mut params = CertificateParams::default(); + + // Generate random serial number for uniqueness + params.serial_number = Some(rand::thread_rng().gen::().into()); + + // Set up distinguished name + let mut dn = DistinguishedName::new(); + dn.push(DnType::CommonName, domain); + params.distinguished_name = dn; + + // Add subject alternative names + // If domain parses as an IP literal, include both an IP SAN and a DNS SAN. + // Some clients strictly check iPAddress in SAN for IP targets while others + // may check dNSName; including both increases compatibility for local IPs. + params.subject_alt_names = if let Ok(ip) = domain.parse::() { + let mut sans = Vec::new(); + sans.push(SanType::IpAddress(ip)); + // Also add a DNS SAN with the textual IP as fallback (if rcgen accepts it). + if let Ok(dns_name) = domain.try_into() { + sans.push(SanType::DnsName(dns_name)); + } + sans + } else { + vec![SanType::DnsName(domain.try_into().map_err(|_| { + Error::certificate_error(format!("Invalid domain name: {}", domain)) + })?)] + }; + + // Set validity period with clock skew handling + let now = OffsetDateTime::now_utc(); + params.not_before = now - Duration::seconds(NOT_BEFORE_OFFSET); + params.not_after = now + Duration::seconds(TTL_SECS); + + let key_pair = KeyPair::generate() + .map_err(|e| Error::certificate_error(format!("Failed to generate key pair: {}", e)))?; + + let cert = params + .signed_by(&key_pair, &self.issuer) + .map_err(|e| Error::certificate_error(format!("Failed to sign server cert: {}", e)))?; + + let cert_der = CertificateDer::from(cert.der().to_vec()); + + let key_der = PrivateKeyDer::try_from(key_pair.serialize_der()) + .map_err(|_| Error::certificate_error("Failed to serialize server key"))?; + + // Return chain: [server_cert, ca_cert] + Ok((vec![cert_der, self.ca_cert_der.clone()], key_der)) + } + + /// Get CA certificate in PEM format for client installation + pub fn ca_cert_pem(&self) -> Result { + // Read the saved certificate file + let ca_cert_path = self.storage_path.join("ca_cert.pem"); + std::fs::read_to_string(&ca_cert_path) + .map_err(|e| Error::certificate_error(format!("Failed to read CA cert: {}", e))) + } + + /// Get CA certificate path + pub fn ca_cert_path(&self) -> PathBuf { + self.storage_path.join("ca_cert.pem") + } +} + +/// Manager for caching generated server certificates +pub struct CertificateManager { + ca: CertificateAuthority, + /// Cache for generated server certificates + cert_cache: Cache>, PrivateKeyDer<'static>)>>, +} + +impl CertificateManager { + /// Create a new certificate manager + pub async fn new(storage_path: impl AsRef) -> Result { + let ca = CertificateAuthority::new(storage_path).await?; + + // Create cache with TTL matching certificate validity + let cert_cache = Cache::builder() + .max_capacity(1000) + .time_to_live(std::time::Duration::from_secs(CACHE_TTL)) + .build(); + + Ok(Self { ca, cert_cache }) + } + + /// Get or generate a server certificate for the given domain + pub async fn get_server_cert( + &self, + domain: &str, + ) -> Result<(Vec>, PrivateKeyDer<'static>)> { + // If domain is an IP literal, avoid returning a potentially stale cached + // certificate that might lack an iPAddress SAN; always generate a fresh + // certificate containing the IP SAN. For hostnames, use the cache for + // performance. + if domain.parse::().is_ok() { + let (cert_chain, key) = self.ca.generate_server_cert(domain)?; + // Cache the generated cert for future use + let cached_cert = (cert_chain.clone(), key.clone_key()); + self + .cert_cache + .insert(domain.to_string(), Arc::new(cached_cert)) + .await; + return Ok((cert_chain, key)); + } + + // Try to get from cache for non-IP hostnames + if let Some(cached) = self.cert_cache.get(domain).await { + // Clone the certificate chain and key from cache + let (cert_chain, key) = cached.as_ref(); + return Ok((cert_chain.clone(), key.clone_key())); + } + + // Generate new certificate + let (cert_chain, key) = self.ca.generate_server_cert(domain)?; + + // Clone before caching since we need to return the original + let cached_cert = (cert_chain.clone(), key.clone_key()); + + // Store in cache + self + .cert_cache + .insert(domain.to_string(), Arc::new(cached_cert)) + .await; + + Ok((cert_chain, key)) + } + + /// Get the CA certificate in PEM format + pub fn ca_cert_pem(&self) -> Result { + self.ca.ca_cert_pem() + } + + /// Get the CA certificate path + pub fn ca_cert_path(&self) -> PathBuf { + self.ca.ca_cert_path() + } +} diff --git a/slinger-mitm/src/error.rs b/slinger-mitm/src/error.rs new file mode 100644 index 0000000..625dd8e --- /dev/null +++ b/slinger-mitm/src/error.rs @@ -0,0 +1,115 @@ +//! Error types for MITM proxy + +use std::io; +use thiserror::Error; + +/// Result type for MITM operations +pub type Result = std::result::Result; + +/// Error types for MITM proxy operations +#[derive(Error, Debug)] +pub enum Error { + /// IO error + #[error("IO error: {0}")] + Io(io::Error), + + /// Certificate error + #[error("Certificate error: {0}")] + CertificateError(String), + + /// TLS error + #[error("TLS error: {0}")] + TlsError(String), + + /// HTTP parsing error + #[error("HTTP error: {0}")] + HttpError(http::Error), + + /// Slinger client error + #[error("Slinger error: {0}")] + SlingerError(slinger::Error), + + /// Proxy error + #[error("Proxy error: {0}")] + ProxyError(String), + + /// Invalid request + #[error("Invalid request: {0}")] + InvalidRequest(String), + + /// Connection error + #[error("Connection error: {0}")] + ConnectionError(String), + + /// Other errors + #[error("{0}")] + Other(String), +} + +impl Error { + /// Create a certificate error and log it + pub fn certificate_error(msg: impl Into) -> Self { + let error = Error::CertificateError(msg.into()); + tracing::error!("Certificate error: {}", error); + error + } + + /// Create a TLS error and log it + pub fn tls_error(msg: impl Into) -> Self { + let error = Error::TlsError(msg.into()); + tracing::error!("TLS error: {}", error); + error + } + + /// Create a proxy error and log it + pub fn proxy_error(msg: impl Into) -> Self { + let error = Error::ProxyError(msg.into()); + tracing::error!("Proxy error: {}", error); + error + } + + /// Create an invalid request error and log it + pub fn invalid_request(msg: impl Into) -> Self { + let error = Error::InvalidRequest(msg.into()); + tracing::error!("Invalid request: {}", error); + error + } + + /// Create a connection error and log it + pub fn connection_error(msg: impl Into) -> Self { + let error = Error::ConnectionError(msg.into()); + tracing::error!("Connection error: {}", error); + error + } + + /// Create an other error and log it + pub fn other(msg: impl Into) -> Self { + let error = Error::Other(msg.into()); + tracing::error!("Other error: {}", error); + error + } +} + +impl From for Error { + fn from(value: io::Error) -> Self { + let error = Error::Io(value); + tracing::error!("IO error: {}", error); + error + } +} + +impl From for Error { + fn from(value: http::Error) -> Self { + let error = Error::HttpError(value); + tracing::error!("HTTP error: {}", error); + error + } +} + +impl From for Error { + fn from(value: slinger::Error) -> Self { + let error = Error::SlingerError(value); + tracing::error!("Slinger error: {}", error); + error + } +} diff --git a/slinger-mitm/src/interceptor.rs b/slinger-mitm/src/interceptor.rs new file mode 100644 index 0000000..c03a7a0 --- /dev/null +++ b/slinger-mitm/src/interceptor.rs @@ -0,0 +1,113 @@ +//! Traffic interception and modification interfaces + +use crate::error::Result; +use slinger::{Request, Response}; +use std::sync::Arc; + +/// Trait for intercepting and modifying HTTP requests +#[async_trait::async_trait] +pub trait RequestInterceptor: Send + Sync { + /// Intercept and optionally modify an HTTP request + /// + /// Return `None` to block the request, or return a modified request + async fn intercept_request(&self, request: Request) -> Result>; +} + +/// Trait for intercepting and modifying HTTP responses +#[async_trait::async_trait] +pub trait ResponseInterceptor: Send + Sync { + /// Intercept and optionally modify an HTTP response + /// + /// Return `None` to block the response, or return a modified response + async fn intercept_response(&self, response: Response) -> Result>; +} + +/// Combined interceptor handler +pub struct InterceptorHandler { + request_interceptors: Vec>, + response_interceptors: Vec>, +} + +impl InterceptorHandler { + /// Create a new interceptor handler + pub fn new() -> Self { + Self { + request_interceptors: Vec::new(), + response_interceptors: Vec::new(), + } + } + + /// Add a request interceptor + pub fn add_request_interceptor(&mut self, interceptor: Arc) { + self.request_interceptors.push(interceptor); + } + + /// Add a response interceptor + pub fn add_response_interceptor(&mut self, interceptor: Arc) { + self.response_interceptors.push(interceptor); + } + + /// Process a request through all interceptors + pub async fn process_request(&self, mut request: Request) -> Result> { + for interceptor in &self.request_interceptors { + match interceptor.intercept_request(request).await? { + Some(modified) => request = modified, + None => return Ok(None), // Request blocked + } + } + Ok(Some(request)) + } + + /// Process a response through all interceptors + pub async fn process_response(&self, mut response: Response) -> Result> { + for interceptor in &self.response_interceptors { + match interceptor.intercept_response(response).await? { + Some(modified) => response = modified, + None => return Ok(None), // Response blocked + } + } + Ok(Some(response)) + } +} + +impl Default for InterceptorHandler { + fn default() -> Self { + Self::new() + } +} + +/// Default pass-through interceptor +pub struct Interceptor; + +impl Interceptor { + /// Create a logging interceptor that prints requests/responses + pub fn logging() -> LoggingInterceptor { + LoggingInterceptor + } +} + +/// Logging interceptor implementation +pub struct LoggingInterceptor; + +#[async_trait::async_trait] +impl RequestInterceptor for LoggingInterceptor { + async fn intercept_request(&self, request: Request) -> Result> { + println!("[MITM] Request: {} {}", request.method(), request.uri()); + for (name, value) in request.headers() { + println!(" {}: {:?}", name, value); + } + Ok(Some(request)) + } +} + +#[async_trait::async_trait] +impl ResponseInterceptor for LoggingInterceptor { + async fn intercept_response(&self, response: Response) -> Result> { + println!("[MITM] Response: {}", response.status_code()); + for (name, value) in response.headers() { + println!(" {}: {:?}", name, value); + } + // println!("[MITM] Response Body: {:?}", response.body()); + Ok(Some(response)) + } +} diff --git a/slinger-mitm/src/lib.rs b/slinger-mitm/src/lib.rs new file mode 100644 index 0000000..c251aa6 --- /dev/null +++ b/slinger-mitm/src/lib.rs @@ -0,0 +1,48 @@ +//! MITM Proxy with Transparent Traffic Interception +//! +//! This crate provides a man-in-the-middle (MITM) proxy implementation similar to Burp Suite, +//! allowing transparent interception and modification of HTTP/HTTPS traffic. +//! +//! # Features +//! +//! - Automatic CA certificate generation +//! - Transparent HTTPS interception using rustls backend +//! - Traffic interception and modification interfaces +//! - Reuses slinger's Socket implementation +//! +//! # Example +//! +//! ```no_run +//! use slinger_mitm::{MitmProxy, MitmConfig}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let config = MitmConfig::default(); +//! let proxy = MitmProxy::new(config).await?; +//! proxy.start("127.0.0.1:8080").await?; +//! Ok(()) +//! } +//! ``` + +mod ca; +mod error; +mod interceptor; +mod proxy; +mod server; +mod socks5; + +pub use ca::{CertificateAuthority, CertificateManager}; +pub use error::{Error, Result}; +pub use interceptor::{Interceptor, InterceptorHandler, RequestInterceptor, ResponseInterceptor}; +pub use proxy::{MitmConfig, MitmProxy}; +pub use server::ProxyServer; +pub use socks5::{Socks5Server, TargetAddr}; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/slinger-mitm/src/proxy.rs b/slinger-mitm/src/proxy.rs new file mode 100644 index 0000000..31f7e87 --- /dev/null +++ b/slinger-mitm/src/proxy.rs @@ -0,0 +1,112 @@ +//! MITM Proxy configuration and main proxy implementation + +use crate::ca::CertificateManager; +use crate::error::Result; +use crate::interceptor::InterceptorHandler; +use crate::server::ProxyServer; +use slinger::Proxy; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// Configuration for MITM proxy +#[derive(Clone)] +pub struct MitmConfig { + /// Path to store CA certificates + pub ca_storage_path: PathBuf, + /// Enable HTTPS interception + pub enable_https_interception: bool, + /// Maximum concurrent connections + pub max_connections: usize, + /// Connection timeout in seconds + pub connection_timeout: u64, + /// Optional upstream proxy (supports HTTP, HTTPS, SOCKS5, SOCKS5h) + /// Example: "" or "" + pub upstream_proxy: Option, +} + +impl Default for MitmConfig { + fn default() -> Self { + Self { + ca_storage_path: PathBuf::from(".slinger-mitm"), + enable_https_interception: true, + max_connections: 1000, + connection_timeout: 30, + upstream_proxy: None, + } + } +} + +/// MITM Proxy main struct +pub struct MitmProxy { + config: MitmConfig, + cert_manager: Arc, + interceptor_handler: Arc>, +} + +impl MitmProxy { + /// Create a new MITM proxy with the given configuration + pub async fn new(config: MitmConfig) -> Result { + let cert_manager = Arc::new(CertificateManager::new(&config.ca_storage_path).await?); + let interceptor_handler = Arc::new(RwLock::new(InterceptorHandler::new())); + + Ok(Self { + config, + cert_manager, + interceptor_handler, + }) + } + + /// Create a new MITM proxy with default configuration + pub async fn default() -> Result { + Self::new(MitmConfig::default()).await + } + + /// Get the CA certificate in PEM format + /// + /// This certificate should be installed in the client's trust store + pub fn ca_cert_pem(&self) -> Result { + self.cert_manager.ca_cert_pem() + } + + /// Get the CA certificate path + pub fn ca_cert_path(&self) -> PathBuf { + self.cert_manager.ca_cert_path() + } + + /// Get a reference to the interceptor handler + pub fn interceptor_handler(&self) -> Arc> { + self.interceptor_handler.clone() + } + + /// Start the MITM proxy server on the given address + pub async fn start(&self, addr: &str) -> Result<()> { + let server = ProxyServer::new( + self.config.clone(), + self.cert_manager.clone(), + self.interceptor_handler.clone(), + )?; + server.run(addr).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_mitm_proxy_creation() { + let config = MitmConfig { + ca_storage_path: PathBuf::from("/tmp/test-mitm-ca"), + ..Default::default() + }; + + let proxy = MitmProxy::new(config).await; + assert!(proxy.is_ok()); + + if let Ok(p) = proxy { + let ca_pem = p.ca_cert_pem(); + assert!(ca_pem.is_ok()); + } + } +} diff --git a/slinger-mitm/src/server.rs b/slinger-mitm/src/server.rs new file mode 100644 index 0000000..f19cb80 --- /dev/null +++ b/slinger-mitm/src/server.rs @@ -0,0 +1,567 @@ +//! MITM Proxy server implementation + +use crate::ca::CertificateManager; +use crate::error::{Error, Result}; +use crate::interceptor::InterceptorHandler; +use crate::proxy::MitmConfig; +use bytes::Bytes; +use http::Version; +use slinger::{Client, Request, Response}; +use std::sync::Arc; +use std::time::Duration; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::RwLock; +use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use tokio_rustls::rustls::ServerConfig; +use tokio_rustls::TlsAcceptor; + +/// Proxy server implementation +pub struct ProxyServer { + config: MitmConfig, + cert_manager: Arc, + interceptor_handler: Arc>, + client: Client, +} + +impl ProxyServer { + /// Create a new proxy server + pub fn new( + config: MitmConfig, + cert_manager: Arc, + interceptor_handler: Arc>, + ) -> Result { + let client = if let Some(proxy) = &config.upstream_proxy { + // Enable HTTP keep-alive so the connector can reuse TCP connections + Client::builder() + .timeout(Some(Duration::from_secs(60))) + .keepalive(true) + .proxy(proxy.clone()) + .build() + .map_err(|e| { + Error::proxy_error(format!( + "Failed to build client with proxy {}: {}", + proxy.uri(), + e + )) + })? + } else { + // Use a client configured to reuse connections (keep-alive) + Client::builder() + .keepalive(true) + .build() + .map_err(|e| Error::proxy_error(format!("Failed to build default client: {}", e)))? + }; + Ok(Self { + config, + cert_manager, + interceptor_handler, + client, + }) + } + + /// Run the proxy server + pub async fn run(&self, addr: &str) -> Result<()> { + let listener = TcpListener::bind(addr) + .await + .map_err(|e| Error::proxy_error(format!("Failed to bind to {}: {}", addr, e)))?; + loop { + match listener.accept().await { + Ok((stream, _peer_addr)) => { + let config = self.config.clone(); + let cert_manager = self.cert_manager.clone(); + let interceptor = self.interceptor_handler.clone(); + let client = self.client.clone(); + + tokio::spawn(async move { + if let Err(e) = + Self::handle_connection(stream, config, cert_manager, interceptor, client).await + { + eprintln!("[MITM] Error handling connection: {}", e); + } + }); + } + Err(e) => { + eprintln!("[MITM] Failed to accept connection: {}", e); + } + } + } + } + + /// Handle a client connection + async fn handle_connection( + mut stream: TcpStream, + config: MitmConfig, + cert_manager: Arc, + interceptor: Arc>, + client: Client, + ) -> Result<()> { + use crate::socks5::Socks5Server; + + // Read the first byte to determine protocol + let mut first_byte = [0u8; 1]; + stream.read_exact(&mut first_byte).await?; + + // SOCKS5 version is 0x05, HTTP methods start with ASCII letters + if first_byte[0] == 0x05 { + // Handle as SOCKS5 - we already consumed the version byte + // Put it back by handling the rest of the handshake + match Socks5Server::handle_handshake_with_version(&mut stream).await { + Ok(target_addr) => { + let target_host_port = target_addr.to_host_port(); + match target_addr { + crate::socks5::TargetAddr::Domain(_domain, _port) => { + if config.enable_https_interception { + Self::handle_https_connect_socks5( + stream, + &target_host_port, + cert_manager, + interceptor, + client, + ) + .await + } else { + Self::handle_tcp_tunnel(stream, &target_host_port).await + } + } + _ => { + if config.enable_https_interception { + Self::handle_https_connect_socks5( + stream, + &target_host_port, + cert_manager, + interceptor, + client, + ) + .await + } else { + Self::handle_tcp_tunnel(stream, &target_host_port).await + } + } + } + } + Err(e) => { + Err(e) + } + } + } else { + let mut request_line = vec![first_byte[0]]; + let mut buffer = [0u8; 1]; + loop { + stream.read_exact(&mut buffer).await?; + request_line.push(buffer[0]); + if buffer[0] == b'\n' { + break; + } + if request_line.len() > 8192 { + return Err(Error::invalid_request("Request line too long".to_string())); + } + } + + let request_line_str = String::from_utf8_lossy(&request_line); + let parts: Vec<&str> = request_line_str.split_whitespace().collect(); + if parts.len() < 3 { + return Err(Error::invalid_request("Invalid request line".to_string())); + } + + let method = parts[0].to_string(); + let uri = parts[1].to_string(); + if method == "CONNECT" { + let mut reader = BufReader::new(stream); + const MAX_CONNECT_HEADERS: usize = 16 * 1024; // 16KB max for proxy headers + let mut headers_acc = 0usize; + loop { + let mut line = String::new(); + let n = reader.read_line(&mut line).await?; + // n==0 indicates EOF; bail out + if n == 0 { + break; + } + headers_acc += n; + if headers_acc > MAX_CONNECT_HEADERS { + return Err(Error::invalid_request( + "CONNECT headers size exceeds maximum allowed".to_string(), + )); + } + // End of headers is an empty line (CRLF) + if line == "\r\n" || line == "\n" || line.is_empty() { + break; + } + } + let stream = reader.into_inner(); + if config.enable_https_interception { + Self::handle_https_connect(stream, &uri, cert_manager, interceptor, client).await + } else { + Self::handle_https_tunnel(stream, &uri).await + } + } else { + let buf_reader = BufReader::new(stream); + Self::handle_http_request(&method, &uri, buf_reader, interceptor, client).await + } + } + } + + /// Handle HTTPS CONNECT with MITM interception + async fn handle_https_connect( + mut client_stream: TcpStream, + uri: &str, + cert_manager: Arc, + interceptor: Arc>, + slinger_client: Client, + ) -> Result<()> { + // Parse domain and port + let (domain, port) = Self::parse_host_port(uri)?; + // Send 200 Connection Established to client + client_stream + .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n") + .await?; + client_stream + .flush() // ← 关键:确保 HTTP 响应完全发送 + .await + .map_err(Error::Io)?; + // Generate server certificate for this domain + let (cert_chain, key) = cert_manager.get_server_cert(&domain).await?; + // Create TLS acceptor + let tls_config = Self::create_tls_server_config(cert_chain, key)?; + let acceptor = TlsAcceptor::from(Arc::new(tls_config)); + // Perform TLS handshake with client + let tls_stream = acceptor + .accept(client_stream) + .await + .map_err(|e| Error::tls_error(format!("TLS handshake failed: {}", e)))?; + let domain_with_port = format!("{}:{}", domain, port); + Self::handle_https_stream(tls_stream, domain_with_port, interceptor, slinger_client).await + } + + /// Handle HTTPS tunnel without interception (transparent proxy) + async fn handle_https_tunnel(mut client_stream: TcpStream, uri: &str) -> Result<()> { + let (host, port) = Self::parse_host_port(uri)?; + let target_addr = format!("{}:{}", host, port); + + // Connect to target server + let mut target_stream = TcpStream::connect(&target_addr).await.map_err(|e| { + Error::connection_error(format!("Failed to connect to {}: {}", target_addr, e)) + })?; + client_stream + .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n") + .await?; + let (mut client_read, mut client_write) = client_stream.split(); + let (mut target_read, mut target_write) = target_stream.split(); + let client_to_target = tokio::io::copy(&mut client_read, &mut target_write); + let target_to_client = tokio::io::copy(&mut target_read, &mut client_write); + tokio::select! { + _ = client_to_target => {}, + _ = target_to_client => {}, + } + Ok(()) + } + + /// Handle TCP tunnel without interception (for SOCKS5) + /// This function doesn't send any response - the SOCKS5 handshake already sent the reply + async fn handle_tcp_tunnel(mut client_stream: TcpStream, target_addr: &str) -> Result<()> { + // Parse host and port + let (host, port) = Self::parse_host_port(target_addr)?; + let addr = format!("{}:{}", host, port); + + // Connect to target server + let mut target_stream = TcpStream::connect(&addr) + .await + .map_err(|e| Error::connection_error(format!("Failed to connect to {}: {}", addr, e)))?; + + // Bidirectional copy + let (mut client_read, mut client_write) = client_stream.split(); + let (mut target_read, mut target_write) = target_stream.split(); + + let client_to_target = tokio::io::copy(&mut client_read, &mut target_write); + let target_to_client = tokio::io::copy(&mut target_read, &mut client_write); + + tokio::select! { + _ = client_to_target => {}, + _ = target_to_client => {}, + } + + Ok(()) + } + + /// Handle HTTPS CONNECT with MITM interception for SOCKS5 + /// This function doesn't send HTTP response - the SOCKS5 handshake already sent the reply + async fn handle_https_connect_socks5( + client_stream: TcpStream, + uri: &str, + cert_manager: Arc, + interceptor: Arc>, + slinger_client: Client, + ) -> Result<()> { + // Parse domain and port + let (domain, port) = Self::parse_host_port(uri)?; + + // Generate server certificate for this domain + let (cert_chain, key) = cert_manager.get_server_cert(&domain).await?; + + // Create TLS acceptor + let tls_config = Self::create_tls_server_config(cert_chain, key)?; + let acceptor = TlsAcceptor::from(Arc::new(tls_config)); + + // Perform TLS handshake with client + let tls_stream = acceptor + .accept(client_stream) + .await + .map_err(|e| Error::tls_error(format!("TLS handshake failed: {}", e)))?; + let domain_with_port = format!("{}:{}", domain, port); + Self::handle_https_stream(tls_stream, domain_with_port, interceptor, slinger_client).await + } + + /// Handle HTTPS requests over TLS connection + async fn handle_https_stream( + mut tls_stream: S, + domain: String, + interceptor: Arc>, + client: Client, + ) -> Result<()> + where + S: AsyncReadExt + AsyncWriteExt + Unpin, + { + // Read HTTP request from TLS stream with size limit + const MAX_REQUEST_SIZE: usize = 1024 * 1024; // 1MB limit + let mut buffer = Vec::new(); + let mut temp_buf = [0u8; 8192]; + + loop { + match tls_stream.read(&mut temp_buf).await { + Ok(0) => break, + Ok(n) => { + buffer.extend_from_slice(&temp_buf[..n]); + if buffer.len() > MAX_REQUEST_SIZE { + return Err(Error::invalid_request( + "Request size exceeds maximum allowed".to_string(), + )); + } + if buffer.windows(4).any(|w| w == b"\r\n\r\n") { + break; + } + } + Err(e) => return Err(Error::Io(e)), + } + } + + // Parse request + if let Ok(request) = Self::parse_http_request(&buffer, &domain) { + // Process through interceptors + let handler = interceptor.read().await; + if let Some(modified_req) = handler.process_request(request).await? { + let uri = modified_req.uri().clone(); + let method = modified_req.method().clone(); + let headers = modified_req.headers().clone(); + let body_data = if let Some(body) = modified_req.body() { + body.to_vec() + } else { + Vec::new() + }; + let mut req_builder = client.request(method, uri); + for (name, value) in headers.iter() { + req_builder = req_builder.header(name, value); + } + req_builder = req_builder.body(body_data); + match req_builder.send().await { + Ok(response) => { + if let Some(final_response) = handler.process_response(response).await? { + let response_bytes = Self::serialize_http_response(&final_response)?; + tls_stream.write_all(&response_bytes).await?; + } + } + Err(_e) => { + let error_response = b"HTTP/1.1 502 Bad Gateway\r\n\r\n"; + tls_stream.write_all(error_response).await?; + } + } + } + } + + Ok(()) + } + + /// Handle HTTP request (non-HTTPS) + async fn handle_http_request( + method: &str, + uri: &str, + mut reader: BufReader, + interceptor: Arc>, + client: Client, + ) -> Result<()> + where + R: AsyncReadExt + AsyncWriteExt + Unpin, + { + // Read headers with size limit + const MAX_HEADERS_SIZE: usize = 64 * 1024; // 64KB limit for headers + let mut headers_buf = Vec::new(); + loop { + let mut line = String::new(); + reader.read_line(&mut line).await?; + if line == "\r\n" || line == "\n" { + break; + } + headers_buf.extend_from_slice(line.as_bytes()); + + // Check headers size limit + if headers_buf.len() > MAX_HEADERS_SIZE { + return Err(Error::invalid_request( + "Headers size exceeds maximum allowed".to_string(), + )); + } + } + + // Build request using http::Request::builder, then convert to slinger::Request + let mut request_builder = http::Request::builder() + .method(method) + .uri(uri) + .version(Version::HTTP_11); + + // Parse headers + for line in String::from_utf8_lossy(&headers_buf).lines() { + if let Some(idx) = line.find(':') { + let (name, value) = line.split_at(idx); + let value = value[1..].trim(); + request_builder = request_builder.header(name.trim(), value); + } + } + + let http_request = request_builder.body(Bytes::new())?; + let request: Request = http_request.into(); + + // Process through interceptors + let handler = interceptor.read().await; + if let Some(modified_req) = handler.process_request(request).await? { + // Forward using slinger + let uri = modified_req.uri().clone(); + let method = modified_req.method().clone(); + let headers = modified_req.headers().clone(); + let body_data = if let Some(body) = modified_req.body() { + body.to_vec() + } else { + Vec::new() + }; + + let mut req_builder = client.request(method, uri); + for (name, value) in headers.iter() { + req_builder = req_builder.header(name, value); + } + req_builder = req_builder.body(body_data); + + match req_builder.send().await { + Ok(response) => { + if let Some(final_response) = handler.process_response(response).await? { + let response_bytes = Self::serialize_http_response(&final_response)?; + let mut stream = reader.into_inner(); + stream.write_all(&response_bytes).await?; + } + } + Err(_e) => { + let mut stream = reader.into_inner(); + stream + .write_all(b"HTTP/1.1 502 Bad Gateway\r\n\r\n") + .await?; + } + } + } + + Ok(()) + } + + /// Create TLS server configuration + fn create_tls_server_config( + cert_chain: Vec>, + key: PrivateKeyDer<'static>, + ) -> Result { + let config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain, key) + .map_err(|e| Error::tls_error(format!("Failed to create TLS config: {}", e)))?; + + Ok(config) + } + + /// Parse host and port from URI + fn parse_host_port(uri: &str) -> Result<(String, u16)> { + let parts: Vec<&str> = uri.split(':').collect(); + if parts.len() != 2 { + return Err(Error::invalid_request(format!("Invalid URI: {}", uri))); + } + + let host = parts[0].to_string(); + let port = parts[1] + .parse::() + .map_err(|_| Error::invalid_request(format!("Invalid port: {}", parts[1])))?; + + Ok((host, port)) + } + + /// Parse HTTP request from bytes + fn parse_http_request(buffer: &[u8], domain: &str) -> Result { + let request_str = String::from_utf8_lossy(buffer); + let mut lines = request_str.lines(); + + let request_line = lines + .next() + .ok_or_else(|| Error::invalid_request("Empty request".to_string()))?; + let parts: Vec<&str> = request_line.split_whitespace().collect(); + if parts.len() < 3 { + return Err(Error::invalid_request("Invalid request line".to_string())); + } + + let method = parts[0]; + let path = parts[1]; + let uri = if path.starts_with("http://") || path.starts_with("https://") { + path.to_string() + } else { + format!("https://{}{}", domain, path) + }; + + let mut request_builder = http::Request::builder() + .method(method) + .uri(uri) + .version(Version::HTTP_11); + + for line in lines { + if line.is_empty() { + break; + } + if let Some(idx) = line.find(':') { + let (name, value) = line.split_at(idx); + let value = value[1..].trim(); + request_builder = request_builder.header(name.trim(), value); + } + } + + let http_request = request_builder.body(Bytes::new())?; + Ok(http_request.into()) + } + + /// Serialize HTTP response to bytes + fn serialize_http_response(response: &Response) -> Result> { + let mut buf = Vec::new(); + + // Status line + let status = response.status_code(); + let status_line = format!( + "HTTP/1.1 {} {}\r\n", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown") + ); + buf.extend_from_slice(status_line.as_bytes()); + + // Headers + for (name, value) in response.headers() { + buf.extend_from_slice(name.as_str().as_bytes()); + buf.extend_from_slice(b": "); + buf.extend_from_slice(value.as_bytes()); + buf.extend_from_slice(b"\r\n"); + } + // Empty line before body + buf.extend_from_slice(b"\r\n"); + // Body + if let Some(body) = response.body() { + buf.extend_from_slice(body.as_ref()); + } + Ok(buf) + } +} diff --git a/slinger-mitm/src/socks5.rs b/slinger-mitm/src/socks5.rs new file mode 100644 index 0000000..8010e99 --- /dev/null +++ b/slinger-mitm/src/socks5.rs @@ -0,0 +1,240 @@ +//! SOCKS5 server implementation for MITM proxy + +use crate::error::{Error, Result}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +const SOCKS5_VERSION: u8 = 0x05; +const SOCKS5_NO_AUTHENTICATION: u8 = 0x00; +const SOCKS5_NO_ACCEPTABLE_METHODS: u8 = 0xFF; + +const SOCKS5_CMD_CONNECT: u8 = 0x01; + +const SOCKS5_ATYP_IPV4: u8 = 0x01; +const SOCKS5_ATYP_DOMAIN: u8 = 0x03; +const SOCKS5_ATYP_IPV6: u8 = 0x04; + +const SOCKS5_REP_SUCCESS: u8 = 0x00; +const SOCKS5_REP_COMMAND_NOT_SUPPORTED: u8 = 0x07; +const SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08; + +/// SOCKS5 target address +#[derive(Debug, Clone)] +pub enum TargetAddr { + /// IPv4 address + Ipv4([u8; 4], u16), + /// IPv6 address + Ipv6([u8; 16], u16), + /// Domain name + Domain(String, u16), +} + +impl TargetAddr { + /// Get host and port as string + pub fn to_host_port(&self) -> String { + match self { + TargetAddr::Ipv4(ip, port) => { + format!("{}.{}.{}.{}:{}", ip[0], ip[1], ip[2], ip[3], port) + } + TargetAddr::Ipv6(ip, port) => { + // Group bytes into 16-bit segments + let segments: Vec = (0..8) + .map(|i| u16::from_be_bytes([ip[i * 2], ip[i * 2 + 1]])) + .collect(); + format!( + "[{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}]:{}", + segments[0], + segments[1], + segments[2], + segments[3], + segments[4], + segments[5], + segments[6], + segments[7], + port + ) + } + TargetAddr::Domain(domain, port) => format!("{}:{}", domain, port), + } + } + + /// Get host (without port) + pub fn host(&self) -> String { + match self { + TargetAddr::Ipv4(ip, _) => format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]), + TargetAddr::Ipv6(ip, _) => { + // Group bytes into 16-bit segments + let segments: Vec = (0..8) + .map(|i| u16::from_be_bytes([ip[i * 2], ip[i * 2 + 1]])) + .collect(); + format!( + "{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}", + segments[0], + segments[1], + segments[2], + segments[3], + segments[4], + segments[5], + segments[6], + segments[7] + ) + } + TargetAddr::Domain(domain, _) => domain.clone(), + } + } + + /// Get port + pub fn port(&self) -> u16 { + match self { + TargetAddr::Ipv4(_, port) | TargetAddr::Ipv6(_, port) | TargetAddr::Domain(_, port) => *port, + } + } +} + +/// SOCKS5 server for MITM proxy +pub struct Socks5Server; + +impl Socks5Server { + /// Handle SOCKS5 client connection + /// Returns the target address if successful + pub async fn handle_handshake(stream: &mut TcpStream) -> Result { + // Read version and methods + let mut buf = [0u8; 2]; + stream.read_exact(&mut buf).await?; + + let version = buf[0]; + let nmethods = buf[1]; + + if version != SOCKS5_VERSION { + return Err(Error::proxy_error(format!( + "Unsupported SOCKS version: {}", + version + ))); + } + + Self::handle_handshake_internal(stream, nmethods).await + } + + /// Handle SOCKS5 client connection when version byte has already been read + /// Returns the target address if successful + pub async fn handle_handshake_with_version(stream: &mut TcpStream) -> Result { + // Version byte (0x05) was already read, now read nmethods + let mut buf = [0u8; 1]; + stream.read_exact(&mut buf).await?; + let nmethods = buf[0]; + + Self::handle_handshake_internal(stream, nmethods).await + } + + /// Internal handshake processing after version check + async fn handle_handshake_internal(stream: &mut TcpStream, nmethods: u8) -> Result { + // Read methods + let mut methods = vec![0u8; nmethods as usize]; + stream.read_exact(&mut methods).await?; + + // For now, we only support no authentication + // Check if client supports no auth + let selected_method = if methods.contains(&SOCKS5_NO_AUTHENTICATION) { + SOCKS5_NO_AUTHENTICATION + } else { + SOCKS5_NO_ACCEPTABLE_METHODS + }; + + // Send method selection response + let response = [SOCKS5_VERSION, selected_method]; + stream.write_all(&response).await?; + + if selected_method == SOCKS5_NO_ACCEPTABLE_METHODS { + return Err(Error::proxy_error( + "No acceptable authentication method".to_string(), + )); + } + + // Read connection request + let mut buf = [0u8; 4]; + stream.read_exact(&mut buf).await?; + + let version = buf[0]; + let cmd = buf[1]; + // buf[2] is reserved + let atyp = buf[3]; + + if version != SOCKS5_VERSION { + return Err(Error::proxy_error(format!( + "Invalid SOCKS version in request: {}", + version + ))); + } + + // Only support CONNECT command + if cmd != SOCKS5_CMD_CONNECT { + Self::send_reply(stream, SOCKS5_REP_COMMAND_NOT_SUPPORTED).await?; + return Err(Error::proxy_error(format!("Unsupported command: {}", cmd))); + } + + // Read target address + let target_addr = match atyp { + SOCKS5_ATYP_IPV4 => { + let mut addr = [0u8; 4]; + stream.read_exact(&mut addr).await?; + let mut port_buf = [0u8; 2]; + stream.read_exact(&mut port_buf).await?; + let port = u16::from_be_bytes(port_buf); + TargetAddr::Ipv4(addr, port) + } + SOCKS5_ATYP_IPV6 => { + let mut addr = [0u8; 16]; + stream.read_exact(&mut addr).await?; + let mut port_buf = [0u8; 2]; + stream.read_exact(&mut port_buf).await?; + let port = u16::from_be_bytes(port_buf); + TargetAddr::Ipv6(addr, port) + } + SOCKS5_ATYP_DOMAIN => { + let mut len_buf = [0u8; 1]; + stream.read_exact(&mut len_buf).await?; + let len = len_buf[0] as usize; + let mut domain = vec![0u8; len]; + stream.read_exact(&mut domain).await?; + let mut port_buf = [0u8; 2]; + stream.read_exact(&mut port_buf).await?; + let port = u16::from_be_bytes(port_buf); + let domain_str = String::from_utf8(domain) + .map_err(|_| Error::proxy_error("Invalid domain name".to_string()))?; + TargetAddr::Domain(domain_str, port) + } + _ => { + Self::send_reply(stream, SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED).await?; + return Err(Error::proxy_error(format!( + "Unsupported address type: {}", + atyp + ))); + } + }; + + // Send success reply + Self::send_reply(stream, SOCKS5_REP_SUCCESS).await?; + + Ok(target_addr) + } + + /// Send SOCKS5 reply + async fn send_reply(stream: &mut TcpStream, reply_code: u8) -> Result<()> { + // Reply format: [VER, REP, RSV, ATYP, BND.ADDR, BND.PORT] + // We'll use a simple IPv4 address 0.0.0.0:0 as the bound address + let response = [ + SOCKS5_VERSION, + reply_code, + 0x00, // Reserved + SOCKS5_ATYP_IPV4, + 0x00, + 0x00, + 0x00, + 0x00, // Bind address (0.0.0.0) + 0x00, + 0x00, // Bind port (0) + ]; + stream.write_all(&response).await?; + Ok(()) + } +} diff --git a/slinger-mitm/tests/integration_tests.rs b/slinger-mitm/tests/integration_tests.rs new file mode 100644 index 0000000..a766707 --- /dev/null +++ b/slinger-mitm/tests/integration_tests.rs @@ -0,0 +1,314 @@ +//! Integration tests for slinger-mitm + +use slinger_mitm::{CertificateManager, MitmConfig, MitmProxy}; + +#[tokio::test] +async fn test_ca_generation() { + let temp_dir = std::env::temp_dir().join("slinger-mitm-test-ca"); + + // Clean up if exists + if temp_dir.exists() { + std::fs::remove_dir_all(&temp_dir).ok(); + } + + let manager = CertificateManager::new(&temp_dir).await; + assert!(manager.is_ok(), "Failed to create certificate manager"); + + let manager = manager.unwrap(); + + // Verify CA cert PEM can be retrieved + let ca_pem = manager.ca_cert_pem(); + assert!(ca_pem.is_ok(), "Failed to get CA certificate PEM"); + + let pem_content = ca_pem.unwrap(); + assert!( + pem_content.contains("BEGIN CERTIFICATE"), + "Invalid PEM format" + ); + assert!( + pem_content.contains("END CERTIFICATE"), + "Invalid PEM format" + ); + + // Verify CA cert file exists + let ca_path = manager.ca_cert_path(); + assert!(ca_path.exists(), "CA certificate file not created"); + + // Clean up + std::fs::remove_dir_all(&temp_dir).ok(); +} + +#[tokio::test] +async fn test_server_cert_generation() { + let temp_dir = std::env::temp_dir().join("slinger-mitm-test-server"); + + // Clean up if exists + if temp_dir.exists() { + std::fs::remove_dir_all(&temp_dir).ok(); + } + + let manager = CertificateManager::new(&temp_dir).await; + assert!(manager.is_ok(), "Failed to create certificate manager"); + + let manager = manager.unwrap(); + + // Generate server certificate + let result = manager.get_server_cert("example.com").await; + assert!(result.is_ok(), "Failed to generate server certificate"); + + let (cert_chain, _key) = result.unwrap(); + assert!(!cert_chain.is_empty(), "Certificate chain is empty"); + assert_eq!( + cert_chain.len(), + 2, + "Expected 2 certificates in chain (server + CA)" + ); + + // Clean up + std::fs::remove_dir_all(&temp_dir).ok(); +} + +#[tokio::test] +async fn test_server_cert_caching_and_tls_config() { + use tokio_rustls::rustls::ServerConfig; + + let temp_dir = std::env::temp_dir().join("slinger-mitm-test-caching"); + + // Clean up if exists + if temp_dir.exists() { + std::fs::remove_dir_all(&temp_dir).ok(); + } + + let manager = CertificateManager::new(&temp_dir).await; + assert!(manager.is_ok(), "Failed to create certificate manager"); + + let manager = manager.unwrap(); + + // First request - generates and caches certificate + let result1 = manager.get_server_cert("test.example.com").await; + assert!( + result1.is_ok(), + "Failed to generate server certificate (first request)" + ); + + let (cert_chain1, key1) = result1.unwrap(); + + // Verify TLS config can be created from first request + let config1 = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain1.clone(), key1); + if let Err(e) = &config1 { + eprintln!("Error creating TLS config from first request: {:?}", e); + } + assert!( + config1.is_ok(), + "Failed to create TLS config from first request: {:?}", + config1.err() + ); + + // Second request - should retrieve from cache + let result2 = manager.get_server_cert("test.example.com").await; + assert!( + result2.is_ok(), + "Failed to get cached server certificate (second request)" + ); + + let (cert_chain2, key2) = result2.unwrap(); + + // Verify TLS config can be created from cached certificate + // This is where the KeyMismatch error would occur if caching is broken + let config2 = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain2.clone(), key2); + assert!( + config2.is_ok(), + "Failed to create TLS config from cached certificate - KeyMismatch error!" + ); + + // Third request - verify cache still works + let result3 = manager.get_server_cert("test.example.com").await; + assert!( + result3.is_ok(), + "Failed to get cached server certificate (third request)" + ); + + let (cert_chain3, key3) = result3.unwrap(); + + // Verify TLS config works on third request too + let config3 = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain3, key3); + assert!( + config3.is_ok(), + "Failed to create TLS config from cached certificate (third request)" + ); + + // Clean up + std::fs::remove_dir_all(&temp_dir).ok(); +} + +#[tokio::test] +async fn test_mitm_proxy_creation() { + let config = MitmConfig { + ca_storage_path: std::env::temp_dir().join("slinger-mitm-test-proxy"), + enable_https_interception: true, + max_connections: 100, + connection_timeout: 10, + upstream_proxy: None, + }; + + // Clean up if exists + if config.ca_storage_path.exists() { + std::fs::remove_dir_all(&config.ca_storage_path).ok(); + } + + let proxy = MitmProxy::new(config.clone()).await; + assert!(proxy.is_ok(), "Failed to create MITM proxy"); + + let proxy = proxy.unwrap(); + + // Verify CA certificate is accessible + let ca_pem = proxy.ca_cert_pem(); + assert!(ca_pem.is_ok(), "Failed to get CA certificate from proxy"); + + // Verify CA path + let ca_path = proxy.ca_cert_path(); + assert!(ca_path.exists(), "CA certificate file not found"); + + // Clean up + std::fs::remove_dir_all(&config.ca_storage_path).ok(); +} + +#[tokio::test] +async fn test_interceptor_handler() { + use slinger_mitm::{Interceptor, InterceptorHandler}; + use std::sync::Arc; + + let mut handler = InterceptorHandler::new(); + + // Add interceptors + handler.add_request_interceptor(Arc::new(Interceptor::logging())); + handler.add_response_interceptor(Arc::new(Interceptor::logging())); + + // Test with a simple request + use bytes::Bytes; + + let http_request = http::Request::builder() + .method("GET") + .uri("http://example.com") + .body(Bytes::new()) + .unwrap(); + + let request: slinger::Request = http_request.into(); + + let result = handler.process_request(request).await; + assert!(result.is_ok(), "Failed to process request through handler"); + assert!( + result.unwrap().is_some(), + "Request was blocked unexpectedly" + ); +} + +#[tokio::test] +async fn test_mitm_proxy_with_upstream_proxy() { + use slinger_mitm::MitmConfig; + + // Test proxy URL parsing + let proxy_result = slinger::Proxy::parse("socks5h://127.0.0.1:1080"); + assert!(proxy_result.is_ok(), "Failed to parse socks5h proxy URL"); + + let config = MitmConfig { + ca_storage_path: std::env::temp_dir().join("slinger-mitm-test-with-proxy"), + enable_https_interception: true, + max_connections: 100, + connection_timeout: 10, + upstream_proxy: Some(proxy_result.unwrap()), + }; + + // Clean up if exists + if config.ca_storage_path.exists() { + std::fs::remove_dir_all(&config.ca_storage_path).ok(); + } + + let proxy = MitmProxy::new(config.clone()).await; + assert!( + proxy.is_ok(), + "Failed to create MITM proxy with upstream proxy" + ); + + // Clean up + std::fs::remove_dir_all(&config.ca_storage_path).ok(); +} + +#[tokio::test] +async fn test_various_proxy_types() { + // Test different proxy types + let test_cases = [ + ("socks5://127.0.0.1:1080", "socks5"), + ("socks5h://127.0.0.1:1080", "socks5h"), + ("http://127.0.0.1:8080", "http"), + ("https://127.0.0.1:8443", "https"), + ]; + + for (idx, (proxy_url, proxy_type)) in test_cases.iter().enumerate() { + let proxy_result = slinger::Proxy::parse(*proxy_url); + assert!( + proxy_result.is_ok(), + "Failed to parse proxy URL: {}", + proxy_url + ); + + let config = MitmConfig { + ca_storage_path: std::env::temp_dir() + .join(format!("slinger-mitm-test-{}-{}", proxy_type, idx)), + upstream_proxy: Some(proxy_result.unwrap()), + ..Default::default() + }; + + // Clean up if exists + if config.ca_storage_path.exists() { + std::fs::remove_dir_all(&config.ca_storage_path).ok(); + } + + let proxy = MitmProxy::new(config.clone()).await; + assert!( + proxy.is_ok(), + "Failed to create MITM proxy with {} proxy", + proxy_url + ); + + // Clean up + std::fs::remove_dir_all(&config.ca_storage_path).ok(); + } +} + +#[tokio::test] +async fn test_socks5_target_addr() { + use slinger_mitm::TargetAddr; + + // Test IPv4 address + let ipv4 = TargetAddr::Ipv4([192, 168, 1, 1], 8080); + assert_eq!(ipv4.to_host_port(), "192.168.1.1:8080"); + assert_eq!(ipv4.host(), "192.168.1.1"); + assert_eq!(ipv4.port(), 8080); + + // Test IPv6 address (::1 = loopback) + let ipv6 = TargetAddr::Ipv6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 443); + assert_eq!(ipv6.to_host_port(), "[0:0:0:0:0:0:0:1]:443"); + assert_eq!(ipv6.host(), "0:0:0:0:0:0:0:1"); + assert_eq!(ipv6.port(), 443); + + // Test IPv6 with different values (2001:db8::1) + let ipv6_2 = TargetAddr::Ipv6( + [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + 80, + ); + assert_eq!(ipv6_2.to_host_port(), "[2001:db8:0:0:0:0:0:1]:80"); + assert_eq!(ipv6_2.host(), "2001:db8:0:0:0:0:0:1"); + + // Test domain address + let domain = TargetAddr::Domain("example.com".to_string(), 443); + assert_eq!(domain.to_host_port(), "example.com:443"); + assert_eq!(domain.host(), "example.com"); + assert_eq!(domain.port(), 443); +} diff --git a/slinger/Cargo.toml b/slinger/Cargo.toml new file mode 100644 index 0000000..69ac328 --- /dev/null +++ b/slinger/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "slinger" +version = "0.2.10" +edition.workspace = true +description = "An HTTP Client for Rust designed for hackers." +homepage.workspace = true +repository.workspace = true +authors.workspace = true +readme = "../README.md" +license.workspace = true + + +[dependencies] +thiserror.workspace = true +http.workspace = true +socket2.workspace = true +bytes.workspace = true +tracing.workspace = true +base64 = "0.22" +percent-encoding = "2.1" +flate2 = { version = "1.0", default-features = false, features = ["default"], optional = true } +serde = { version = "1", features = ["derive"], optional = true } +http-serde = { version = "2.1.0", optional = true } +encoding_rs = { version = "0.8", optional = true } +mime = { version = "0.3.17", optional = true } +cookie = { version = "0.18.1", optional = true } +tokio-rustls = { workspace = true, optional = true } +tokio = { workspace = true, features = ["net", "io-util", "time", "rt"] } +rustls-pemfile = { workspace = true, optional = true } +rustls-pki-types = { workspace = true, optional = true } +rustls-native-certs = { workspace = true, optional = true } +h2 = { version = "0.4", optional = true } +schemars = { version = "1.1.0", optional = true, features = ["bytes1"] } +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +futures = { version = "0.3.31" } +serde_json = "1" +tokio-native-tls = "0.3" +[features] +default = [] +# TLS feature hierarchy: +# - `tls`: Base TLS feature flag (enables TLS types and interfaces, but no backend) +# - `rustls`: Rustls TLS backend (requires `tls`) +# Note: If `tls` is enabled without a backend, you must provide a custom TLS connector callback. +tls = [] +rustls = ["tls", "tokio-rustls", "rustls-pemfile", "rustls-pki-types", "rustls-native-certs"] +http2 = ["dep:h2", "tls"] +cookie = ["dep:cookie"] +charset = ["dep:encoding_rs", "dep:mime"] +serde = ["dep:serde", "http-serde"] +gzip = ["flate2"] +schema = ["dep:schemars", "serde"] \ No newline at end of file diff --git a/examples/async.rs b/slinger/examples/async.rs similarity index 100% rename from examples/async.rs rename to slinger/examples/async.rs diff --git a/examples/base.rs b/slinger/examples/base.rs similarity index 100% rename from examples/base.rs rename to slinger/examples/base.rs diff --git a/examples/certificate.rs b/slinger/examples/certificate.rs similarity index 81% rename from examples/certificate.rs rename to slinger/examples/certificate.rs index 07359c7..e5b49b6 100644 --- a/examples/certificate.rs +++ b/slinger/examples/certificate.rs @@ -6,7 +6,7 @@ async fn main() -> Result<(), Box> { use slinger::ClientBuilder; let client = ClientBuilder::default().build().unwrap(); let resp = client.get("https://httpbin.org/get").send().await?; - let certificate = resp.extensions().get::().unwrap(); + let certificate = resp.extensions().get::>().unwrap(); println!("{:?}", certificate); } Ok(()) diff --git a/examples/cookies.rs b/slinger/examples/cookies.rs similarity index 100% rename from examples/cookies.rs rename to slinger/examples/cookies.rs diff --git a/examples/custom_connector_builder.rs b/slinger/examples/custom_connector_builder.rs similarity index 100% rename from examples/custom_connector_builder.rs rename to slinger/examples/custom_connector_builder.rs diff --git a/examples/custom_tls.rs b/slinger/examples/custom_tls.rs similarity index 66% rename from examples/custom_tls.rs rename to slinger/examples/custom_tls.rs index b31f070..6b37b2a 100644 --- a/examples/custom_tls.rs +++ b/slinger/examples/custom_tls.rs @@ -1,89 +1,62 @@ //! Example demonstrating how to use a custom TLS connector //! //! This example shows how to implement your own TLS handshake logic -//! when the `tls` feature is enabled without a specific backend. +//! when the `tls` feature is enabled. //! //! To run this example, compile with: //! ```bash //! cargo run --example custom_tls --features tls //! ``` //! -//! Note: This example requires the `tls` feature without `rustls`. -//! It demonstrates the API with a mock TLS implementation for educational purposes. - -#[cfg(all(feature = "tls", not(feature = "rustls")))] -use slinger::{ - ConnectorBuilder, CustomTlsConnector, CustomTlsStream, MaybeTlsStream, PeerCertificate, Result, - Socket, -}; -#[cfg(all(feature = "tls", not(feature = "rustls")))] +//! Note: This example demonstrates the API with a mock TLS implementation for educational purposes. + +use slinger::tls::CustomTlsStream; +#[cfg(feature = "tls")] +use slinger::tls::{CustomTlsConnector, PeerCertificate}; +#[cfg(feature = "tls")] +use slinger::{ConnectorBuilder, Result, Socket, StreamWrapper}; use std::pin::Pin; -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] use std::sync::Arc; -#[cfg(all(feature = "tls", not(feature = "rustls")))] use std::task::{Context, Poll}; -#[cfg(all(feature = "tls", not(feature = "rustls")))] use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -#[cfg(all(feature = "tls", not(feature = "rustls")))] -use tokio::net::TcpStream; -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] /// Mock TLS stream for demonstration purposes /// /// In a real implementation, this would wrap an actual TLS stream from /// a library like OpenSSL, BoringSSL, or another TLS implementation. -#[derive(Debug)] struct MockTlsStream { - inner: TcpStream, - // In a real implementation, you would store TLS session state here - mock_certificate: Vec, + inner: tokio::net::TcpStream, } - -#[cfg(all(feature = "tls", not(feature = "rustls")))] -impl MockTlsStream { - fn new(stream: TcpStream) -> Self { - // Create a mock certificate for demonstration - let mock_certificate = b"This is a mock certificate".to_vec(); - Self { - inner: stream, - mock_certificate, - } +#[cfg(feature = "tls")] +impl CustomTlsStream for MockTlsStream { + fn peer_certificate(&self) -> Option> { + None } -} -#[cfg(all(feature = "tls", not(feature = "rustls")))] -impl CustomTlsStream for MockTlsStream { - fn peer_certificate(&self) -> Option { - // Return the mock certificate - // In a real implementation, this would extract the actual peer certificate - Some(PeerCertificate { - inner: self.mock_certificate.clone(), - }) + fn alpn_protocol(&self) -> Option> { + None } } - -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] impl AsyncRead for MockTlsStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { - // In a real implementation, this would read encrypted data, - // decrypt it, and place it in the buffer Pin::new(&mut self.inner).poll_read(cx, buf) } } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] impl AsyncWrite for MockTlsStream { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { - // In a real implementation, this would encrypt the data - // before writing it to the underlying stream Pin::new(&mut self.inner).poll_write(cx, buf) } @@ -102,7 +75,7 @@ impl AsyncWrite for MockTlsStream { } } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] /// Example custom TLS connector implementation /// /// In a real implementation, you would: @@ -111,8 +84,7 @@ impl AsyncWrite for MockTlsStream { /// 3. Wrap the resulting TLS stream in a type that implements CustomTlsStream /// 4. Return it wrapped in a Socket struct MyCustomTlsConnector; - -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] impl CustomTlsConnector for MyCustomTlsConnector { fn connect<'a>( &'a self, @@ -124,7 +96,7 @@ impl CustomTlsConnector for MyCustomTlsConnector { // Extract the TCP stream from the socket let tcp_stream = match stream.inner { - MaybeTlsStream::Tcp(tcp) => tcp, + StreamWrapper::Tcp(tcp) => tcp, _ => { return Err(slinger::Error::Other( "Expected plain TCP stream for TLS upgrade".to_string(), @@ -138,13 +110,14 @@ impl CustomTlsConnector for MyCustomTlsConnector { // 3. Establish encrypted communication // Create our mock TLS stream - let tls_stream = MockTlsStream::new(tcp_stream); + let mock = MockTlsStream { inner: tcp_stream }; + // let tls_stream = TlsStreamWrapper::new(mock); println!("✓ Mock TLS handshake completed successfully"); // Wrap in a Socket and return Ok(Socket::new( - MaybeTlsStream::Custom(Box::new(tls_stream)), + StreamWrapper::Custom(Box::new(mock)), stream.read_timeout, stream.write_timeout, )) @@ -152,7 +125,7 @@ impl CustomTlsConnector for MyCustomTlsConnector { } } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] #[tokio::main] async fn main() -> std::result::Result<(), Box> { println!("Custom TLS Connector Example"); @@ -191,8 +164,8 @@ async fn main() -> std::result::Result<(), Box> { Ok(()) } -#[cfg(not(all(feature = "tls", not(feature = "rustls"))))] +#[cfg(not(feature = "tls"))] fn main() { - eprintln!("This example requires the 'tls' feature without 'rustls'."); + eprintln!("This example requires the 'tls' feature."); eprintln!("Run with: cargo run --example custom_tls --features tls"); } diff --git a/examples/cve-2019-5418.rs b/slinger/examples/cve-2019-5418.rs similarity index 100% rename from examples/cve-2019-5418.rs rename to slinger/examples/cve-2019-5418.rs diff --git a/examples/gzip.rs b/slinger/examples/gzip.rs similarity index 100% rename from examples/gzip.rs rename to slinger/examples/gzip.rs diff --git a/examples/http2.rs b/slinger/examples/http2.rs similarity index 100% rename from examples/http2.rs rename to slinger/examples/http2.rs diff --git a/examples/http_record.rs b/slinger/examples/http_record.rs similarity index 100% rename from examples/http_record.rs rename to slinger/examples/http_record.rs diff --git a/examples/http_to_command.rs b/slinger/examples/http_to_command.rs similarity index 100% rename from examples/http_to_command.rs rename to slinger/examples/http_to_command.rs diff --git a/examples/native_tls_example.rs b/slinger/examples/native_tls_example.rs similarity index 57% rename from examples/native_tls_example.rs rename to slinger/examples/native_tls_example.rs index 2719f10..d81a59e 100644 --- a/examples/native_tls_example.rs +++ b/slinger/examples/native_tls_example.rs @@ -1,97 +1,63 @@ //! Example demonstrating how to implement native-tls support as a custom TLS connector //! //! This example shows how users can implement their own native-tls integration -//! when the built-in native-tls feature has been removed from the main library. +//! using the unified CustomTlsConnector interface. //! //! To run this example: //! ```bash //! cargo run --example native_tls_example --features tls //! ``` //! -//! Note: This example requires the `tls` feature without `rustls`. +//! Note: This example requires the `tls` feature. //! The native-tls dependencies are available in dev-dependencies for examples. -#[cfg(all(feature = "tls", not(feature = "rustls")))] -use slinger::{ - ConnectorBuilder, CustomTlsConnector, CustomTlsStream, MaybeTlsStream, PeerCertificate, Result, - Socket, -}; -#[cfg(all(feature = "tls", not(feature = "rustls")))] -use std::pin::Pin; -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] +use slinger::tls::{CustomTlsConnector, CustomTlsStream, PeerCertificate}; +#[cfg(feature = "tls")] +use slinger::{ConnectorBuilder, Result, Socket, StreamWrapper}; +#[cfg(feature = "tls")] use std::sync::Arc; -#[cfg(all(feature = "tls", not(feature = "rustls")))] -use std::task::{Context, Poll}; -#[cfg(all(feature = "tls", not(feature = "rustls")))] -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] use tokio::net::TcpStream; -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] /// Wrapper for tokio-native-tls TlsStream that implements CustomTlsStream -#[derive(Debug)] +/// Uses TlsStreamWrapper to automatically implement AsyncRead and AsyncWrite struct NativeTlsStream { inner: tokio_native_tls::TlsStream, } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] impl NativeTlsStream { fn new(stream: tokio_native_tls::TlsStream) -> Self { Self { inner: stream } } } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] +// Use the macro to implement AsyncRead and AsyncWrite by delegating to the inner TlsStreamWrapper +slinger::impl_tls_stream!(NativeTlsStream, inner); + +#[cfg(feature = "tls")] impl CustomTlsStream for NativeTlsStream { - fn peer_certificate(&self) -> Option { - self + fn peer_certificate(&self) -> Option> { + let cert = self .inner .get_ref() .peer_certificate() - .ok() - .flatten() - .and_then(|cert| cert.to_der().ok()) - .map(|der| PeerCertificate { inner: der }) - } -} - -#[cfg(all(feature = "tls", not(feature = "rustls")))] -impl AsyncRead for NativeTlsStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Pin::new(&mut self.inner).poll_read(cx, buf) - } -} - -#[cfg(all(feature = "tls", not(feature = "rustls")))] -impl AsyncWrite for NativeTlsStream { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.inner).poll_write(cx, buf) - } + .ok()?? + .to_der() + .ok()?; - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) + Some(vec![PeerCertificate { inner: cert }]) } - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) + fn alpn_protocol(&self) -> Option> { + None } } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] /// Native-TLS connector implementation /// /// This demonstrates how to wrap tokio-native-tls to work with slinger's @@ -100,7 +66,7 @@ struct NativeTlsConnector { connector: tokio_native_tls::TlsConnector, } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] impl NativeTlsConnector { fn new() -> std::result::Result> { let mut builder = tokio_native_tls::native_tls::TlsConnector::builder(); @@ -125,7 +91,7 @@ impl NativeTlsConnector { } } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] impl CustomTlsConnector for NativeTlsConnector { fn connect<'a>( &'a self, @@ -138,7 +104,7 @@ impl CustomTlsConnector for NativeTlsConnector { Box::pin(async move { // Extract the TCP stream from the socket let tcp_stream = match stream.inner { - MaybeTlsStream::Tcp(tcp) => tcp, + StreamWrapper::Tcp(tcp) => tcp, _ => { return Err(slinger::Error::Other( "Expected plain TCP stream for TLS upgrade".to_string(), @@ -152,12 +118,12 @@ impl CustomTlsConnector for NativeTlsConnector { .await .map_err(|e| slinger::Error::Other(format!("native-tls handshake failed: {}", e)))?; - // Wrap in our custom stream type + // Wrap in NativeTlsStream which uses TlsStreamWrapper internally let custom_stream = NativeTlsStream::new(tls_stream); // Create a new socket with the TLS stream Ok(Socket::new( - MaybeTlsStream::Custom(Box::new(custom_stream)), + StreamWrapper::Custom(Box::new(custom_stream)), stream.read_timeout, stream.write_timeout, )) @@ -165,7 +131,7 @@ impl CustomTlsConnector for NativeTlsConnector { } } -#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[cfg(feature = "tls")] #[tokio::main] async fn main() -> std::result::Result<(), Box> { println!("Native-TLS Custom Connector Example"); @@ -192,7 +158,7 @@ async fn main() -> std::result::Result<(), Box> { // Try to get peer certificate if let Some(cert) = socket.inner.peer_certificate() { - println!("✓ Retrieved peer certificate ({} bytes)", cert.len()); + println!("✓ Retrieved peer certificate ({})", cert.len()); } else { println!("✗ No peer certificate available"); } @@ -200,21 +166,23 @@ async fn main() -> std::result::Result<(), Box> { println!("\nThis example demonstrates:"); println!("1. Creating a custom TLS connector using native-tls"); println!("2. Implementing the CustomTlsConnector trait"); - println!("3. Implementing the CustomTlsStream trait for the TLS stream wrapper"); - println!("4. Using ConnectorBuilder to configure the connector"); - println!("5. Getting peer certificate information from the TLS connection"); + println!("3. Using impl_tls_stream! macro to avoid AsyncRead/AsyncWrite boilerplate"); + println!("4. Implementing only the CustomTlsStream trait for custom logic"); + println!("5. Using ConnectorBuilder to configure the connector"); + println!("6. Getting peer certificate information from the TLS connection"); println!("\nKey implementation details:"); - println!(" - NativeTlsStream wraps tokio_native_tls::TlsStream"); - println!(" - Implements CustomTlsStream with AsyncRead/AsyncWrite"); - println!(" - Provides peer_certificate() to extract certificate info"); - println!(" - Provides get_ref() to access underlying TcpStream"); + println!(" - NativeTlsStream wraps TlsStreamWrapper"); + println!(" - impl_tls_stream! macro generates AsyncRead/AsyncWrite delegations"); + println!(" - No manual AsyncRead/AsyncWrite implementation needed"); + println!(" - Only CustomTlsStream trait needs custom implementation"); + println!(" - Clean and minimal code pattern"); Ok(()) } -#[cfg(not(all(feature = "tls", not(feature = "rustls"))))] +#[cfg(not(feature = "tls"))] fn main() { - eprintln!("This example requires the 'tls' feature without 'rustls'."); + eprintln!("This example requires the 'tls' feature."); eprintln!("Run with: cargo run --example native_tls_example --features tls"); } diff --git a/examples/proxy.rs b/slinger/examples/proxy.rs similarity index 100% rename from examples/proxy.rs rename to slinger/examples/proxy.rs diff --git a/examples/redirect.rs b/slinger/examples/redirect.rs similarity index 100% rename from examples/redirect.rs rename to slinger/examples/redirect.rs diff --git a/examples/request_budiler.rs b/slinger/examples/request_budiler.rs similarity index 100% rename from examples/request_budiler.rs rename to slinger/examples/request_budiler.rs diff --git a/slinger/examples/rustls_custom_connector.rs b/slinger/examples/rustls_custom_connector.rs new file mode 100644 index 0000000..75f977c --- /dev/null +++ b/slinger/examples/rustls_custom_connector.rs @@ -0,0 +1,98 @@ +//! Example demonstrating how to use rustls as a custom TLS connector +//! +//! This example shows that when the rustls feature is enabled, you can either: +//! 1. Use the default rustls implementation (automatic) +//! 2. Customize rustls settings and provide it as a custom connector +//! +//! To run this example: +//! ```bash +//! cargo run --example rustls_custom_connector --features rustls +//! ``` + +#[cfg(feature = "rustls")] +use slinger::tls::{rustls::RustlsTlsConnector, CustomTlsConnector}; +#[cfg(feature = "rustls")] +use slinger::ConnectorBuilder; +#[cfg(feature = "rustls")] +use std::sync::Arc; + +#[cfg(feature = "rustls")] +#[tokio::main] +async fn main() -> std::result::Result<(), Box> { + println!("Rustls Custom Connector Example"); + println!("================================\n"); + + // Example 1: Using default rustls (automatic when rustls feature is enabled) + println!("Example 1: Default rustls connector"); + println!("------------------------------------"); + let connector1 = ConnectorBuilder::default().build()?; + println!("✓ Built connector with default rustls backend"); + + // Test connection + let socket1 = connector1 + .connect_with_uri(&http::Uri::from_static("https://www.rust-lang.org")) + .await?; + println!("✓ Successfully connected to rust-lang.org\n"); + drop(socket1); + + // Example 2: Creating a custom rustls connector with specific settings + println!("Example 2: Custom rustls connector with specific settings"); + println!("----------------------------------------------------------"); + + // Build a rustls connector with custom settings + let custom_rustls = { + use tokio_rustls::rustls; + + let mut root_cert_store = rustls::RootCertStore::empty(); + let certs = rustls_native_certs::load_native_certs().certs; + for cert in certs { + root_cert_store.add(cert)?; + } + + let provider = rustls::crypto::CryptoProvider::get_default() + .cloned() + .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider())); + + let config = rustls::ClientConfig::builder_with_provider(provider) + .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])? + .with_root_certificates(root_cert_store) + .with_no_client_auth(); + + let rustls_connector = tokio_rustls::TlsConnector::from(Arc::new(config)); + + // Wrap in RustlsTlsConnector + Arc::new(RustlsTlsConnector::new( + rustls_connector, + Some(std::time::Duration::from_secs(30)), + )) as Arc + }; + + // Use the custom rustls connector + let connector2 = ConnectorBuilder::default() + .custom_tls_connector(custom_rustls) + .build()?; + + println!("✓ Built connector with custom rustls settings"); + + // Test connection + let socket2 = connector2 + .connect_with_uri(&http::Uri::from_static("https://www.rust-lang.org")) + .await?; + println!("✓ Successfully connected to rust-lang.org with custom rustls\n"); + drop(socket2); + + println!("Key points:"); + println!("1. When rustls feature is enabled, default connector uses RustlsTlsConnector"); + println!("2. You can create custom rustls settings and wrap them in RustlsTlsConnector"); + println!("3. RustlsTlsConnector implements CustomTlsConnector trait"); + println!("4. This provides a unified interface for all TLS implementations"); + println!("5. Users can easily switch between rustls, native-tls, or other TLS libraries"); + + Ok(()) +} + +#[cfg(not(feature = "rustls"))] +fn main() { + eprintln!("This example requires the 'rustls' feature."); + eprintln!("Run with: cargo run --example rustls_custom_connector --features rustls"); +} diff --git a/examples/schema.rs b/slinger/examples/schema.rs similarity index 100% rename from examples/schema.rs rename to slinger/examples/schema.rs diff --git a/examples/smuggling.rs b/slinger/examples/smuggling.rs similarity index 100% rename from examples/smuggling.rs rename to slinger/examples/smuggling.rs diff --git a/examples/tcp_socket.rs b/slinger/examples/tcp_socket.rs similarity index 100% rename from examples/tcp_socket.rs rename to slinger/examples/tcp_socket.rs diff --git a/examples/tls.rs b/slinger/examples/tls.rs similarity index 100% rename from examples/tls.rs rename to slinger/examples/tls.rs diff --git a/src/body.rs b/slinger/src/body.rs similarity index 100% rename from src/body.rs rename to slinger/src/body.rs diff --git a/src/client.rs b/slinger/src/client.rs similarity index 96% rename from src/client.rs rename to slinger/src/client.rs index f8f9f5b..87de44e 100644 --- a/src/client.rs +++ b/slinger/src/client.rs @@ -14,7 +14,9 @@ use crate::redirect::{remove_sensitive_headers, Action, Policy}; use crate::response::{ResponseBuilder, ResponseConfig}; use crate::socket::Socket; #[cfg(feature = "tls")] -use crate::tls::{self, Certificate, Identity, PeerCertificate}; +use crate::tls::{ + self, Certificate, CustomTlsConnector, CustomTlsStream, Identity, PeerCertificate, +}; use crate::{Connector, ConnectorBuilder, Request, RequestBuilder, Response}; use bytes::Bytes; use http::{HeaderMap, HeaderValue, Method, StatusCode}; @@ -247,7 +249,7 @@ impl Client { // HTTP/1.1 path (existing code) let raw: Bytes = request.to_raw(); #[cfg(feature = "tls")] - let mut peer_certificate: Option = None; + let mut peer_certificate: Option> = None; #[cfg(feature = "tls")] { if let Some(pc) = socket.peer_certificate() { @@ -333,14 +335,14 @@ impl Client { } record.record_request(&request); let key = uniq_key(&cur_uri); - let socket = match conn.entry(key) { + let socket = match conn.entry(key.clone()) { Entry::Occupied(mut entry) => { entry.get_mut().take() // take out the Option } Entry::Vacant(entry) => { let new_socket = self.inner.connector.connect_with_uri(&cur_uri).await?; - entry.insert(None); // placeholder - Some(new_socket) + let slot = entry.insert(Some(new_socket)); + slot.take() } }; let (mut response, socket) = self.execute_request(socket, &request).await?; @@ -348,24 +350,24 @@ impl Client { if let Some(cv) = response.headers().get(http::header::CONNECTION) { match cv.to_str().unwrap_or_default() { "keep-alive" => { - if let Some(Some(s)) = conn.get_mut(&uniq_key(&cur_uri)) { + if let Some(slot) = conn.get_mut(&key) { if let Some(socket) = socket { - *s = socket; + *slot = Some(socket); } } if !self.inner.keepalive { - conn.remove(&uniq_key(&cur_uri)); + conn.remove(&key); } else { keepalive = true; } } _ => { - conn.remove(&uniq_key(&cur_uri)); + conn.remove(&key); keepalive = false; } } } else { - conn.remove(&uniq_key(&cur_uri)); + conn.remove(&key); keepalive = false; } // 原始请求不跳转 @@ -531,12 +533,12 @@ impl ClientBuilder { pub fn build(self) -> Result { let config = self.config; let connector_builder = if let Some(custom_builder) = config.connector_builder { - // Use the custom connector builder directly custom_builder } else { // Build default connector with config values let mut default_builder = ConnectorBuilder::default() .proxy(config.proxy) + .keepalive(config.keepalive) .read_timeout(config.read_timeout) .connect_timeout(config.connect_timeout) .write_timeout(config.timeout); @@ -553,6 +555,9 @@ impl ClientBuilder { if let Some(identity) = config.identity { default_builder = default_builder.identity(identity); } + if let Some(tls) = config.tls { + default_builder = default_builder.custom_tls_connector(tls) + } } #[cfg(feature = "http2")] { @@ -855,6 +860,15 @@ impl ClientBuilder { self.config.max_tls_version = version; self } + /// Controls the use of TLS server name indication. + /// + /// Defaults to `None`. + /// + #[cfg(feature = "tls")] + pub fn tls(mut self, tls: Option>) -> ClientBuilder { + self.config.tls = tls; + self + } #[cfg(feature = "http2")] /// Enable or disable HTTP/2 support. pub fn enable_http2(mut self, http2: bool) -> Self { @@ -933,6 +947,8 @@ struct Config { min_tls_version: Option, #[cfg(feature = "tls")] max_tls_version: Option, + #[cfg(feature = "tls")] + tls: Option>, redirect_policy: Policy, #[cfg(feature = "cookie")] cookie_store: Option>, @@ -980,6 +996,8 @@ impl Default for Config { min_tls_version: None, #[cfg(feature = "tls")] max_tls_version: None, + #[cfg(feature = "tls")] + tls: None, redirect_policy: Policy::Limit(10), #[cfg(feature = "cookie")] cookie_store: None, diff --git a/src/connector.rs b/slinger/src/connector.rs similarity index 61% rename from src/connector.rs rename to slinger/src/connector.rs index 6a00f28..3c4b874 100644 --- a/src/connector.rs +++ b/slinger/src/connector.rs @@ -1,133 +1,150 @@ use crate::errors::Result; use crate::proxy::{Proxy, ProxySocket}; -use crate::socket::{MaybeTlsStream, Socket}; +use crate::socket::{Socket, StreamWrapper}; #[cfg(feature = "tls")] -use crate::tls::Identity; -#[cfg(feature = "rustls")] -use crate::tls::{IgnoreHostname, NoVerifier}; -#[cfg(feature = "tls")] -use crate::{tls, Certificate}; +use crate::tls::{self, Certificate, CustomTlsConnector, Identity}; use socket2::Socket as RawSocket; use socket2::{Domain, Protocol, Type}; use std::net::SocketAddr; use std::time::Duration; use tokio::net::TcpSocket; -#[cfg(feature = "rustls")] -use tokio_rustls::rustls; -#[cfg(feature = "rustls")] -use tokio_rustls::rustls::pki_types::ServerName; - -/// TLS connector type enum to support rustls and custom connectors -#[cfg(feature = "tls")] -#[derive(Clone)] -pub enum TlsConnectorType { - /// Rustls-based TLS connector - #[cfg(feature = "rustls")] - Rustls(tokio_rustls::TlsConnector), - /// Custom TLS connector callback - #[cfg(not(feature = "rustls"))] - Custom(std::sync::Arc), -} - -#[cfg(all(feature = "tls", not(feature = "rustls")))] -/// Trait for custom TLS connector implementations. -/// -/// This trait allows users to implement their own TLS handshake logic when the `tls` feature -/// is enabled without the `rustls` backend. -/// -/// # Example -/// -/// ```ignore -/// use slinger::connector::CustomTlsConnector; -/// use slinger::Socket; -/// use tokio::net::TcpStream; -/// -/// struct MyTlsConnector; -/// -/// impl CustomTlsConnector for MyTlsConnector { -/// fn connect<'a>( -/// &'a self, -/// domain: &'a str, -/// stream: Socket, -/// ) -> std::pin::Pin> + Send + 'a>> { -/// Box::pin(async move { -/// // Implement your custom TLS handshake here -/// // You can use openssl, boringssl, or any other TLS library -/// todo!("Implement custom TLS handshake") -/// }) -/// } -/// } -/// ``` -pub trait CustomTlsConnector: Send + Sync + 'static { - /// Perform TLS handshake on the given TCP stream. - /// - /// # Arguments - /// - /// * `domain` - The domain name for SNI (Server Name Indication) - /// * `stream` - The TCP socket to upgrade to TLS - /// - /// # Returns - /// - /// Returns a `Socket` wrapping the TLS stream on success. - fn connect<'a>( - &'a self, - domain: &'a str, - stream: Socket, - ) -> std::pin::Pin> + Send + 'a>>; -} /// ConnectorBuilder #[derive(Clone)] pub struct ConnectorBuilder { - #[cfg(feature = "http2")] - http2: bool, - hostname_verification: bool, - certs_verification: bool, read_timeout: Option, write_timeout: Option, connect_timeout: Option, - #[cfg(feature = "tls")] - min_tls_version: Option, - #[cfg(feature = "tls")] - max_tls_version: Option, nodelay: bool, keepalive: bool, + proxy: Option, #[cfg(feature = "tls")] - tls_sni: bool, - #[cfg(feature = "tls")] - identity: Option, + tls_config: TlsConfig, #[cfg(feature = "tls")] - certificate: Vec, - proxy: Option, - #[cfg(all(feature = "tls", not(feature = "rustls")))] custom_tls_connector: Option>, } impl Default for ConnectorBuilder { fn default() -> Self { Self { - #[cfg(feature = "http2")] - http2: false, - hostname_verification: true, - certs_verification: true, read_timeout: Some(Duration::from_secs(30)), write_timeout: Some(Duration::from_secs(30)), connect_timeout: Some(Duration::from_secs(10)), - #[cfg(feature = "tls")] - min_tls_version: None, - #[cfg(feature = "tls")] - max_tls_version: None, nodelay: false, keepalive: false, + proxy: None, #[cfg(feature = "tls")] - tls_sni: true, + tls_config: TlsConfig::default(), #[cfg(feature = "tls")] + custom_tls_connector: None, + } + } +} + +#[cfg(feature = "tls")] +/// TLS related configuration extracted from ConnectorBuilder. +#[derive(Clone)] +pub struct TlsConfig { + #[cfg(feature = "http2")] + pub http2: bool, + pub hostname_verification: bool, + pub certs_verification: bool, + pub min_tls_version: Option, + pub max_tls_version: Option, + pub tls_sni: bool, + pub identity: Option, + pub certificate: Vec, +} +#[cfg(feature = "rustls")] +impl TlsConfig { + fn custom( + &self, + connect_timeout: Option, + ) -> Result> { + let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty(); + for cert in self.certificate.clone() { + cert.add_to_tls(&mut root_cert_store)?; + } + let certs = rustls_native_certs::load_native_certs().certs; + for cert in certs { + root_cert_store.add(cert)?; + } + let mut versions = tokio_rustls::rustls::ALL_VERSIONS.to_vec(); + if let Some(min_tls_version) = self.min_tls_version { + versions.retain(|&supported_version| { + match tls::Version::from_tls(supported_version.version) { + Some(version) => version >= min_tls_version, + // Assume it's so new we don't know about it, allow it + // (as of writing this is unreachable) + None => true, + } + }); + } + if let Some(max_tls_version) = self.max_tls_version { + versions.retain(|&supported_version| { + match tls::Version::from_tls(supported_version.version) { + Some(version) => version <= max_tls_version, + None => false, + } + }); + } + if versions.is_empty() { + return Err(crate::errors::builder("empty supported tls versions")); + } + let provider = tokio_rustls::rustls::crypto::CryptoProvider::get_default() + .cloned() + .unwrap_or_else(|| std::sync::Arc::new(tokio_rustls::rustls::crypto::ring::default_provider())); + let signature_algorithms = provider.signature_verification_algorithms; + let config_builder = + tokio_rustls::rustls::ClientConfig::builder_with_provider(provider.clone()) + .with_protocol_versions(&versions) + .map_err(|_| crate::errors::builder("invalid TLS versions"))?; + let config_builder = if !self.certs_verification { + config_builder + .dangerous() + .with_custom_certificate_verifier(std::sync::Arc::new(tls::rustls::NoVerifier)) + } else if !self.hostname_verification { + config_builder + .dangerous() + .with_custom_certificate_verifier(std::sync::Arc::new(tls::rustls::IgnoreHostname::new( + root_cert_store, + signature_algorithms, + ))) + } else { + config_builder.with_root_certificates(root_cert_store) + }; + let rustls_config = if let Some(id) = self.identity.clone() { + id.add_to_tls(config_builder)? + } else { + config_builder.with_no_client_auth() + }; + #[cfg(feature = "http2")] + let rustls_config = { + let mut config = rustls_config; + if self.http2 { + config.alpn_protocols = vec![b"http/1.1".to_vec(), b"h2".to_vec()]; + } + config + }; + Ok(std::sync::Arc::new(tls::rustls::RustlsTlsConnector::new( + tokio_rustls::TlsConnector::from(std::sync::Arc::new(rustls_config)), + connect_timeout, + ))) + } +} +#[cfg(feature = "tls")] +impl Default for TlsConfig { + fn default() -> Self { + Self { + #[cfg(feature = "http2")] + http2: false, + hostname_verification: true, + certs_verification: true, + min_tls_version: None, + max_tls_version: None, + tls_sni: true, identity: None, - #[cfg(feature = "tls")] certificate: vec![], - proxy: None, - #[cfg(all(feature = "tls", not(feature = "rustls")))] - custom_tls_connector: None, } } } @@ -136,33 +153,23 @@ impl ConnectorBuilder { #[cfg(feature = "http2")] /// Enable HTTP/2 support. pub fn enable_http2(mut self, http2: bool) -> Self { - self.http2 = http2; + self.tls_config.http2 = http2; self } + #[cfg(feature = "tls")] /// Controls the use of hostname verification. /// - /// Defaults to `false`. - /// - /// # Warning - /// - /// You should think very carefully before using this method. If invalid hostnames are trusted, *any* valid - /// certificate for *any* site will be trusted for use. This introduces significant vulnerabilities, and should - /// only be used as a last resort. + /// Defaults to `true`. pub fn hostname_verification(mut self, value: bool) -> ConnectorBuilder { - self.hostname_verification = value; + self.tls_config.hostname_verification = value; self } + #[cfg(feature = "tls")] /// Controls the use of certificate validation. /// - /// Defaults to `false`. - /// - /// # Warning - /// - /// You should think very carefully before using this method. If invalid certificates are trusted, *any* - /// certificate for *any* site will be trusted for use. This includes expired certificates. This introduces - /// significant vulnerabilities, and should only be used as a last resort. + /// Defaults to `true`. pub fn certs_verification(mut self, value: bool) -> ConnectorBuilder { - self.certs_verification = value; + self.tls_config.certs_verification = value; self } /// Set that all sockets have `SO_NODELAY` set to the supplied value `nodelay`. @@ -184,19 +191,19 @@ impl ConnectorBuilder { /// Defaults to `true`. #[cfg(feature = "tls")] pub fn tls_sni(mut self, value: bool) -> ConnectorBuilder { - self.tls_sni = value; + self.tls_config.tls_sni = value; self } /// Adds a certificate to the set of roots that the connector will trust. #[cfg(feature = "tls")] pub fn certificate(mut self, value: Vec) -> ConnectorBuilder { - self.certificate = value; + self.tls_config.certificate = value; self } /// Sets the identity to be used for client certificate authentication. #[cfg(feature = "tls")] pub fn identity(mut self, value: Identity) -> ConnectorBuilder { - self.identity = Some(value); + self.tls_config.identity = Some(value); self } /// Enables a read timeout. @@ -253,7 +260,7 @@ impl ConnectorBuilder { /// This requires the optional `tls` feature to be enabled. #[cfg(feature = "tls")] pub fn min_tls_version(mut self, version: Option) -> ConnectorBuilder { - self.min_tls_version = version; + self.tls_config.min_tls_version = version; self } /// Set the maximum required TLS version for connections. @@ -265,15 +272,16 @@ impl ConnectorBuilder { /// This requires the optional `tls` feature to be enabled. #[cfg(feature = "tls")] pub fn max_tls_version(mut self, version: Option) -> ConnectorBuilder { - self.max_tls_version = version; + self.tls_config.max_tls_version = version; self } /// Set a custom TLS connector for custom TLS handshake implementations. /// - /// This is only available when the `tls` feature is enabled without the - /// `rustls` backend. It allows you to provide your own TLS - /// implementation using libraries like openssl, boringssl, native-tls, or any other TLS library. + /// This is available when the `tls` feature is enabled. It allows you to provide + /// your own TLS implementation using libraries like openssl, boringssl, native-tls, + /// or any other TLS library. When the `rustls` feature is enabled, a default rustls + /// implementation is used if no custom connector is provided. /// /// # Example /// @@ -287,8 +295,8 @@ impl ConnectorBuilder { /// impl CustomTlsConnector for MyTlsConnector { /// fn connect<'a>( /// &'a self, - /// stream: Socket, /// domain: &'a str, + /// stream: Socket, /// ) -> std::pin::Pin> + Send + 'a>> { /// Box::pin(async move { /// // Your custom TLS handshake logic here @@ -301,7 +309,7 @@ impl ConnectorBuilder { /// .custom_tls_connector(Arc::new(MyTlsConnector)) /// .build()?; /// ``` - #[cfg(all(feature = "tls", not(feature = "rustls")))] + #[cfg(feature = "tls")] pub fn custom_tls_connector( mut self, connector: std::sync::Arc, @@ -312,97 +320,23 @@ impl ConnectorBuilder { } impl ConnectorBuilder { - #[cfg(feature = "rustls")] - /// Use rustls only - fn only_rustls(&self) -> Result { - let mut root_cert_store = rustls::RootCertStore::empty(); - for cert in self.certificate.clone() { - cert.add_to_tls(&mut root_cert_store)?; - } - let certs = rustls_native_certs::load_native_certs().certs; - for cert in certs { - root_cert_store.add(cert)?; - } - let mut versions = rustls::ALL_VERSIONS.to_vec(); - - if let Some(min_tls_version) = self.min_tls_version { - versions.retain(|&supported_version| { - match tls::Version::from_tls(supported_version.version) { - Some(version) => version >= min_tls_version, - // Assume it's so new we don't know about it, allow it - // (as of writing this is unreachable) - None => true, - } - }); - } - - if let Some(max_tls_version) = self.max_tls_version { - versions.retain(|&supported_version| { - match tls::Version::from_tls(supported_version.version) { - Some(version) => version <= max_tls_version, - None => false, - } - }); - } - - if versions.is_empty() { - return Err(crate::errors::builder("empty supported tls versions")); - } - let provider = rustls::crypto::CryptoProvider::get_default() - .cloned() - .unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::ring::default_provider())); - let signature_algorithms = provider.signature_verification_algorithms; - let config_builder = rustls::ClientConfig::builder_with_provider(provider.clone()) - .with_protocol_versions(&versions) - .map_err(|_| crate::errors::builder("invalid TLS versions"))?; - let config_builder = if !self.certs_verification { - config_builder - .dangerous() - .with_custom_certificate_verifier(std::sync::Arc::new(NoVerifier)) - } else if !self.hostname_verification { - config_builder - .dangerous() - .with_custom_certificate_verifier(std::sync::Arc::new(IgnoreHostname::new( - root_cert_store, - signature_algorithms, - ))) - } else { - config_builder.with_root_certificates(root_cert_store) - }; - let rustls_config = if let Some(id) = self.identity.clone() { - id.add_to_tls(config_builder)? - } else { - config_builder.with_no_client_auth() - }; - #[cfg(feature = "http2")] - let rustls_config = { - let mut config = rustls_config; - if self.http2 { - config.alpn_protocols = vec![b"http/1.1".to_vec(), b"h2".to_vec()]; - } - config - }; - Ok(TlsConnectorType::Rustls(tokio_rustls::TlsConnector::from( - std::sync::Arc::new(rustls_config), - ))) - } - /// Combine the configuration of this builder with a connector to create a `Connector`. pub fn build(&self) -> Result { #[cfg(feature = "tls")] let tls = { - #[cfg(feature = "rustls")] - { - self.only_rustls()? - } - #[cfg(not(feature = "rustls"))] - { - // When only tls feature is enabled, require a custom connector - if let Some(custom) = &self.custom_tls_connector { - TlsConnectorType::Custom(custom.clone()) - } else { + // custom connector takes precedence; otherwise if rustls is enabled, build it from config + if let Some(custom) = &self.custom_tls_connector { + custom.clone() + } else { + #[cfg(feature = "rustls")] + { + // Try to convert the builder into a rustls connector. Clone self because TryInto consumes it. + self.tls_config.custom(self.connect_timeout)? + } + #[cfg(not(feature = "rustls"))] + { return Err(crate::errors::builder( - "TLS feature enabled without backend: please enable 'rustls' feature, or provide a custom TLS connector using .custom_tls_connector()" + "TLS feature enabled without backend: please enable 'rustls' feature, or provide a custom TLS connector using .custom_tls_connector()", )); } } @@ -431,7 +365,7 @@ pub struct Connector { write_timeout: Option, proxy: Option, #[cfg(feature = "tls")] - tls: TlsConnectorType, + tls: std::sync::Arc, } impl PartialEq for Connector { @@ -463,7 +397,7 @@ impl Connector { .map_err(|x| crate::errors::new_io_error(std::io::ErrorKind::TimedOut, &x.to_string()))??, }; Ok(Socket::new( - MaybeTlsStream::Tcp(s), + StreamWrapper::Tcp(s), self.read_timeout, self.write_timeout, )) @@ -477,52 +411,7 @@ impl Connector { #[cfg(feature = "tls")] /// A `Connector` will use transport layer security (TLS) by default to connect to destinations. pub async fn upgrade_to_tls(&self, stream: Socket, domain: &str) -> Result { - match &self.tls { - #[cfg(feature = "rustls")] - TlsConnectorType::Rustls(connector) => { - // Rustls implementation - let domain = ServerName::try_from(domain.to_owned()) - .map_err(|e| crate::errors::Error::Other(e.to_string()))?; - let connect_timeout = self.connect_timeout.unwrap_or(Duration::from_secs(30)); - match stream.inner { - MaybeTlsStream::Tcp(t) => { - let s = tokio::time::timeout(connect_timeout, connector.connect(domain, t)) - .await - .map_err(|e| { - crate::errors::new_io_error(std::io::ErrorKind::TimedOut, &e.to_string()) - })? - .map_err(|e| { - crate::errors::Error::Other(format!("rustls handshake failed: {}", e)) - })?; - let tls = Socket::new( - MaybeTlsStream::Rustls(s.into()), - stream.read_timeout, - stream.write_timeout, - ); - Ok(tls) - } - MaybeTlsStream::Rustls(t) => Ok(Socket::new( - MaybeTlsStream::Rustls(t), - stream.read_timeout, - stream.write_timeout, - )), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - MaybeTlsStream::Custom(t) => Ok(Socket::new( - MaybeTlsStream::Custom(t), - stream.read_timeout, - stream.write_timeout, - )), - } - } - #[cfg(not(feature = "rustls"))] - TlsConnectorType::Custom(connector) => { - // Custom TLS implementation - // let connect_timeout = self.connect_timeout.unwrap_or(Duration::from_secs(30)); - let domain = domain.to_string(); - let tls = connector.connect(&domain, stream).await?; - Ok(tls) - } - } + self.tls.connect(domain, stream).await } } diff --git a/src/cookies.rs b/slinger/src/cookies.rs similarity index 100% rename from src/cookies.rs rename to slinger/src/cookies.rs diff --git a/src/errors.rs b/slinger/src/errors.rs similarity index 79% rename from src/errors.rs rename to slinger/src/errors.rs index c622a6d..8be3846 100644 --- a/src/errors.rs +++ b/slinger/src/errors.rs @@ -53,20 +53,28 @@ pub enum ReplyError { impl From for Error { fn from(value: http::Error) -> Self { - Error::Http(value) + let error = Error::Http(value); + tracing::error!("HTTP error: {}", error); + error } } impl From for Error { fn from(value: http::header::InvalidHeaderValue) -> Self { - Error::Http(http::Error::from(value)) + let error = Error::Http(http::Error::from(value)); + tracing::error!("Invalid header value error: {}", error); + error } } pub(crate) fn new_io_error(error_kind: ErrorKind, msg: &str) -> Error { - Error::IO(std::io::Error::new(error_kind, msg)) + let error = Error::IO(std::io::Error::new(error_kind, msg)); + tracing::error!("IO error: {}", error); + error } #[cfg(feature = "tls")] pub(crate) fn builder>>(e: E) -> Error { - Error::Other(e.into().to_string()) + let error = Error::Other(e.into().to_string()); + tracing::error!("Builder error: {}", error); + error } diff --git a/src/h2_client.rs b/slinger/src/h2_client.rs similarity index 55% rename from src/h2_client.rs rename to slinger/src/h2_client.rs index 0c751cf..744fdd2 100644 --- a/src/h2_client.rs +++ b/slinger/src/h2_client.rs @@ -2,76 +2,12 @@ //! //! This module provides HTTP/2 support using the h2 library -use crate::socket::{MaybeTlsStream, Socket}; +use crate::socket::Socket; use crate::{Request, Response}; use bytes::Bytes; use h2::client; use http::Version; -use std::pin::Pin; -use std::task::{Context, Poll}; use std::time::Duration; -use tokio::io::{AsyncRead, AsyncWrite}; - -/// Wrapper for MaybeTlsStream that allows h2 to work with any stream type -enum StreamWrapper { - Tcp(tokio::net::TcpStream), - #[cfg(feature = "rustls")] - Tls(Box>), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - Custom(Box), -} - -impl AsyncRead for StreamWrapper { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - match self.get_mut() { - StreamWrapper::Tcp(stream) => Pin::new(stream).poll_read(cx, buf), - #[cfg(feature = "rustls")] - StreamWrapper::Tls(stream) => Pin::new(stream).poll_read(cx, buf), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - StreamWrapper::Custom(stream) => Pin::new(stream).poll_read(cx, buf), - } - } -} - -impl AsyncWrite for StreamWrapper { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - match self.get_mut() { - StreamWrapper::Tcp(stream) => Pin::new(stream).poll_write(cx, buf), - #[cfg(feature = "rustls")] - StreamWrapper::Tls(stream) => Pin::new(stream).poll_write(cx, buf), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - StreamWrapper::Custom(stream) => Pin::new(stream).poll_write(cx, buf), - } - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.get_mut() { - StreamWrapper::Tcp(stream) => Pin::new(stream).poll_flush(cx), - #[cfg(feature = "rustls")] - StreamWrapper::Tls(stream) => Pin::new(stream).poll_flush(cx), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - StreamWrapper::Custom(stream) => Pin::new(stream).poll_flush(cx), - } - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.get_mut() { - StreamWrapper::Tcp(stream) => Pin::new(stream).poll_shutdown(cx), - #[cfg(feature = "rustls")] - StreamWrapper::Tls(stream) => Pin::new(stream).poll_shutdown(cx), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - StreamWrapper::Custom(stream) => Pin::new(stream).poll_shutdown(cx), - } - } -} pub(crate) async fn send_h2_request( socket: Socket, @@ -81,17 +17,8 @@ pub(crate) async fn send_h2_request( // Extract the stream from the socket let _read_timeout = socket.read_timeout; let _write_timeout = socket.write_timeout; - - let io = match socket.inner { - MaybeTlsStream::Tcp(tcp_stream) => StreamWrapper::Tcp(tcp_stream), - #[cfg(feature = "rustls")] - MaybeTlsStream::Rustls(tls_stream) => StreamWrapper::Tls(tls_stream), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - MaybeTlsStream::Custom(tls_stream) => StreamWrapper::Custom(tls_stream), - }; - // Create an h2 client handshake - let (client, h2_conn) = client::handshake(io) + let (client, h2_conn) = client::handshake(socket.inner) .await .map_err(|e| crate::errors::Error::Other(format!("HTTP/2 handshake failed: {}", e)))?; tokio::spawn(async { diff --git a/src/lib.rs b/slinger/src/lib.rs similarity index 86% rename from src/lib.rs rename to slinger/src/lib.rs index c2a3f82..e18e541 100644 --- a/src/lib.rs +++ b/slinger/src/lib.rs @@ -93,35 +93,44 @@ //! HTTPS destinations. //! //! - Additional server certificates can be configured on a `ClientBuilder` -//! with the [`tls::Certificate`][Certificate] type. +//! with the [`tls::Certificate`][tls::Certificate] type. //! - Client certificates can be added to a `ClientBuilder` with the //! [`tls::Identity`][tls::Identity] type. //! - Various parts of TLS can also be configured or even disabled on the //! `ClientBuilder`. //! +//! ### TLS Backend +//! +//! When the `rustls` feature is enabled (recommended), slinger uses rustls as the default +//! TLS backend through the unified `CustomTlsConnector` interface. This provides a consistent +//! API regardless of which TLS implementation you use. +//! +//! You can also provide your own TLS implementation by implementing the `CustomTlsConnector` +//! trait and setting it with `ConnectorBuilder::custom_tls_connector()`. +//! //! ## Optional Features //! -//! The following are a list of [Cargo features][cargo-features] that can be +//! The following are a list of [Cargo features]\[cargo-features\] that can be //! enabled or disabled: //! //! - **charset**: Improved support for decoding text. //! - **cookie**: Provides cookie session support. -//! - **tls**: Base TLS feature flag. Enables TLS types and interfaces without a specific backend. -//! - **rustls**: Provides HTTPS support via rustls (requires `tls` feature). +//! - **tls**: Base TLS feature flag. Enables TLS types and interfaces. +//! - **rustls**: Provides HTTPS support via rustls (requires `tls` feature). This is the recommended TLS backend. //! - **serde**: Provides serialization and deserialization support. //! - **gzip**: Provides response body gzip decompression. //! - **http2**: Provides HTTP/2 support (requires a TLS backend). //! //! ### TLS Backend Selection //! -//! When the `tls` feature is enabled, you can choose a TLS backend: -//! - Enable `rustls` for pure Rust TLS implementation +//! When the `tls` feature is enabled, you have several options: +//! - Enable `rustls` for the default pure Rust TLS implementation (recommended) //! - Or provide a custom TLS connector by implementing the `CustomTlsConnector` trait //! //! ### Custom TLS Backend //! -//! If you enable only the `tls` feature without a backend, you can implement your own -//! TLS handshake logic using any TLS library (OpenSSL, BoringSSL, native-tls, etc.): +//! If you enable the `tls` feature, you can implement your own TLS handshake logic +//! using any TLS library (OpenSSL, BoringSSL, native-tls, etc.): //! //! ```ignore //! use slinger::{ConnectorBuilder, CustomTlsConnector}; @@ -129,7 +138,7 @@ //! struct MyTlsConnector; //! //! impl CustomTlsConnector for MyTlsConnector { -//! fn connect<'a>(&'a self, stream: Socket, domain: &'a str) +//! fn connect<'a>(&'a self, domain: &'a str, stream: Socket) //! -> Pin> + Send + 'a>> //! { //! // Your TLS implementation here @@ -142,6 +151,9 @@ //! .build()?; //! ``` //! +//! When the `rustls` feature is enabled, rustls is provided as the default implementation +//! but you can still override it with your own custom connector if needed. +//! mod body; mod client; mod connector; @@ -162,33 +174,20 @@ mod serde_schema; mod socket; #[cfg(feature = "tls")] pub mod tls; -#[cfg(feature = "tls")] -pub use tls::Certificate; - pub use body::Body; use bytes::Bytes; pub use client::{Client, ClientBuilder}; -#[cfg(all(feature = "tls", not(feature = "rustls")))] -pub use connector::CustomTlsConnector; pub use connector::{Connector, ConnectorBuilder}; pub use errors::{Error, Result}; pub use http; #[cfg(feature = "serde")] pub use http_serde; -// #[cfg(feature = "tls")] -// pub use native_tls; -// #[cfg(feature = "tls")] -// pub use openssl; pub use proxy::Proxy; pub use request::{RawRequest, Request, RequestBuilder}; pub use response::{Response, ResponseBuilder, ResponseConfig}; -#[cfg(all(feature = "tls", not(feature = "rustls")))] -pub use socket::CustomTlsStream; -#[cfg(all(feature = "tls", not(feature = "rustls")))] -pub use socket::MaybeTlsStream; pub use socket::Socket; #[cfg(feature = "tls")] -pub use tls::PeerCertificate; +pub use socket::StreamWrapper; /// Shortcut method to quickly make a `GET` request. /// diff --git a/src/proxy.rs b/slinger/src/proxy.rs similarity index 100% rename from src/proxy.rs rename to slinger/src/proxy.rs diff --git a/src/record.rs b/slinger/src/record.rs similarity index 100% rename from src/record.rs rename to slinger/src/record.rs diff --git a/src/redirect.rs b/slinger/src/redirect.rs similarity index 100% rename from src/redirect.rs rename to slinger/src/redirect.rs diff --git a/src/request.rs b/slinger/src/request.rs similarity index 100% rename from src/request.rs rename to slinger/src/request.rs diff --git a/src/response.rs b/slinger/src/response.rs similarity index 99% rename from src/response.rs rename to slinger/src/response.rs index cd56b6b..de405ce 100644 --- a/src/response.rs +++ b/slinger/src/response.rs @@ -372,8 +372,8 @@ impl Response { /// ``` /// #[cfg(feature = "tls")] - pub fn certificate(&self) -> Option<&PeerCertificate> { - self.extensions().get::() + pub fn certificate(&self) -> Option<&Vec> { + self.extensions().get::>() } /// Get the http record used to get this `Response`. /// diff --git a/src/serde_schema.rs b/slinger/src/serde_schema.rs similarity index 100% rename from src/serde_schema.rs rename to slinger/src/serde_schema.rs diff --git a/src/socket.rs b/slinger/src/socket.rs similarity index 61% rename from src/socket.rs rename to slinger/src/socket.rs index b11cc17..6fbbd12 100644 --- a/src/socket.rs +++ b/slinger/src/socket.rs @@ -1,20 +1,16 @@ #[cfg(feature = "tls")] -use crate::tls::PeerCertificate; +use crate::tls::{CustomTlsStream, PeerCertificate}; use std::io::Error; use std::ops::{Deref, DerefMut}; use std::pin::Pin; use std::task::{Context, Poll}; use std::time::Duration; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; -use tokio::net::TcpStream; -#[cfg(feature = "rustls")] -use tokio_rustls::client::TlsStream as RustlsStream; /// Socket -#[derive(Debug)] pub struct Socket { /// The underlying stream (TCP, TLS, or custom TLS) - pub inner: MaybeTlsStream, + pub inner: StreamWrapper, /// Read timeout for socket operations pub read_timeout: Option, /// Write timeout for socket operations @@ -24,7 +20,7 @@ pub struct Socket { impl Socket { /// Create a new Socket with the given stream and timeouts pub fn new( - maybe_tls_stream: MaybeTlsStream, + maybe_tls_stream: StreamWrapper, read_timeout: Option, write_timeout: Option, ) -> Self { @@ -44,78 +40,40 @@ impl Socket { #[cfg(feature = "http2")] pub(crate) fn http2_negotiated(&self) -> Option> { match &self.inner { - MaybeTlsStream::Tcp(_) => None, - #[cfg(feature = "rustls")] - MaybeTlsStream::Rustls(tls) => tls - .get_ref() - .1 - .alpn_protocol() - .map(|protocol| protocol.to_vec()), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - MaybeTlsStream::Custom(c) => c.alpn_protocol(), + StreamWrapper::Tcp(_) => None, + #[cfg(feature = "tls")] + StreamWrapper::Custom(c) => c.alpn_protocol(), } } } -/// Enum representing different types of streams (TCP, TLS with rustls, or custom TLS) -#[derive(Debug)] -pub enum MaybeTlsStream { + +/// Wrapper for StreamWrapper that allows h2 to work with any stream type +pub enum StreamWrapper { /// TCP - Tcp(TcpStream), - #[cfg(feature = "rustls")] - /// TLS with rustls - Rustls(Box>), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - /// Custom TLS implementation (when tls feature is enabled without rustls backend) + Tcp(tokio::net::TcpStream), + #[cfg(feature = "tls")] + /// Custom TLS implementation (when tls feature is enabled) Custom(Box), } - -#[cfg(all(feature = "tls", not(feature = "rustls")))] -/// Trait for custom TLS stream implementations -pub trait CustomTlsStream: - AsyncRead + AsyncWrite + Send + Sync + std::fmt::Debug + Unpin + 'static -{ - /// Get the peer certificate from the TLS connection, if available - fn peer_certificate(&self) -> Option { - None - } - /// Get the alpn_protocol - fn alpn_protocol(&self) -> Option> { - None - } -} - -#[cfg(feature = "rustls")] -impl From> for MaybeTlsStream { - fn from(stream: RustlsStream) -> Self { - MaybeTlsStream::Rustls(Box::new(stream)) - } -} - -#[cfg(all(feature = "tls", not(feature = "rustls")))] -impl From for MaybeTlsStream { - fn from(stream: T) -> Self { - MaybeTlsStream::Custom(Box::new(stream)) +#[cfg(feature = "tls")] +impl CustomTlsStream for StreamWrapper { + fn peer_certificate(&self) -> Option> { + match &self { + StreamWrapper::Tcp(_) => None, + #[cfg(feature = "tls")] + StreamWrapper::Custom(stream) => stream.peer_certificate(), + } } -} -impl MaybeTlsStream { - #[cfg(feature = "tls")] - /// get peer_certificate - pub fn peer_certificate(&self) -> Option { + fn alpn_protocol(&self) -> Option> { match &self { - MaybeTlsStream::Tcp(_) => None, - #[cfg(feature = "rustls")] - MaybeTlsStream::Rustls(stream) => stream - .get_ref() - .1 - .peer_certificates() - .and_then(|certs| certs.first()) - .map(|x| PeerCertificate { inner: x.to_vec() }), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - MaybeTlsStream::Custom(stream) => stream.peer_certificate(), + StreamWrapper::Tcp(_) => None, + #[cfg(feature = "tls")] + StreamWrapper::Custom(stream) => stream.alpn_protocol(), } } } + // 实现socket的读写 impl AsyncRead for Socket { fn poll_read( @@ -143,52 +101,44 @@ impl AsyncWrite for Socket { Pin::new(&mut self.inner).poll_shutdown(cx) } } -impl AsyncRead for MaybeTlsStream { +impl AsyncRead for StreamWrapper { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { match self.get_mut() { - MaybeTlsStream::Tcp(stream) => Pin::new(stream).poll_read(cx, buf), - #[cfg(feature = "rustls")] - MaybeTlsStream::Rustls(stream) => Pin::new(stream).poll_read(cx, buf), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - MaybeTlsStream::Custom(stream) => Pin::new(stream).poll_read(cx, buf), + StreamWrapper::Tcp(stream) => Pin::new(stream).poll_read(cx, buf), + #[cfg(feature = "tls")] + StreamWrapper::Custom(stream) => Pin::new(stream).poll_read(cx, buf), } } } -impl AsyncWrite for MaybeTlsStream { +impl AsyncWrite for StreamWrapper { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { match self.get_mut() { - MaybeTlsStream::Tcp(stream) => Pin::new(stream).poll_write(cx, buf), - #[cfg(feature = "rustls")] - MaybeTlsStream::Rustls(stream) => Pin::new(stream).poll_write(cx, buf), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - MaybeTlsStream::Custom(stream) => Pin::new(stream).poll_write(cx, buf), + StreamWrapper::Tcp(stream) => Pin::new(stream).poll_write(cx, buf), + #[cfg(feature = "tls")] + StreamWrapper::Custom(stream) => Pin::new(stream).poll_write(cx, buf), } } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { - MaybeTlsStream::Tcp(stream) => Pin::new(stream).poll_flush(cx), - #[cfg(feature = "rustls")] - MaybeTlsStream::Rustls(stream) => Pin::new(stream).poll_flush(cx), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - MaybeTlsStream::Custom(stream) => Pin::new(stream).poll_flush(cx), + StreamWrapper::Tcp(stream) => Pin::new(stream).poll_flush(cx), + #[cfg(feature = "tls")] + StreamWrapper::Custom(stream) => Pin::new(stream).poll_flush(cx), } } fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { - MaybeTlsStream::Tcp(stream) => Pin::new(stream).poll_shutdown(cx), - #[cfg(feature = "rustls")] - MaybeTlsStream::Rustls(stream) => Pin::new(stream).poll_shutdown(cx), - #[cfg(all(feature = "tls", not(feature = "rustls")))] - MaybeTlsStream::Custom(stream) => Pin::new(stream).poll_shutdown(cx), + StreamWrapper::Tcp(stream) => Pin::new(stream).poll_shutdown(cx), + #[cfg(feature = "tls")] + StreamWrapper::Custom(stream) => Pin::new(stream).poll_shutdown(cx), } } } @@ -259,7 +209,7 @@ impl Socket { // 直接暴露socket的全部外部接口 impl Deref for Socket { - type Target = MaybeTlsStream; + type Target = StreamWrapper; fn deref(&self) -> &Self::Target { &self.inner diff --git a/src/tls.rs b/slinger/src/tls/mod.rs similarity index 56% rename from src/tls.rs rename to slinger/src/tls/mod.rs index e607462..f4bf6e3 100644 --- a/src/tls.rs +++ b/slinger/src/tls/mod.rs @@ -1,22 +1,125 @@ //! TLS configuration and types //! #[cfg(feature = "rustls")] -use std::io::{BufRead, BufReader}; +pub mod rustls; use std::ops::Deref; +use tokio::io::{AsyncRead, AsyncWrite}; +use crate::Socket; #[cfg(feature = "rustls")] -use tokio_rustls::rustls; -#[cfg(feature = "rustls")] -use tokio_rustls::rustls::crypto::WebPkiSupportedAlgorithms; -#[cfg(feature = "rustls")] -use tokio_rustls::rustls::pki_types::{ServerName, UnixTime}; -#[cfg(feature = "rustls")] -use tokio_rustls::rustls::server::ParsedCertificate; -#[cfg(feature = "rustls")] -use tokio_rustls::rustls::{ - client::danger::HandshakeSignatureValid, client::danger::ServerCertVerified, - client::danger::ServerCertVerifier, DigitallySignedStruct, Error as TLSError, RootCertStore, - SignatureScheme, -}; +use std::io::{BufRead, BufReader}; +/// Trait for custom TLS connector implementations. +/// +/// This trait allows users to implement their own TLS handshake logic when the `tls` feature +/// is enabled. Both rustls and custom TLS implementations use this unified interface. +/// +/// # Example +/// +/// ```ignore +/// use slinger::connector::CustomTlsConnector; +/// use slinger::Socket; +/// use tokio::net::TcpStream; +/// +/// struct MyTlsConnector; +/// +/// impl CustomTlsConnector for MyTlsConnector { +/// fn connect<'a>( +/// &'a self, +/// domain: &'a str, +/// stream: Socket, +/// ) -> std::pin::Pin> + Send + 'a>> { +/// Box::pin(async move { +/// // Implement your custom TLS handshake here +/// // You can use openssl, boringssl, or any other TLS library +/// todo!("Implement custom TLS handshake") +/// }) +/// } +/// } +/// ``` +pub trait CustomTlsConnector: Send + Sync + 'static { + /// Perform TLS handshake on the given TCP stream. + /// + /// # Arguments + /// + /// * `domain` - The domain name for SNI (Server Name Indication) + /// * `stream` - The TCP socket to upgrade to TLS + /// + /// # Returns + /// + /// Returns a `Socket` wrapping the TLS stream on success. + fn connect<'a>( + &'a self, + domain: &'a str, + stream: Socket, + ) -> std::pin::Pin> + Send + 'a>>; +} + +/// Macro to implement AsyncRead and AsyncWrite by delegating to an inner TlsStreamWrapper field +/// +/// This macro reduces boilerplate when creating custom TLS stream wrappers. +/// It generates AsyncRead and AsyncWrite implementations that delegate to a field +/// containing a TlsStreamWrapper. +/// +/// # Usage +/// +/// ```ignore +/// struct MyTlsStream { +/// inner: TlsStreamWrapper, +/// } +/// +/// slinger::impl_tls_stream!(MyTlsStream, inner); +/// ``` +/// +/// The first argument is the type name, and the second is the field name containing +/// the TlsStreamWrapper. +#[macro_export] +macro_rules! impl_tls_stream { + ($type:ty, $field:ident) => { + impl tokio::io::AsyncRead for $type { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + std::pin::Pin::new(&mut self.$field).poll_read(cx, buf) + } + } + + impl tokio::io::AsyncWrite for $type { + fn poll_write( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + std::pin::Pin::new(&mut self.$field).poll_write(cx, buf) + } + + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::pin::Pin::new(&mut self.$field).poll_flush(cx) + } + + fn poll_shutdown( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::pin::Pin::new(&mut self.$field).poll_shutdown(cx) + } + } + }; +} +/// Trait for custom TLS stream implementations +pub trait CustomTlsStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static { + /// Get the peer certificate from the TLS connection, if available + fn peer_certificate(&self) -> Option> { + None + } + /// Get the alpn_protocol + fn alpn_protocol(&self) -> Option> { + None + } +} /// peer certificate #[derive(Clone, Debug)] pub struct PeerCertificate { @@ -34,17 +137,45 @@ impl Deref for PeerCertificate { /// Represents a server X509 certificate. #[derive(Clone, Debug)] pub struct Certificate { - #[cfg_attr(not(feature = "rustls"), allow(dead_code))] original: Cert, } +/// Cert #[derive(Clone, Debug)] -enum Cert { - #[cfg_attr(not(feature = "rustls"), allow(dead_code))] +pub enum Cert { + /// Der Der(Vec), - #[cfg_attr(not(feature = "rustls"), allow(dead_code))] + /// Pem Pem(Vec), } +impl Cert { + /// Returns the DER-encoded bytes if this is a DER certificate. + pub fn as_der(&self) -> Option<&[u8]> { + match self { + Cert::Der(data) => Some(data), + Cert::Pem(_) => None, + } + } + + /// Returns the PEM-encoded bytes if this is a PEM certificate. + pub fn as_pem(&self) -> Option<&[u8]> { + match self { + Cert::Pem(data) => Some(data), + Cert::Der(_) => None, + } + } + + /// Returns the raw bytes regardless of format. + pub fn to_bytes(&self) -> Vec { + match self { + Cert::Der(data) | Cert::Pem(data) => data.clone(), + } + } +} impl Certificate { + /// Returns the inner certificate representation. + pub fn original(&self) -> &Cert { + &self.original + } /// Create a `Certificate` from a binary DER encoded certificate /// /// # Examples @@ -56,7 +187,7 @@ impl Certificate { /// let mut buf = Vec::new(); /// File::open("my_cert.der")? /// .read_to_end(&mut buf)?; - /// let cert = slinger::Certificate::from_der(&buf)?; + /// let cert = slinger::tls::Certificate::from_der(&buf)?; /// # drop(cert); /// # Ok(()) /// # } @@ -78,7 +209,7 @@ impl Certificate { /// let mut buf = Vec::new(); /// File::open("my_cert.pem")? /// .read_to_end(&mut buf)?; - /// let cert = slinger::Certificate::from_pem(&buf)?; + /// let cert = slinger::tls::Certificate::from_pem(&buf)?; /// # drop(cert); /// # Ok(()) /// # } @@ -101,7 +232,7 @@ impl Certificate { /// let mut buf = Vec::new(); /// File::open("ca-bundle.crt")? /// .read_to_end(&mut buf)?; - /// let certs = slinger::Certificate::from_pem_bundle(&buf)?; + /// let certs = slinger::tls::Certificate::from_pem_bundle(&buf)?; /// # drop(certs); /// # Ok(()) /// # } @@ -123,13 +254,13 @@ impl Certificate { } #[cfg(feature = "rustls")] - pub(crate) fn add_to_tls(self, root_cert_store: &mut RootCertStore) -> crate::Result<()> { - use std::io::Cursor; + pub(crate) fn add_to_tls(self, root_cert_store: &mut tokio_rustls::rustls::RootCertStore) -> crate::Result<()> { match self.original { Cert::Der(buf) => root_cert_store .add(buf.into()) .map_err(crate::errors::builder)?, Cert::Pem(buf) => { + use std::io::Cursor; let mut reader = Cursor::new(buf); let certs = Self::read_pem_certs(&mut reader)?; for c in certs { @@ -152,10 +283,10 @@ impl Certificate { .collect() } } +#[allow(dead_code)] /// Represents a private key and X509 cert as a client certificate. #[derive(Clone)] pub struct Identity { - #[cfg_attr(not(feature = "rustls"), allow(dead_code))] inner: ClientCert, } enum ClientCert { @@ -214,12 +345,10 @@ impl Identity { { use rustls_pemfile::Item; use std::io::Cursor; - let (key, certs) = { let mut pem = Cursor::new(buf); let mut sk = Vec::::new(); let mut certs = Vec::::new(); - for result in rustls_pemfile::read_all(&mut pem) { match result { Ok(Item::X509Certificate(cert)) => certs.push(cert), @@ -227,27 +356,25 @@ impl Identity { Ok(Item::Pkcs8Key(key)) => sk.push(key.into()), Ok(Item::Sec1Key(key)) => sk.push(key.into()), Ok(_) => { - return Err(crate::errors::builder(TLSError::General(String::from( + return Err(crate::errors::builder(tokio_rustls::rustls::Error::General(String::from( "No valid certificate was found", )))) } Err(_) => { - return Err(crate::errors::builder(TLSError::General(String::from( + return Err(crate::errors::builder(tokio_rustls::rustls::Error::General(String::from( "Invalid identity PEM file", )))) } } } - if let (Some(sk), false) = (sk.pop(), certs.is_empty()) { (sk, certs) } else { - return Err(crate::errors::builder(TLSError::General(String::from( + return Err(crate::errors::builder(tokio_rustls::rustls::Error::General(String::from( "private key or certificate not found", )))); } }; - Ok(Identity { inner: ClientCert::RustlsPem { key, certs }, }) @@ -255,28 +382,23 @@ impl Identity { #[cfg(not(feature = "rustls"))] { // For custom TLS backend, store the PEM data - Ok(Identity { + return Ok(Identity { inner: ClientCert::CustomPem { pem_data: buf.to_vec(), }, - }) + }); } } #[cfg(feature = "rustls")] pub(crate) fn add_to_tls( self, - config_builder: rustls::ConfigBuilder, - ) -> crate::Result { - match self.inner { - ClientCert::RustlsPem { key, certs } => config_builder - .with_client_auth_cert(certs, key) - .map_err(crate::errors::builder), - #[cfg(not(feature = "rustls"))] - ClientCert::CustomPem { .. } => Err(crate::errors::builder( - "Cannot use custom identity with rustls without proper conversion", - )), - } + config_builder: tokio_rustls::rustls::ConfigBuilder, + ) -> crate::Result { + let ClientCert::RustlsPem { key, certs } = self.inner; + config_builder + .with_client_auth_cert(certs, key) + .map_err(crate::errors::builder) } } /// A TLS protocol version. @@ -300,133 +422,4 @@ impl Version { pub const TLS_1_2: Version = Version(InnerVersion::Tls1_2); /// Version 1.3 of the TLS protocol. pub const TLS_1_3: Version = Version(InnerVersion::Tls1_3); - - #[cfg(feature = "rustls")] - pub(crate) fn from_tls(version: rustls::ProtocolVersion) -> Option { - match version { - rustls::ProtocolVersion::SSLv2 => None, - rustls::ProtocolVersion::SSLv3 => None, - rustls::ProtocolVersion::TLSv1_0 => Some(Self(InnerVersion::Tls1_0)), - rustls::ProtocolVersion::TLSv1_1 => Some(Self(InnerVersion::Tls1_1)), - rustls::ProtocolVersion::TLSv1_2 => Some(Self(InnerVersion::Tls1_2)), - rustls::ProtocolVersion::TLSv1_3 => Some(Self(InnerVersion::Tls1_3)), - _ => None, - } - } -} - -#[cfg(feature = "rustls")] -#[derive(Debug)] -pub(crate) struct NoVerifier; - -#[cfg(feature = "rustls")] -impl ServerCertVerifier for NoVerifier { - fn verify_server_cert( - &self, - _end_entity: &rustls_pki_types::CertificateDer, - _intermediates: &[rustls_pki_types::CertificateDer], - _server_name: &ServerName, - _ocsp_response: &[u8], - _now: UnixTime, - ) -> Result { - Ok(ServerCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - _message: &[u8], - _cert: &rustls_pki_types::CertificateDer, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } - - fn verify_tls13_signature( - &self, - _message: &[u8], - _cert: &rustls_pki_types::CertificateDer, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } - - fn supported_verify_schemes(&self) -> Vec { - vec![ - SignatureScheme::RSA_PKCS1_SHA1, - SignatureScheme::ECDSA_SHA1_Legacy, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - SignatureScheme::ECDSA_NISTP521_SHA512, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::ED25519, - SignatureScheme::ED448, - ] - } -} - -#[cfg(feature = "rustls")] -#[derive(Debug)] -pub(crate) struct IgnoreHostname { - roots: RootCertStore, - signature_algorithms: WebPkiSupportedAlgorithms, -} - -#[cfg(feature = "rustls")] -impl IgnoreHostname { - pub(crate) fn new(roots: RootCertStore, signature_algorithms: WebPkiSupportedAlgorithms) -> Self { - Self { - roots, - signature_algorithms, - } - } -} - -#[cfg(feature = "rustls")] -impl ServerCertVerifier for IgnoreHostname { - fn verify_server_cert( - &self, - end_entity: &rustls_pki_types::CertificateDer<'_>, - intermediates: &[rustls_pki_types::CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp_response: &[u8], - now: UnixTime, - ) -> Result { - let cert = ParsedCertificate::try_from(end_entity)?; - - rustls::client::verify_server_cert_signed_by_trust_anchor( - &cert, - &self.roots, - intermediates, - now, - self.signature_algorithms.all, - )?; - Ok(ServerCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &rustls_pki_types::CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - rustls::crypto::verify_tls12_signature(message, cert, dss, &self.signature_algorithms) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &rustls_pki_types::CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - rustls::crypto::verify_tls13_signature(message, cert, dss, &self.signature_algorithms) - } - - fn supported_verify_schemes(&self) -> Vec { - self.signature_algorithms.supported_schemes() - } } diff --git a/slinger/src/tls/rustls.rs b/slinger/src/tls/rustls.rs new file mode 100644 index 0000000..0a9abfc --- /dev/null +++ b/slinger/src/tls/rustls.rs @@ -0,0 +1,212 @@ +//! rustls backend +use crate::tls::{CustomTlsConnector, CustomTlsStream, InnerVersion, PeerCertificate, Version}; +use crate::{impl_tls_stream, Socket, StreamWrapper}; +use std::time::Duration; +use tokio_rustls::rustls::{ + self, + client::danger::HandshakeSignatureValid, + client::danger::ServerCertVerified, + client::danger::ServerCertVerifier, + crypto::WebPkiSupportedAlgorithms, + pki_types::{ServerName, UnixTime}, + server::ParsedCertificate, + DigitallySignedStruct, RootCertStore, SignatureScheme, +}; + +#[derive(Debug)] +pub(crate) struct NoVerifier; + +impl ServerCertVerifier for NoVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls_pki_types::CertificateDer, + _intermediates: &[rustls_pki_types::CertificateDer], + _server_name: &ServerName, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls_pki_types::CertificateDer, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls_pki_types::CertificateDer, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } +} + +#[derive(Debug)] +pub(crate) struct IgnoreHostname { + roots: RootCertStore, + signature_algorithms: WebPkiSupportedAlgorithms, +} + +impl IgnoreHostname { + pub(crate) fn new(roots: RootCertStore, signature_algorithms: WebPkiSupportedAlgorithms) -> Self { + Self { + roots, + signature_algorithms, + } + } +} + +impl ServerCertVerifier for IgnoreHostname { + fn verify_server_cert( + &self, + end_entity: &rustls_pki_types::CertificateDer<'_>, + intermediates: &[rustls_pki_types::CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + let cert = ParsedCertificate::try_from(end_entity)?; + + rustls::client::verify_server_cert_signed_by_trust_anchor( + &cert, + &self.roots, + intermediates, + now, + self.signature_algorithms.all, + )?; + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &rustls_pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature(message, cert, dss, &self.signature_algorithms) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &rustls_pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.signature_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.signature_algorithms.supported_schemes() + } +} + +/// enable rustls feature +pub struct RustTlsStream { + inner: tokio_rustls::client::TlsStream, +} +impl_tls_stream!(RustTlsStream, inner); + +/// Default rustls implementation of CustomTlsConnector +/// This struct wraps tokio_rustls::TlsConnector and implements the CustomTlsConnector trait, +/// providing a unified interface for TLS connections when the rustls feature is enabled. +pub struct RustlsTlsConnector { + connector: tokio_rustls::TlsConnector, + connect_timeout: Option, +} + +impl CustomTlsStream for RustTlsStream { + fn peer_certificate(&self) -> Option> { + self.inner.get_ref().1.peer_certificates().map(|certs| { + certs + .iter() + .map(|cert| PeerCertificate { + inner: cert.to_vec(), + }) + .collect() + }) + } + fn alpn_protocol(&self) -> Option> { + self.inner.get_ref().1.alpn_protocol().map(|p| p.to_vec()) + } +} + +impl RustlsTlsConnector { + /// Create a new RustlsTlsConnector + pub fn new(connector: tokio_rustls::TlsConnector, connect_timeout: Option) -> Self { + Self { + connector, + connect_timeout, + } + } +} + +impl CustomTlsConnector for RustlsTlsConnector { + fn connect<'a>( + &'a self, + domain: &'a str, + stream: Socket, + ) -> std::pin::Pin> + Send + 'a>> { + Box::pin(async move { + let domain = ServerName::try_from(domain.to_owned()) + .map_err(|e| crate::errors::Error::Other(e.to_string()))?; + let connect_timeout = self.connect_timeout.unwrap_or(Duration::from_secs(30)); + match stream.inner { + StreamWrapper::Tcp(t) => { + let s = tokio::time::timeout(connect_timeout, self.connector.connect(domain, t)) + .await + .map_err(|e| crate::errors::new_io_error(std::io::ErrorKind::TimedOut, &e.to_string()))? + .map_err(|e| crate::errors::Error::Other(format!("rustls handshake failed: {}", e)))?; + let rust_stream = RustTlsStream { inner: s }; + let tls = Socket::new( + StreamWrapper::Custom(Box::new(rust_stream)), + stream.read_timeout, + stream.write_timeout, + ); + Ok(tls) + } + StreamWrapper::Custom(t) => Ok(Socket::new( + StreamWrapper::Custom(t), + stream.read_timeout, + stream.write_timeout, + )), + } + }) + } +} +impl Version { + pub(crate) fn from_tls(version: rustls::ProtocolVersion) -> Option { + match version { + rustls::ProtocolVersion::SSLv2 => None, + rustls::ProtocolVersion::SSLv3 => None, + rustls::ProtocolVersion::TLSv1_0 => Some(Self(InnerVersion::Tls1_0)), + rustls::ProtocolVersion::TLSv1_1 => Some(Self(InnerVersion::Tls1_1)), + rustls::ProtocolVersion::TLSv1_2 => Some(Self(InnerVersion::Tls1_2)), + rustls::ProtocolVersion::TLSv1_3 => Some(Self(InnerVersion::Tls1_3)), + _ => None, + } + } +} diff --git a/slinger/tests/tests.rs b/slinger/tests/tests.rs new file mode 100644 index 0000000..5fb6b68 --- /dev/null +++ b/slinger/tests/tests.rs @@ -0,0 +1,6 @@ +#[cfg(test)] +mod tests { + + #[test] + fn it_works() {} +}