Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 }
Expand All @@ -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"]
Expand Down
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -89,6 +89,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
```

<!-- FEATURES -->

## 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.

<!-- USAGE EXAMPLES -->

## Example
Expand Down Expand Up @@ -116,7 +151,7 @@ X: GET http://192.168.83.1:8080/admin.jsp HTTP/1.0
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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() {
Expand Down
2 changes: 1 addition & 1 deletion examples/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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();
Expand Down
2 changes: 1 addition & 1 deletion examples/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use slinger::{ClientBuilder, Request};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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",
))
Expand Down
2 changes: 1 addition & 1 deletion examples/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
{
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::<PeerCertificate>().unwrap();
println!("{:?}", certificate);
Expand Down
2 changes: 1 addition & 1 deletion examples/cookies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[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()
Expand Down
46 changes: 46 additions & 0 deletions examples/custom_connector_builder.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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(())
}
198 changes: 198 additions & 0 deletions examples/custom_tls.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
}

#[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<PeerCertificate> {
// 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<std::io::Result<()>> {
// 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<std::result::Result<usize, std::io::Error>> {
// 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<std::result::Result<(), std::io::Error>> {
Pin::new(&mut self.inner).poll_flush(cx)
}

fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), std::io::Error>> {
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<Box<dyn std::future::Future<Output = Result<Socket>> + 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<dyn std::error::Error>> {
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");
}
2 changes: 1 addition & 1 deletion examples/gzip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[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());
}
Expand Down
Loading
Loading