diff --git a/Cargo.toml b/Cargo.toml index 2afd2ed..b4795de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slinger" #改这个 -version = "0.2.8" +version = "0.2.9" edition = "2021" description = "An HTTP Client for Rust designed for hackers." homepage = "https://github.com/emo-crab/slinger" @@ -32,9 +32,9 @@ opt-level = 3 [dependencies] thiserror = "2" -http = { version = "1.1.0" } +http = { version = "1.3.1" } percent-encoding = "2.1" -socket2 = "0.6.0" +socket2 = "0.6.1" bytes = "1.6.1" base64 = "0.22" flate2 = { version = "1.0", default-features = false, features = ["default"], optional = true } @@ -49,15 +49,22 @@ 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.3", optional = true, features = ["bytes1"] } +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"] -tls = ["tokio-rustls", "rustls-pemfile", "rustls-pki-types", "rustls-native-certs"] cookie = ["dep:cookie"] charset = ["dep:encoding_rs", "dep:mime"] serde = ["dep:serde", "http-serde"] diff --git a/README.md b/README.md index 7784c4d..39a85bd 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ This example enables some optional features, so your `Cargo.toml` could look lik ```toml [dependencies] -slinger = { version = "0.2.2", features = ["serde", "cookie", "charset", "tls", "gzip"] } +slinger = { version = "0.2.2", features = ["serde", "cookie", "charset", "tls", "rustls", "gzip"] } ``` And then the code: @@ -89,6 +89,41 @@ async fn main() -> Result<(), Box> { } ``` + + +## Features + +Slinger supports the following optional features: + +- `tls` - Base TLS feature (enables TLS types and interfaces without a specific backend) +- `rustls` - HTTPS support using Rustls (requires `tls`, pure Rust implementation) +- `http2` - HTTP/2 protocol support (requires a TLS backend) +- `cookie` - Cookie handling support +- `charset` - Character encoding support +- `serde` - Serialization/deserialization support +- `gzip` - Gzip compression support +- `schema` - JSON Schema support + +### TLS Backend Selection + +To use TLS, you must: +1. Enable the `tls` feature +2. Choose the `rustls` backend, OR provide a custom TLS connector + +Example feature combinations: +```toml +# Using rustls backend +slinger = { version = "0.2.8", features = ["tls", "rustls"] } + +# Using custom TLS backend (requires implementing CustomTlsConnector) +slinger = { version = "0.2.8", features = ["tls"] } +``` + +### Custom TLS Backend (e.g., native-tls, OpenSSL) + +If you want to use native-tls, OpenSSL, or other TLS libraries, you can implement a custom TLS connector. +See the [native_tls_example.rs](examples/native_tls_example.rs) for a complete example of how to integrate native-tls. + ## Example @@ -116,7 +151,7 @@ X: GET http://192.168.83.1:8080/admin.jsp HTTP/1.0 #[tokio::main] async fn main() -> Result<(), Box> { // let proxy = slinger::Proxy::parse("http://127.0.0.1:8080").unwrap(); - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); let mut raw = Vec::new(); // replace \n to \r\n for line in RAW.lines() { diff --git a/examples/async.rs b/examples/async.rs index e50e833..3fcd116 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -4,7 +4,7 @@ use futures::StreamExt; #[tokio::main] async fn main() -> Result<(), Box> { use slinger::ClientBuilder; - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); let mut worker = FuturesUnordered::new(); let start = std::time::Instant::now(); let mut t = vec![1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter(); diff --git a/examples/base.rs b/examples/base.rs index 382d8ac..666f6db 100644 --- a/examples/base.rs +++ b/examples/base.rs @@ -3,7 +3,7 @@ use slinger::{ClientBuilder, Request}; #[tokio::main] async fn main() -> Result<(), Box> { - let client = ClientBuilder::new() + let client = ClientBuilder::default() .user_agent(HeaderValue::from_static( "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0", )) diff --git a/examples/certificate.rs b/examples/certificate.rs index 146533d..07359c7 100644 --- a/examples/certificate.rs +++ b/examples/certificate.rs @@ -4,7 +4,7 @@ async fn main() -> Result<(), Box> { { use slinger::tls::PeerCertificate; use slinger::ClientBuilder; - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); let resp = client.get("https://httpbin.org/get").send().await?; let certificate = resp.extensions().get::().unwrap(); println!("{:?}", certificate); diff --git a/examples/cookies.rs b/examples/cookies.rs index d203aff..6b3b2c7 100644 --- a/examples/cookies.rs +++ b/examples/cookies.rs @@ -3,7 +3,7 @@ async fn main() -> Result<(), Box> { #[cfg(feature = "cookie")] { use slinger::ClientBuilder; - let client = ClientBuilder::new().cookie_store(true).build().unwrap(); + let client = ClientBuilder::default().cookie_store(true).build().unwrap(); let resp = client .get("http://httpbin.org/cookies/set/key/value") .send() diff --git a/examples/custom_connector_builder.rs b/examples/custom_connector_builder.rs new file mode 100644 index 0000000..c11491f --- /dev/null +++ b/examples/custom_connector_builder.rs @@ -0,0 +1,46 @@ +//! Example demonstrating how to use a custom ConnectorBuilder with ClientBuilder +//! +//! This example shows how to customize the connector configuration by providing +//! a custom ConnectorBuilder instead of using the default one. + +use slinger::{Client, ConnectorBuilder}; +use std::time::Duration; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("Custom ConnectorBuilder Example"); + println!("================================\n"); + + // Create a custom ConnectorBuilder with specific settings + let custom_connector = ConnectorBuilder::default() + .connect_timeout(Some(Duration::from_secs(5))) + .read_timeout(Some(Duration::from_secs(15))) + .write_timeout(Some(Duration::from_secs(15))) + .nodelay(true); + + println!("Creating client with custom connector builder..."); + + // Build a client using the custom connector builder + let _client = Client::builder() + .connector_builder(custom_connector) + .keepalive(true) + .build()?; + + println!("✓ Client created successfully with custom connector configuration"); + + println!("\nThis demonstrates:"); + println!("1. Creating a custom ConnectorBuilder with specific timeouts"); + println!("2. Using .connector_builder() to set the custom connector"); + println!("3. Building the client with both custom connector and client settings"); + + println!("\nNote: When using a custom ConnectorBuilder, you have full control"); + println!("over the connector configuration, including timeouts, TCP settings,"); + println!("and TLS options (when the tls feature is enabled)."); + + // For demonstration, we won't make an actual HTTP request + // In a real scenario, you would use the client like this: + // let response = client.get("http://httpbin.org/get").send().await?; + // println!("Response status: {}", response.status_code()); + + Ok(()) +} diff --git a/examples/custom_tls.rs b/examples/custom_tls.rs new file mode 100644 index 0000000..b31f070 --- /dev/null +++ b/examples/custom_tls.rs @@ -0,0 +1,198 @@ +//! 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. +//! +//! 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")))] +use std::pin::Pin; +#[cfg(all(feature = "tls", not(feature = "rustls")))] +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")))] +/// 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, +} + +#[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(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(), + }) + } +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +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")))] +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) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) + } +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +/// Example custom TLS connector implementation +/// +/// In a real implementation, you would: +/// 1. Take the TCP socket from the provided Socket +/// 2. Perform TLS handshake using your preferred TLS library (openssl, boringssl, etc.) +/// 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")))] +impl CustomTlsConnector for MyCustomTlsConnector { + fn connect<'a>( + &'a self, + domain: &'a str, + stream: Socket, + ) -> std::pin::Pin> + Send + 'a>> { + Box::pin(async move { + println!("Performing mock TLS handshake for domain: {}", domain); + + // Extract the TCP stream from the socket + let tcp_stream = match stream.inner { + MaybeTlsStream::Tcp(tcp) => tcp, + _ => { + return Err(slinger::Error::Other( + "Expected plain TCP stream for TLS upgrade".to_string(), + )); + } + }; + + // In a real implementation, you would: + // 1. Perform TLS handshake using your TLS library + // 2. Verify the server's certificate + // 3. Establish encrypted communication + + // Create our mock TLS stream + let tls_stream = MockTlsStream::new(tcp_stream); + + println!("✓ Mock TLS handshake completed successfully"); + + // Wrap in a Socket and return + Ok(Socket::new( + MaybeTlsStream::Custom(Box::new(tls_stream)), + stream.read_timeout, + stream.write_timeout, + )) + }) + } +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[tokio::main] +async fn main() -> std::result::Result<(), Box> { + println!("Custom TLS Connector Example"); + println!("============================\n"); + + // Create a custom TLS connector + let custom_connector = Arc::new(MyCustomTlsConnector); + + // Build a connector with the custom TLS implementation + let _connector = ConnectorBuilder::default() + .custom_tls_connector(custom_connector) + .build()?; + + println!("✓ Successfully created connector with custom TLS backend"); + + println!("\nThis example demonstrates:"); + println!("1. Implementing CustomTlsStream trait for a mock TLS stream"); + println!("2. Implementing CustomTlsConnector trait for custom TLS handshake"); + println!("3. Extracting the TCP stream from a Socket"); + println!("4. Wrapping the TLS stream back into a Socket"); + println!("5. Providing peer certificate information via peer_certificate()"); + println!("6. Providing access to underlying TcpStream via get_ref()"); + + println!("\nIn a full implementation, you would:"); + println!(" - Use a real TLS library (openssl, boringssl, etc.)"); + println!(" - Perform actual cryptographic handshake"); + println!(" - Verify server certificates"); + println!(" - Encrypt/decrypt data in AsyncRead/AsyncWrite implementations"); + + println!("\nExample libraries you could use:"); + println!(" - openssl: Use openssl-sys or tokio-openssl"); + println!(" - boring: Use boring (BoringSSL bindings)"); + println!(" - native-tls: See native_tls_example.rs"); + println!(" - Any other TLS library with async support"); + + Ok(()) +} + +#[cfg(not(all(feature = "tls", not(feature = "rustls"))))] +fn main() { + eprintln!("This example requires the 'tls' feature without 'rustls'."); + eprintln!("Run with: cargo run --example custom_tls --features tls"); +} diff --git a/examples/gzip.rs b/examples/gzip.rs index 33f9a45..26109da 100644 --- a/examples/gzip.rs +++ b/examples/gzip.rs @@ -3,7 +3,7 @@ async fn main() -> Result<(), Box> { #[cfg(feature = "gzip")] { use slinger::ClientBuilder; - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); let resp = client.get("http://httpbin.org/gzip").send().await?; println!("{:?}", resp.text()); } diff --git a/examples/http2.rs b/examples/http2.rs index d94ba9c..b4eac9b 100644 --- a/examples/http2.rs +++ b/examples/http2.rs @@ -15,7 +15,7 @@ async fn main() -> Result<(), Box> { use slinger::ClientBuilder; // Example 2: HTTP/2 with standard TLS (rustls) println!("=== HTTP/2 with Rustls TLS ==="); - let client = ClientBuilder::new().enable_http2(true).build()?; + let client = ClientBuilder::default().enable_http2(true).build()?; // Note: Rustls doesn't support HTTP/2 ALPN by default in this configuration // This will likely fall back to HTTP/1.1 let resp = client.get("https://httpbin.org/").send().await?; diff --git a/examples/http_record.rs b/examples/http_record.rs index 3729e73..47af3a8 100644 --- a/examples/http_record.rs +++ b/examples/http_record.rs @@ -2,7 +2,7 @@ use slinger::ClientBuilder; #[tokio::main] async fn main() -> Result<(), Box> { - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); let resp = client .get("https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget") .send() diff --git a/examples/native_tls_example.rs b/examples/native_tls_example.rs new file mode 100644 index 0000000..2719f10 --- /dev/null +++ b/examples/native_tls_example.rs @@ -0,0 +1,220 @@ +//! 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. +//! +//! To run this example: +//! ```bash +//! cargo run --example native_tls_example --features tls +//! ``` +//! +//! Note: This example requires the `tls` feature without `rustls`. +//! 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")))] +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")))] +/// Wrapper for tokio-native-tls TlsStream that implements CustomTlsStream +#[derive(Debug)] +struct NativeTlsStream { + inner: tokio_native_tls::TlsStream, +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +impl NativeTlsStream { + fn new(stream: tokio_native_tls::TlsStream) -> Self { + Self { inner: stream } + } +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +impl CustomTlsStream for NativeTlsStream { + fn peer_certificate(&self) -> Option { + 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) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) + } +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +/// Native-TLS connector implementation +/// +/// This demonstrates how to wrap tokio-native-tls to work with slinger's +/// CustomTlsConnector trait. +struct NativeTlsConnector { + connector: tokio_native_tls::TlsConnector, +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +impl NativeTlsConnector { + fn new() -> std::result::Result> { + let mut builder = tokio_native_tls::native_tls::TlsConnector::builder(); + + // Configure certificate verification + builder.danger_accept_invalid_certs(false); + builder.danger_accept_invalid_hostnames(false); + + // Optional: Configure TLS version + builder.min_protocol_version(Some(tokio_native_tls::native_tls::Protocol::Tlsv12)); + + // Optional: Add custom certificates + // let cert = tokio_native_tls::native_tls::Certificate::from_pem(pem_bytes)?; + // builder.add_root_certificate(cert); + + // Build the connector + let connector = builder.build()?; + + Ok(Self { + connector: tokio_native_tls::TlsConnector::from(connector), + }) + } +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +impl CustomTlsConnector for NativeTlsConnector { + fn connect<'a>( + &'a self, + domain: &'a str, + stream: Socket, + ) -> std::pin::Pin> + Send + 'a>> { + let connector = self.connector.clone(); + let domain = domain.to_string(); + + Box::pin(async move { + // Extract the TCP stream from the socket + let tcp_stream = match stream.inner { + MaybeTlsStream::Tcp(tcp) => tcp, + _ => { + return Err(slinger::Error::Other( + "Expected plain TCP stream for TLS upgrade".to_string(), + )); + } + }; + + // Perform TLS handshake + let tls_stream = connector + .connect(&domain, tcp_stream) + .await + .map_err(|e| slinger::Error::Other(format!("native-tls handshake failed: {}", e)))?; + + // Wrap in our custom stream type + let custom_stream = NativeTlsStream::new(tls_stream); + + // Create a new socket with the TLS stream + Ok(Socket::new( + MaybeTlsStream::Custom(Box::new(custom_stream)), + stream.read_timeout, + stream.write_timeout, + )) + }) + } +} + +#[cfg(all(feature = "tls", not(feature = "rustls")))] +#[tokio::main] +async fn main() -> std::result::Result<(), Box> { + println!("Native-TLS Custom Connector Example"); + println!("====================================\n"); + + // Create the native-tls connector + let native_tls = Arc::new(NativeTlsConnector::new()?); + println!("✓ Created native-tls connector"); + + // Build a connector with the native-tls implementation + let connector = ConnectorBuilder::default() + .custom_tls_connector(native_tls) + .build()?; + + println!("✓ Built connector with native-tls backend"); + + // Test the connection + println!("\nTesting connection to example.com..."); + let socket = connector + .connect_with_uri(&http::Uri::from_static("https://example.com")) + .await?; + + println!("✓ Successfully connected to example.com via HTTPS"); + + // Try to get peer certificate + if let Some(cert) = socket.inner.peer_certificate() { + println!("✓ Retrieved peer certificate ({} bytes)", cert.len()); + } else { + println!("✗ No peer certificate available"); + } + + 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!("\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"); + + Ok(()) +} + +#[cfg(not(all(feature = "tls", not(feature = "rustls"))))] +fn main() { + eprintln!("This example requires the 'tls' feature without 'rustls'."); + eprintln!("Run with: cargo run --example native_tls_example --features tls"); +} diff --git a/examples/proxy.rs b/examples/proxy.rs index da88875..479efc6 100644 --- a/examples/proxy.rs +++ b/examples/proxy.rs @@ -4,11 +4,14 @@ use slinger::ClientBuilder; #[tokio::main] async fn main() -> Result<(), Box> { let socks5_proxy = slinger::Proxy::parse("socks5://127.0.0.1:7897").unwrap(); - let client = ClientBuilder::new().proxy(socks5_proxy).build().unwrap(); + let client = ClientBuilder::default() + .proxy(socks5_proxy) + .build() + .unwrap(); let resp = client.get("https://httpbin.org/get").send().await?; println!("{:?}", resp.text()); let http_proxy = slinger::Proxy::parse("http://127.0.0.1:7897").unwrap(); - let client = ClientBuilder::new().proxy(http_proxy).build().unwrap(); + let client = ClientBuilder::default().proxy(http_proxy).build().unwrap(); let resp = client.get("https://httpbin.org/get").send().await?; println!("{:?}", resp.text()); Ok(()) diff --git a/examples/redirect.rs b/examples/redirect.rs index 3ba60eb..79265b4 100644 --- a/examples/redirect.rs +++ b/examples/redirect.rs @@ -30,7 +30,7 @@ fn custom(attempt: slinger::redirect::Attempt) -> slinger::redirect::Action { async fn customize() -> Result<(), Box> { let redirect = slinger::redirect::Policy::Custom(custom); - let client = ClientBuilder::new().redirect(redirect).build().unwrap(); + let client = ClientBuilder::default().redirect(redirect).build().unwrap(); let resp = client .get("http://httpbin.org/base64/c2xpbmdlcgo%3D") .send() @@ -40,7 +40,7 @@ async fn customize() -> Result<(), Box> { } async fn limit(max_redirect: usize) -> Result<(), Box> { let redirect = slinger::redirect::Policy::Limit(max_redirect); - let client = ClientBuilder::new().redirect(redirect).build().unwrap(); + let client = ClientBuilder::default().redirect(redirect).build().unwrap(); let resp = client.get("http://httpbin.org/redirect/10").send().await?; let record = resp.extensions().get::>().unwrap(); assert_eq!(record.len(), 3); @@ -48,7 +48,7 @@ async fn limit(max_redirect: usize) -> Result<(), Box> { } async fn jump() -> Result<(), Box> { - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); let resp = client .get("http://httpbin.org/redirect-to?url=http://www.example.com/") .send() @@ -61,7 +61,7 @@ async fn jump() -> Result<(), Box> { async fn only_same_host() -> Result<(), Box> { let redirect = slinger::redirect::Policy::Custom(slinger::redirect::only_same_host); - let client = ClientBuilder::new().redirect(redirect).build().unwrap(); + let client = ClientBuilder::default().redirect(redirect).build().unwrap(); let resp = client .get("http://httpbin.org/redirect-to?url=http://www.example.com/") .send() diff --git a/examples/request_budiler.rs b/examples/request_budiler.rs index 752d5d7..b4b67be 100644 --- a/examples/request_budiler.rs +++ b/examples/request_budiler.rs @@ -2,9 +2,9 @@ use slinger::{ClientBuilder, Request}; #[tokio::main] async fn main() -> Result<(), Box> { - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); let resp = client - .post("https://httpbin.org/post") + .post("http://httpbin.org/post") .header("XX", "XX") .header_line("X: X") .body(b"data".as_slice()) diff --git a/examples/smuggling.rs b/examples/smuggling.rs index fd1d4b5..c519aa0 100644 --- a/examples/smuggling.rs +++ b/examples/smuggling.rs @@ -23,7 +23,7 @@ X: GET http://192.168.83.1:8080/admin.jsp HTTP/1.0 "#; // let proxy = slinger::Proxy::parse("http://127.0.0.1:8080").unwrap(); - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); // replace \n to \r\n let raw = raw.replace('\n', "\r\n"); let resp = client @@ -47,7 +47,7 @@ Host: notlocalhost "#; - let client = ClientBuilder::new().build().unwrap(); + let client = ClientBuilder::default().build().unwrap(); // replace \n to \r\n let raw = raw.replace('\n', "\r\n"); let resp = client diff --git a/examples/tls.rs b/examples/tls.rs index 3083fcd..cb665ab 100644 --- a/examples/tls.rs +++ b/examples/tls.rs @@ -25,7 +25,7 @@ async fn main() -> Result<(), Box> { "https://dh1024.badssl.com/", ]; - let client = ClientBuilder::new() + let client = ClientBuilder::default() .min_tls_version(Some(tls::Version::TLS_1_0)) .build() .unwrap(); diff --git a/src/client.rs b/src/client.rs index f3b6354..f8f9f5b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -9,7 +9,7 @@ use std::time::Duration; use crate::cookies; use crate::errors::{new_io_error, Result}; use crate::proxy::Proxy; -use crate::record::{HTTPRecord, LocalPeerRecord, RedirectRecord}; +use crate::record::{HTTPRecord, RedirectRecord}; use crate::redirect::{remove_sensitive_headers, Action, Policy}; use crate::response::{ResponseBuilder, ResponseConfig}; use crate::socket::Socket; @@ -65,13 +65,13 @@ impl Client { /// See docs /// on [`slinger`][Client] for details. pub fn new() -> Client { - ClientBuilder::new().build().expect("Client::new()") + ClientBuilder::default().build().expect("Client::new()") } /// Creates a `ClientBuilder` to configure a `Client`. /// - /// This is the same as `ClientBuilder::new()`. + /// This is the same as `ClientBuilder::default()`. pub fn builder() -> ClientBuilder { - ClientBuilder::new() + ClientBuilder::default() } /// Convenience method to make a `GET` request to a URL. /// @@ -345,20 +345,12 @@ impl Client { }; let (mut response, socket) = self.execute_request(socket, &request).await?; response.extensions_mut().insert(request.clone()); - if let Some(socket) = socket { - if let (Ok(remote_addr), Ok(local_addr)) = (socket.peer_addr(), socket.local_addr()) { - response.extensions_mut().insert(LocalPeerRecord { - remote_addr: remote_addr.into(), - local_addr: local_addr.into(), - }); - } - }; 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 s.peer_addr().is_err() { - *s = self.inner.connector.connect_with_uri(&cur_uri).await?; + if let Some(socket) = socket { + *s = socket; } } if !self.inner.keepalive { @@ -519,26 +511,12 @@ fn make_referer(next: &http::Uri, previous: &http::Uri) -> Option { /// # } /// ``` #[must_use] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct ClientBuilder { config: Config, } -impl Default for ClientBuilder { - fn default() -> Self { - Self::new() - } -} - impl ClientBuilder { - /// Constructs a new `ClientBuilder`. - /// - /// This is the same as `Client::builder()`. - pub fn new() -> ClientBuilder { - ClientBuilder { - config: Config::default(), - } - } /// Returns a `Client` that uses this `ClientBuilder` configuration. /// /// # Errors @@ -552,29 +530,36 @@ impl ClientBuilder { /// [`slinger::client`][Client] for details. pub fn build(self) -> Result { let config = self.config; - let mut connector_builder = ConnectorBuilder::default() - .proxy(config.proxy) - .read_timeout(config.read_timeout) - .connect_timeout(config.connect_timeout) - .write_timeout(config.timeout); - connector_builder = connector_builder.nodelay(config.nodelay); - #[cfg(feature = "tls")] - { - connector_builder = connector_builder - .hostname_verification(config.hostname_verification) - .certs_verification(config.certs_verification) - .certificate(config.root_certs) - .tls_sni(config.tls_sni) - .min_tls_version(config.min_tls_version) - .max_tls_version(config.max_tls_version); - if let Some(identity) = config.identity { - connector_builder = connector_builder.identity(identity); + 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) + .read_timeout(config.read_timeout) + .connect_timeout(config.connect_timeout) + .write_timeout(config.timeout); + default_builder = default_builder.nodelay(config.nodelay); + #[cfg(feature = "tls")] + { + default_builder = default_builder + .hostname_verification(config.hostname_verification) + .certs_verification(config.certs_verification) + .certificate(config.root_certs) + .tls_sni(config.tls_sni) + .min_tls_version(config.min_tls_version) + .max_tls_version(config.max_tls_version); + if let Some(identity) = config.identity { + default_builder = default_builder.identity(identity); + } } - } - #[cfg(feature = "http2")] - { - connector_builder = connector_builder.enable_http2(config.http2); - } + #[cfg(feature = "http2")] + { + default_builder = default_builder.enable_http2(config.http2); + } + default_builder + }; let connector = connector_builder.build()?; Ok(Client { inner: Arc::new(ClientRef { @@ -876,6 +861,53 @@ impl ClientBuilder { self.config.http2 = http2; self } + /// Set a custom `ConnectorBuilder` for the client. + /// + /// This allows you to fully customize the connector configuration. + /// When a custom connector builder is provided, it will be used directly + /// instead of the default one. + /// + /// # Important + /// + /// When using this method, ALL connector-related settings configured through + /// `ClientBuilder` methods will be ignored, including: + /// - `tcp_nodelay()` + /// - `add_root_certificate()` + /// - `identity()` + /// - `danger_accept_invalid_hostnames()` + /// - `danger_accept_invalid_certs()` + /// - `tls_sni()` + /// - `min_tls_version()` and `max_tls_version()` + /// - `connect_timeout()`, `read_timeout()`, and `timeout()` for the connector + /// - `proxy()` for the connector + /// - `enable_http2()` for the connector + /// + /// You must configure all these settings directly on the `ConnectorBuilder` instead. + /// Client-level settings like `keepalive()`, `cookie_store()`, `redirect()`, etc. + /// will still be respected. + /// + /// # Example + /// + /// ```rust + /// use slinger::{Client, ConnectorBuilder}; + /// use std::time::Duration; + /// + /// # fn doc() -> Result<(), slinger::Error> { + /// let custom_connector = ConnectorBuilder::default() + /// .connect_timeout(Some(Duration::from_secs(5))) + /// .read_timeout(Some(Duration::from_secs(10))) + /// .nodelay(true); + /// + /// let client = Client::builder() + /// .connector_builder(custom_connector) + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn connector_builder(mut self, connector_builder: ConnectorBuilder) -> ClientBuilder { + self.config.connector_builder = Some(connector_builder); + self + } } #[derive(Clone)] struct Config { @@ -904,6 +936,7 @@ struct Config { redirect_policy: Policy, #[cfg(feature = "cookie")] cookie_store: Option>, + connector_builder: Option, } impl Debug for Config { @@ -950,6 +983,7 @@ impl Default for Config { redirect_policy: Policy::Limit(10), #[cfg(feature = "cookie")] cookie_store: None, + connector_builder: None, } } } diff --git a/src/connector.rs b/src/connector.rs index 389e255..6a00f28 100644 --- a/src/connector.rs +++ b/src/connector.rs @@ -2,7 +2,9 @@ use crate::errors::Result; use crate::proxy::{Proxy, ProxySocket}; use crate::socket::{MaybeTlsStream, Socket}; #[cfg(feature = "tls")] -use crate::tls::{Identity, IgnoreHostname, NoVerifier}; +use crate::tls::Identity; +#[cfg(feature = "rustls")] +use crate::tls::{IgnoreHostname, NoVerifier}; #[cfg(feature = "tls")] use crate::{tls, Certificate}; use socket2::Socket as RawSocket; @@ -10,17 +12,68 @@ use socket2::{Domain, Protocol, Type}; use std::net::SocketAddr; use std::time::Duration; use tokio::net::TcpSocket; -#[cfg(feature = "tls")] +#[cfg(feature = "rustls")] use tokio_rustls::rustls; -#[cfg(feature = "tls")] +#[cfg(feature = "rustls")] use tokio_rustls::rustls::pki_types::ServerName; -/// TLS connector type enum to support both rustls and native-tls in the future +/// 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 @@ -46,6 +99,8 @@ pub struct ConnectorBuilder { #[cfg(feature = "tls")] certificate: Vec, proxy: Option, + #[cfg(all(feature = "tls", not(feature = "rustls")))] + custom_tls_connector: Option>, } impl Default for ConnectorBuilder { @@ -71,6 +126,8 @@ impl Default for ConnectorBuilder { #[cfg(feature = "tls")] certificate: vec![], proxy: None, + #[cfg(all(feature = "tls", not(feature = "rustls")))] + custom_tls_connector: None, } } } @@ -193,8 +250,7 @@ impl ConnectorBuilder { /// /// # Optional /// - /// This requires the optional `tls` - /// feature to be enabled. + /// 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; @@ -206,17 +262,57 @@ impl ConnectorBuilder { /// /// # Optional /// - /// This requires the optional `tls` - /// feature to be enabled. + /// 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 } + + /// 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. + /// + /// # Example + /// + /// ```ignore + /// use slinger::connector::{ConnectorBuilder, CustomTlsConnector}; + /// use slinger::{Result, Socket}; + /// use std::sync::Arc; + /// + /// struct MyTlsConnector; + /// + /// impl CustomTlsConnector for MyTlsConnector { + /// fn connect<'a>( + /// &'a self, + /// stream: Socket, + /// domain: &'a str, + /// ) -> std::pin::Pin> + Send + 'a>> { + /// Box::pin(async move { + /// // Your custom TLS handshake logic here + /// todo!() + /// }) + /// } + /// } + /// + /// let connector = ConnectorBuilder::default() + /// .custom_tls_connector(Arc::new(MyTlsConnector)) + /// .build()?; + /// ``` + #[cfg(all(feature = "tls", not(feature = "rustls")))] + pub fn custom_tls_connector( + mut self, + connector: std::sync::Arc, + ) -> ConnectorBuilder { + self.custom_tls_connector = Some(connector); + self + } } impl ConnectorBuilder { - #[cfg(feature = "tls")] + #[cfg(feature = "rustls")] /// Use rustls only fn only_rustls(&self) -> Result { let mut root_cert_store = rustls::RootCertStore::empty(); @@ -290,10 +386,27 @@ impl ConnectorBuilder { 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 = { self.only_rustls()? }; + 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 { + return Err(crate::errors::builder( + "TLS feature enabled without backend: please enable 'rustls' feature, or provide a custom TLS connector using .custom_tls_connector()" + )); + } + } + }; let conn = Connector { connect_timeout: self.connect_timeout, nodelay: self.nodelay, @@ -365,17 +478,48 @@ impl Connector { /// 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 this = connector.clone(); let connect_timeout = self.connect_timeout.unwrap_or(Duration::from_secs(30)); - let tls = stream - .tls(move |t| async move { - tokio::time::timeout(connect_timeout, this.connect(domain, t)).await? - }) - .await?; + 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) } } diff --git a/src/errors.rs b/src/errors.rs index 2e6fbf3..c622a6d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -8,7 +8,7 @@ pub type Result = std::result::Result; #[derive(ThisError, Debug)] pub enum Error { #[error(transparent)] - #[cfg(feature = "tls")] + #[cfg(feature = "rustls")] /// tls Error Tls(#[from] tokio_rustls::rustls::Error), /// Error diff --git a/src/h2_client.rs b/src/h2_client.rs index b8c82b7..0c751cf 100644 --- a/src/h2_client.rs +++ b/src/h2_client.rs @@ -15,8 +15,10 @@ 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 = "tls")] + #[cfg(feature = "rustls")] Tls(Box>), + #[cfg(all(feature = "tls", not(feature = "rustls")))] + Custom(Box), } impl AsyncRead for StreamWrapper { @@ -27,8 +29,10 @@ impl AsyncRead for StreamWrapper { ) -> Poll> { match self.get_mut() { StreamWrapper::Tcp(stream) => Pin::new(stream).poll_read(cx, buf), - #[cfg(feature = "tls")] + #[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), } } } @@ -41,24 +45,30 @@ impl AsyncWrite for StreamWrapper { ) -> Poll> { match self.get_mut() { StreamWrapper::Tcp(stream) => Pin::new(stream).poll_write(cx, buf), - #[cfg(feature = "tls")] + #[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 = "tls")] + #[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 = "tls")] + #[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), } } } @@ -74,8 +84,10 @@ pub(crate) async fn send_h2_request( let io = match socket.inner { MaybeTlsStream::Tcp(tcp_stream) => StreamWrapper::Tcp(tcp_stream), - #[cfg(feature = "tls")] - MaybeTlsStream::Tls(tls_stream) => StreamWrapper::Tls(tls_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 diff --git a/src/lib.rs b/src/lib.rs index 0f7428d..c2a3f82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,7 +81,7 @@ //! async fn run() -> std::result::Result<(), Box> { //! let proxy = slinger::Proxy::parse("http://user:pass@127.0.0.1:1080").unwrap(); //! // let proxy = slinger::Proxy::parse("socks5://user:pass@127.0.0.1:1080").unwrap(); -//! let client = slinger::ClientBuilder::new().proxy(proxy).build().unwrap(); +//! let client = slinger::ClientBuilder::default().proxy(proxy).build().unwrap(); //! let resp = client.get("https://httpbin.org/get").send().await?; //! println!("{:?}", resp); //! Ok(()) @@ -106,9 +106,41 @@ //! //! - **charset**: Improved support for decoding text. //! - **cookie**: Provides cookie session support. -//! - **tls**: Provides https support via rustls. +//! - **tls**: Base TLS feature flag. Enables TLS types and interfaces without a specific backend. +//! - **rustls**: Provides HTTPS support via rustls (requires `tls` feature). //! - **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 +//! - 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.): +//! +//! ```ignore +//! use slinger::{ConnectorBuilder, CustomTlsConnector}; +//! +//! struct MyTlsConnector; +//! +//! impl CustomTlsConnector for MyTlsConnector { +//! fn connect<'a>(&'a self, stream: Socket, domain: &'a str) +//! -> Pin> + Send + 'a>> +//! { +//! // Your TLS implementation here +//! todo!() +//! } +//! } +//! +//! let connector = ConnectorBuilder::default() +//! .custom_tls_connector(Arc::new(MyTlsConnector)) +//! .build()?; +//! ``` //! mod body; mod client; @@ -136,6 +168,8 @@ 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; @@ -148,7 +182,13 @@ pub use http_serde; 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; /// Shortcut method to quickly make a `GET` request. /// diff --git a/src/record.rs b/src/record.rs index 10d23d9..04f21be 100644 --- a/src/record.rs +++ b/src/record.rs @@ -1,15 +1,5 @@ use crate::{Request, Response}; use bytes::Bytes; -use socket2::SockAddr; - -/// http peer_addr and local_addr -#[derive(Clone, Debug)] -pub struct LocalPeerRecord { - /// peer_addr - pub remote_addr: SockAddr, - /// local_addr - pub local_addr: SockAddr, -} /// redirect info #[derive(Clone, Debug)] diff --git a/src/response.rs b/src/response.rs index b15dd9a..cd56b6b 100644 --- a/src/response.rs +++ b/src/response.rs @@ -2,7 +2,7 @@ use crate::body::Body; #[cfg(feature = "cookie")] use crate::cookies; use crate::errors::{new_io_error, Error, Result}; -use crate::record::{HTTPRecord, LocalPeerRecord, RedirectRecord}; +use crate::record::{HTTPRecord, RedirectRecord}; #[cfg(feature = "tls")] use crate::tls::PeerCertificate; use crate::{Request, COLON_SPACE, CR_LF, SPACE}; @@ -359,20 +359,6 @@ impl Response { /// 放一些响应中间过程记录,存起来方便获取 impl Response { - /// Get the remote address used to get this `Response`. - /// - /// # Example - /// - /// ```rust - /// # async fn run() -> Result<(), Box> { - /// let resp = slinger::get("http://httpbin.org/redirect/1").await?; - /// println!("httpbin.org address: {:?}", resp.local_peer_record()); - /// # Ok(()) - /// # } - /// ``` - pub fn local_peer_record(&self) -> Option<&LocalPeerRecord> { - self.extensions().get::() - } /// Get the certificate to get this `Response`. /// /// # Example diff --git a/src/socket.rs b/src/socket.rs index 91ed17d..b11cc17 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -7,19 +7,23 @@ use std::task::{Context, Poll}; use std::time::Duration; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; use tokio::net::TcpStream; -#[cfg(feature = "tls")] -use tokio_rustls::client::TlsStream; +#[cfg(feature = "rustls")] +use tokio_rustls::client::TlsStream as RustlsStream; /// Socket #[derive(Debug)] pub struct Socket { - pub(crate) inner: MaybeTlsStream, - pub(crate) read_timeout: Option, - pub(crate) write_timeout: Option, + /// The underlying stream (TCP, TLS, or custom TLS) + pub inner: MaybeTlsStream, + /// Read timeout for socket operations + pub read_timeout: Option, + /// Write timeout for socket operations + pub write_timeout: Option, } impl Socket { - pub(crate) fn new( + /// Create a new Socket with the given stream and timeouts + pub fn new( maybe_tls_stream: MaybeTlsStream, read_timeout: Option, write_timeout: Option, @@ -41,55 +45,74 @@ impl Socket { pub(crate) fn http2_negotiated(&self) -> Option> { match &self.inner { MaybeTlsStream::Tcp(_) => None, - #[cfg(feature = "tls")] - MaybeTlsStream::Tls(tls) => tls + #[cfg(feature = "rustls")] + MaybeTlsStream::Rustls(tls) => tls .get_ref() .1 .alpn_protocol() .map(|protocol| protocol.to_vec()), - } - } - #[cfg(feature = "tls")] - pub(crate) async fn tls(self, func: F) -> Result - where - F: FnOnce(TcpStream) -> Fut + 'static, - Fut: std::future::Future, Error>>, - { - match self.inner { - MaybeTlsStream::Tcp(t) => Ok(Self { - inner: MaybeTlsStream::Tls(Box::new(func(t).await?)), - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, - }), - MaybeTlsStream::Tls(t) => Ok(Self { - inner: MaybeTlsStream::Tls(t), - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, - }), + #[cfg(all(feature = "tls", not(feature = "rustls")))] + MaybeTlsStream::Custom(c) => c.alpn_protocol(), } } } +/// Enum representing different types of streams (TCP, TLS with rustls, or custom TLS) #[derive(Debug)] pub enum MaybeTlsStream { /// TCP Tcp(TcpStream), - #[cfg(feature = "tls")] + #[cfg(feature = "rustls")] /// TLS with rustls - Tls(Box>), + Rustls(Box>), + #[cfg(all(feature = "tls", not(feature = "rustls")))] + /// Custom TLS implementation (when tls feature is enabled without rustls backend) + 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)) + } } + impl MaybeTlsStream { #[cfg(feature = "tls")] /// get peer_certificate pub fn peer_certificate(&self) -> Option { match &self { MaybeTlsStream::Tcp(_) => None, - #[cfg(feature = "tls")] - MaybeTlsStream::Tls(stream) => stream + #[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(), } } } @@ -128,8 +151,10 @@ impl AsyncRead for MaybeTlsStream { ) -> Poll> { match self.get_mut() { MaybeTlsStream::Tcp(stream) => Pin::new(stream).poll_read(cx, buf), - #[cfg(feature = "tls")] - MaybeTlsStream::Tls(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), } } } @@ -141,23 +166,29 @@ impl AsyncWrite for MaybeTlsStream { ) -> Poll> { match self.get_mut() { MaybeTlsStream::Tcp(stream) => Pin::new(stream).poll_write(cx, buf), - #[cfg(feature = "tls")] - MaybeTlsStream::Tls(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), } } 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 = "tls")] - MaybeTlsStream::Tls(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), } } 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 = "tls")] - MaybeTlsStream::Tls(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), } } } @@ -165,31 +196,31 @@ impl Socket { /// Reads all bytes until EOF in this source, appending them to buf. pub async fn read_to_string(&mut self, buf: &mut String) -> std::io::Result { match self.read_timeout { - None => AsyncReadExt::read_to_string(self.deref_mut(), buf).await, + None => AsyncReadExt::read_to_string(&mut self.inner, buf).await, Some(t) => { - tokio::time::timeout(t, AsyncReadExt::read_to_string(self.deref_mut(), buf)).await? + tokio::time::timeout(t, AsyncReadExt::read_to_string(&mut self.inner, buf)).await? } } } /// Reads all bytes until EOF in this source, placing them into buf. pub async fn read_to_end(&mut self, buf: &mut Vec) -> std::io::Result { match self.read_timeout { - None => AsyncReadExt::read_to_end(self.deref_mut(), buf).await, - Some(t) => tokio::time::timeout(t, AsyncReadExt::read_to_end(self.deref_mut(), buf)).await?, + None => AsyncReadExt::read_to_end(&mut self.inner, buf).await, + Some(t) => tokio::time::timeout(t, AsyncReadExt::read_to_end(&mut self.inner, buf)).await?, } } /// Reads the exact number of bytes required to fill buf. pub async fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result { match self.read_timeout { - None => AsyncReadExt::read_exact(self.deref_mut(), buf).await, - Some(t) => tokio::time::timeout(t, AsyncReadExt::read_exact(self.deref_mut(), buf)).await?, + None => AsyncReadExt::read_exact(&mut self.inner, buf).await, + Some(t) => tokio::time::timeout(t, AsyncReadExt::read_exact(&mut self.inner, buf)).await?, } } /// Pulls some bytes from this source into the specified buffer, returning how many bytes were read. pub async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { match self.read_timeout { - None => AsyncReadExt::read(self.deref_mut(), buf).await, - Some(t) => tokio::time::timeout(t, AsyncReadExt::read(self.deref_mut(), buf)).await?, + None => AsyncReadExt::read(&mut self.inner, buf).await, + Some(t) => tokio::time::timeout(t, AsyncReadExt::read(&mut self.inner, buf)).await?, } } } @@ -197,43 +228,31 @@ impl Socket { /// Writes a buffer into this writer, returning how many bytes were pub async fn write(&mut self, buf: &[u8]) -> std::io::Result { match self.write_timeout { - None => AsyncWriteExt::write(self.deref_mut(), buf).await, - Some(t) => tokio::time::timeout(t, AsyncWriteExt::write(self.deref_mut(), buf)).await?, + None => AsyncWriteExt::write(&mut self.inner, buf).await, + Some(t) => tokio::time::timeout(t, AsyncWriteExt::write(&mut self.inner, buf)).await?, } } /// Attempts to write an entire buffer into this writer. pub async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { match self.write_timeout { - None => AsyncWriteExt::write_all(self.deref_mut(), buf).await, - Some(t) => tokio::time::timeout(t, AsyncWriteExt::write_all(self.deref_mut(), buf)).await?, + None => AsyncWriteExt::write_all(&mut self.inner, buf).await, + Some(t) => tokio::time::timeout(t, AsyncWriteExt::write_all(&mut self.inner, buf)).await?, } } /// Flushes this output stream, ensuring that all intermediately buffered /// contents reach their destination. pub async fn flush(&mut self) -> std::io::Result<()> { match self.write_timeout { - None => AsyncWriteExt::flush(self.deref_mut()).await, - Some(t) => tokio::time::timeout(t, AsyncWriteExt::flush(self.deref_mut())).await?, + None => AsyncWriteExt::flush(&mut self.inner).await, + Some(t) => tokio::time::timeout(t, AsyncWriteExt::flush(&mut self.inner)).await?, } } /// Shuts down the output stream, ensuring that the value can be dropped /// cleanly. pub async fn shutdown(&mut self) -> std::io::Result<()> { match self.write_timeout { - None => AsyncWriteExt::shutdown(self.deref_mut()).await, - Some(t) => tokio::time::timeout(t, AsyncWriteExt::shutdown(self.deref_mut())).await?, - } - } -} -// 直接暴露socket的全部外部接口 -impl Deref for MaybeTlsStream { - type Target = TcpStream; - - fn deref(&self) -> &Self::Target { - match self { - MaybeTlsStream::Tcp(s) => s, - #[cfg(feature = "tls")] - MaybeTlsStream::Tls(t) => t.get_ref().0, + None => AsyncWriteExt::shutdown(&mut self.inner).await, + Some(t) => tokio::time::timeout(t, AsyncWriteExt::shutdown(&mut self.inner)).await?, } } } diff --git a/src/tls.rs b/src/tls.rs index f5f52c8..e607462 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,11 +1,17 @@ //! TLS configuration and types //! +#[cfg(feature = "rustls")] use std::io::{BufRead, BufReader}; use std::ops::Deref; +#[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, @@ -14,7 +20,8 @@ use tokio_rustls::rustls::{ /// peer certificate #[derive(Clone, Debug)] pub struct PeerCertificate { - pub(crate) inner: Vec, + /// The DER-encoded certificate data + pub inner: Vec, } impl Deref for PeerCertificate { @@ -27,11 +34,14 @@ 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, } #[derive(Clone, Debug)] enum Cert { + #[cfg_attr(not(feature = "rustls"), allow(dead_code))] Der(Vec), + #[cfg_attr(not(feature = "rustls"), allow(dead_code))] Pem(Vec), } impl Certificate { @@ -97,17 +107,24 @@ impl Certificate { /// # } /// ``` pub fn from_pem_bundle(pem_bundle: &[u8]) -> crate::Result> { - let mut reader = BufReader::new(pem_bundle); - - Self::read_pem_certs(&mut reader)? - .iter() - .map(|cert_vec| Certificate::from_der(cert_vec)) - .collect::>>() + #[cfg(feature = "rustls")] + { + let mut reader = BufReader::new(pem_bundle); + Self::read_pem_certs(&mut reader)? + .iter() + .map(|cert_vec| Certificate::from_der(cert_vec)) + .collect::>>() + } + #[cfg(not(feature = "rustls"))] + { + // Without rustls backend, just store as PEM + Ok(vec![Certificate::from_pem(pem_bundle)?]) + } } + #[cfg(feature = "rustls")] pub(crate) fn add_to_tls(self, root_cert_store: &mut RootCertStore) -> crate::Result<()> { use std::io::Cursor; - match self.original { Cert::Der(buf) => root_cert_store .add(buf.into()) @@ -125,6 +142,7 @@ impl Certificate { Ok(()) } + #[cfg(feature = "rustls")] fn read_pem_certs(reader: &mut impl BufRead) -> crate::Result>> { rustls_pemfile::certs(reader) .map(|result| match result { @@ -137,21 +155,30 @@ impl Certificate { /// 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 { - Pem { + #[cfg(feature = "rustls")] + RustlsPem { key: rustls_pki_types::PrivateKeyDer<'static>, certs: Vec>, }, + #[cfg(not(feature = "rustls"))] + CustomPem { pem_data: Vec }, } impl Clone for ClientCert { fn clone(&self) -> Self { match self { - ClientCert::Pem { key, certs } => ClientCert::Pem { + #[cfg(feature = "rustls")] + ClientCert::RustlsPem { key, certs } => ClientCert::RustlsPem { key: key.clone_key(), certs: certs.clone(), }, + #[cfg(not(feature = "rustls"))] + ClientCert::CustomPem { pem_data } => ClientCert::CustomPem { + pem_data: pem_data.clone(), + }, } } } @@ -180,62 +207,75 @@ impl Identity { /// /// # Optional /// - /// This requires the `tls(-...)` Cargo feature enabled. + /// This requires the `tls` Cargo feature enabled. /// pub fn from_pem(buf: &[u8]) -> crate::Result { - use rustls_pemfile::Item; - use std::io::Cursor; + #[cfg(feature = "rustls")] + { + 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(); + 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), - Ok(Item::Pkcs1Key(key)) => sk.push(key.into()), - 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( - "No valid certificate was found", - )))) - } - Err(_) => { - return Err(crate::errors::builder(TLSError::General(String::from( - "Invalid identity PEM file", - )))) + for result in rustls_pemfile::read_all(&mut pem) { + match result { + Ok(Item::X509Certificate(cert)) => certs.push(cert), + Ok(Item::Pkcs1Key(key)) => sk.push(key.into()), + 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( + "No valid certificate was found", + )))) + } + Err(_) => { + return Err(crate::errors::builder(TLSError::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( - "private key or certificate not found", - )))); - } - }; + if let (Some(sk), false) = (sk.pop(), certs.is_empty()) { + (sk, certs) + } else { + return Err(crate::errors::builder(TLSError::General(String::from( + "private key or certificate not found", + )))); + } + }; - Ok(Identity { - inner: ClientCert::Pem { key, certs }, - }) + Ok(Identity { + inner: ClientCert::RustlsPem { key, certs }, + }) + } + #[cfg(not(feature = "rustls"))] + { + // For custom TLS backend, store the PEM data + Ok(Identity { + inner: ClientCert::CustomPem { + pem_data: buf.to_vec(), + }, + }) + } } + #[cfg(feature = "rustls")] pub(crate) fn add_to_tls( self, - config_builder: rustls::ConfigBuilder< - rustls::ClientConfig, - // Not sure here - rustls::client::WantsClientCert, - >, + config_builder: rustls::ConfigBuilder, ) -> crate::Result { match self.inner { - ClientCert::Pem { key, certs } => config_builder + 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", + )), } } } @@ -260,6 +300,8 @@ 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, @@ -273,9 +315,11 @@ impl Version { } } +#[cfg(feature = "rustls")] #[derive(Debug)] pub(crate) struct NoVerifier; +#[cfg(feature = "rustls")] impl ServerCertVerifier for NoVerifier { fn verify_server_cert( &self, @@ -324,12 +368,15 @@ impl ServerCertVerifier for NoVerifier { ] } } + +#[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 { @@ -339,6 +386,7 @@ impl IgnoreHostname { } } +#[cfg(feature = "rustls")] impl ServerCertVerifier for IgnoreHostname { fn verify_server_cert( &self,