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
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "slinger" #改这个
version = "0.2.7"
version = "0.2.8"
edition = "2021"
description = "An HTTP Client for Rust designed for hackers."
homepage = "https://github.com/emo-crab/slinger"
Expand Down Expand Up @@ -47,14 +47,17 @@ tokio-rustls = { version = "0.26.2", optional = true, features = ["ring", "tls12
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.3", 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"
[features]
default = []
tls = ["tokio-rustls", "rustls-pemfile", "rustls-pki-types"]
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
11 changes: 8 additions & 3 deletions examples/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new().build().unwrap();
let mut worker = FuturesUnordered::new();
let start = std::time::Instant::now();
let t = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
for n in t {
worker.push(client.get(format!("http://httpbin.org/delay/{}", n)).send());
let mut t = vec![1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter();
for _ in 0..5 {
if let Some(n) = t.next() {
worker.push(client.get(format!("http://httpbin.org/delay/{}", n)).send());
}
}
while let Some(resp) = worker.next().await {
println!("{}", resp?.text()?);
if let Some(n) = t.next() {
worker.push(client.get(format!("http://httpbin.org/delay/{}", n)).send());
}
}
println!("{:?}", std::time::Instant::now() - start);
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion examples/cookies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
use slinger::ClientBuilder;
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
let resp = client
.get("https://httpbin.org/cookies/set/key/value")
.get("http://httpbin.org/cookies/set/key/value")
.send()
.await?;
println!("{:?}", resp.text());
Expand Down
29 changes: 29 additions & 0 deletions examples/http2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! HTTP/2 example
//!
//! This example demonstrates HTTP/2 support with various stream types.
//! HTTP/2 can work with:
//! - Plain TCP connections (h2c - HTTP/2 over cleartext)
//! - TLS connections (with ALPN negotiation)
//!
//! Note: Most servers require TLS with ALPN to negotiate HTTP/2.
//! For testing h2c (HTTP/2 over cleartext), you need a server that supports it.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(all(feature = "http2", feature = "tls"))]
{
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()?;
// 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?;
println!("Status: {}", resp.status_code());
println!("Version: {:?}", resp.version());
let body_len = resp.body().as_ref().map(|b| b.len()).unwrap_or(0);
println!("Body length: {} bytes", body_len);
println!();
}
Ok(())
}
117 changes: 83 additions & 34 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::fmt::{Debug, Formatter};
use std::str::FromStr;
Expand Down Expand Up @@ -228,30 +229,52 @@ impl Client {
///
/// This method fails if there was an error while sending request,
/// or redirect limit was exhausted.
pub async fn execute_request(&self, socket: &mut Socket, request: &Request) -> Result<Response> {
let raw: Bytes = request.to_raw();
#[cfg(feature = "tls")]
let mut peer_certificate: Option<PeerCertificate> = None;
#[cfg(feature = "tls")]
{
if let Some(pc) = socket.peer_certificate() {
peer_certificate = Some(pc);
pub async fn execute_request(
&self,
socket_opt: Option<Socket>,
request: &Request,
) -> Result<(Response, Option<Socket>)> {
if let Some(mut socket) = socket_opt {
#[cfg(feature = "http2")]
{
// Check if HTTP/2 was negotiated via ALPN
if socket.is_http2_negotiated() {
let response =
crate::h2_client::send_h2_request(socket, request, self.inner.timeout).await?;
return Ok((response, None));
}
}
}
socket.write_all(&raw).await?;
socket.flush().await?;
let reader = tokio::io::BufReader::new(socket);
let mut irp = ResponseBuilder::new(reader, ResponseConfig::new(request, self.inner.timeout))
.build()
.await?;
*irp.url_mut() = request.uri().clone();
#[cfg(feature = "tls")]
{
if let Some(cert) = peer_certificate {
irp.extensions_mut().insert(cert);
// HTTP/1.1 path (existing code)
let raw: Bytes = request.to_raw();
#[cfg(feature = "tls")]
let mut peer_certificate: Option<PeerCertificate> = None;
#[cfg(feature = "tls")]
{
if let Some(pc) = socket.peer_certificate() {
peer_certificate = Some(pc);
}
}
socket.write_all(&raw).await?;
socket.flush().await?;
let reader = tokio::io::BufReader::new(socket);
let (mut irp, socket) =
ResponseBuilder::new(reader, ResponseConfig::new(request, self.inner.timeout))
.build()
.await?;
*irp.url_mut() = request.uri().clone();
#[cfg(feature = "tls")]
{
if let Some(cert) = peer_certificate {
irp.extensions_mut().insert(cert);
}
}
Ok((irp, Some(socket)))
} else {
Err(new_io_error(
std::io::ErrorKind::NotConnected,
"socket is None",
))
}
Ok(irp)
}
/// Executes a `Request`.
///
Expand All @@ -270,7 +293,7 @@ impl Client {
let mut request = request.into();
let mut cur_uri = request.uri().clone();
let mut uris = vec![];
let mut conn: BTreeMap<String, Socket> = BTreeMap::new();
let mut conn: BTreeMap<String, Option<Socket>> = BTreeMap::new();
// 连接一次,同一个主机地址下复用socket连接
let uniq_key = |u: &http::Uri| -> String {
let scheme = u.scheme_str().unwrap_or_default();
Expand Down Expand Up @@ -309,21 +332,31 @@ impl Client {
.insert(http::header::CONNECTION, HeaderValue::from_static("close"));
}
record.record_request(&request);
let socket = conn
.entry(uniq_key(&cur_uri))
.or_insert(self.inner.connector.connect_with_uri(&cur_uri).await?);
let mut response = self.execute_request(socket, &request).await?;
let key = uniq_key(&cur_uri);
let socket = match conn.entry(key) {
Entry::Occupied(mut entry) => {
entry.get_mut().take() // take out the Option<Socket>
}
Entry::Vacant(entry) => {
let new_socket = self.inner.connector.connect_with_uri(&cur_uri).await?;
entry.insert(None); // placeholder
Some(new_socket)
}
};
let (mut response, socket) = self.execute_request(socket, &request).await?;
response.extensions_mut().insert(request.clone());
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(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(s) = conn.get_mut(&uniq_key(&cur_uri)) {
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?;
}
Expand Down Expand Up @@ -420,8 +453,10 @@ impl Client {
records.push(record);
break;
}
for (_key, mut socket) in conn {
socket.shutdown().await?;
for (_key, socket) in conn {
if let Some(mut s) = socket {
s.shutdown().await?;
}
}
let mut last_response = records
.last()
Expand Down Expand Up @@ -536,6 +571,10 @@ impl ClientBuilder {
connector_builder = connector_builder.identity(identity);
}
}
#[cfg(feature = "http2")]
{
connector_builder = connector_builder.enable_http2(config.http2);
}
let connector = connector_builder.build()?;
Ok(Client {
inner: Arc::new(ClientRef {
Expand Down Expand Up @@ -831,9 +870,17 @@ impl ClientBuilder {
self.config.max_tls_version = version;
self
}
#[cfg(feature = "http2")]
/// Enable or disable HTTP/2 support.
pub fn enable_http2(mut self, http2: bool) -> Self {
self.config.http2 = http2;
self
}
}
#[derive(Clone)]
struct Config {
#[cfg(feature = "http2")]
http2: bool,
timeout: Option<Duration>,
connect_timeout: Option<Duration>,
read_timeout: Option<Duration>,
Expand Down Expand Up @@ -878,6 +925,8 @@ impl Debug for Config {
impl Default for Config {
fn default() -> Self {
Self {
#[cfg(feature = "http2")]
http2: false,
timeout: Some(Duration::from_secs(30)),
connect_timeout: Some(Duration::from_secs(10)),
read_timeout: Some(Duration::from_secs(30)),
Expand Down
Loading
Loading