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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ members = [
[workspace.package]
version = "1.2.3"
edition = "2021"
rust-version = "1.84"
rust-version = "1.89"
license = "MIT"
repository = "https://github.com/deedy5/primp"
authors = ["deedy5"]
Expand Down
2 changes: 1 addition & 1 deletion crates/primp-h2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ authors = [
]
description = "A fork of the HTTP/2 client and server library from hyperium"
edition = "2021"
rust-version = "1.84"
rust-version = "1.89"

# Aliases
[lib]
Expand Down
2 changes: 1 addition & 1 deletion crates/primp-h2/src/frame/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ impl Settings {
}

// Ensure the payload length is correct, each setting is 6 bytes long.
if payload.len() % 6 != 0 {
if !payload.len().is_multiple_of(6) {
tracing::debug!("invalid settings payload length; len={:?}", payload.len());
return Err(Error::InvalidPayloadAckSettings);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/primp-h2/src/frame/stream_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl StreamId {
/// was initiated by the server.
pub fn is_server_initiated(&self) -> bool {
let id = self.0;
id != 0 && id % 2 == 0
id != 0 && id.is_multiple_of(2)
}

/// Return a new `StreamId` for stream 0.
Expand Down
2 changes: 1 addition & 1 deletion crates/primp-h2/tests/h2-support/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Carl Lerche <me@carllerche.com>"]
publish = false
edition = "2021"
rust-version = "1.84"
rust-version = "1.89"

[dependencies]
h2 = { path = "../..", package = "primp-h2", features = ["stream", "unstable"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/primp-h2/tests/h2-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Carl Lerche <me@carllerche.com>"]
publish = false
edition = "2021"
rust-version = "1.84"
rust-version = "1.89"

[dependencies]

Expand Down
2 changes: 1 addition & 1 deletion crates/primp-hyper-rustls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "primp-hyper-rustls"
version = "0.27.9"
edition = "2021"
rust-version = "1.84"
rust-version = "1.89"
license = "Apache-2.0 OR ISC OR MIT"
description = "Fork of hyper-rustls"

Expand Down
2 changes: 1 addition & 1 deletion crates/primp-hyper-util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.20"
license = "MIT"
authors = ["deedy5", "Sean McArthur <sean@seanmonstar.com>"]
edition = "2021"
rust-version = "1.84"
rust-version = "1.89"
build = false
autolib = false
autobins = false
Expand Down
2 changes: 1 addition & 1 deletion crates/primp-hyper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ readme = "README.md"
license = "MIT"
authors = ["deedy5", "Sean McArthur <sean@seanmonstar.com>"]
edition = "2021"
rust-version = "1.84"
rust-version = "1.89"
build = false
include = [
"Cargo.toml",
Expand Down
7 changes: 2 additions & 5 deletions crates/primp-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "primp-python"
version = "1.2.3"
edition = "2021"
rust-version = "1.84"
rust-version = "1.89"
description = "HTTP client that can impersonate web browsers"
authors = ["deedy5"]

Expand All @@ -24,18 +24,15 @@ primp = { version = "1.2", path = "../primp", features = [
bytes = "1"
encoding_rs = { version = "0.8.35" }
foldhash = "0.2.0"
h2 = { version = "0.4", path = "../primp-h2", package = "primp-h2", default-features = false, features = ["unstable"] }

indexmap = { version = "2.13.0", features = ["serde"] }
tokio = { version = "1.49.0", features = ["full"] }
tokio-util = { version = "0.7.18", features = ["codec"] } # for multipart
html2text = "0.16.6"
pythonize = "0.28.0"
serde_json = "1.0.149"
webpki-root-certs = "1.0.5"
http = "1.4"
mime = "0.3.17"
rand = "0.10"
typed-builder = "0.20.0"
once_cell = "1.21"
url = "2.5"

71 changes: 17 additions & 54 deletions crates/primp-python/src/async/client.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
use std::sync::{Arc, RwLock};
use std::time::Duration;

use ::primp::{
multipart, Body, Client as PrimpClient, Method, Proxy, Response as PrimpResponse, Url,
};
use ::primp::{multipart, Body, Client as PrimpClient, Method, Response as PrimpResponse, Url};
use pyo3::prelude::*;
use pythonize::depythonize;
use serde_json::Value;
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};

use crate::client_builder::{
configure_client_builder, cookies_to_header_values, headers_without_cookie,
parse_cookies_from_header, parse_url_or_domain, IndexMapSSR,
};
use crate::client_builder::{configure_client_builder, cookies_to_header_values, IndexMapSSR};
use crate::error::{PrimpErrorEnum, PrimpResult};
use crate::extract_cookies_to_indexmap;
use crate::traits::{HeaderMapExt, HeadersTraits};
use crate::traits::HeadersTraits;
use crate::utils::extract_encoding;

/// Async HTTP client that can impersonate web browsers.
Expand Down Expand Up @@ -120,32 +115,16 @@ impl AsyncClient {

#[getter]
pub fn get_headers(&self) -> PrimpResult<IndexMapSSR> {
let client = self.client.read().expect("client lock was poisoned");
Ok(headers_without_cookie(client.headers()))
crate::client_builder::client_headers(&self.client)
}

#[setter]
pub fn set_headers(&mut self, new_headers: Option<IndexMapSSR>) -> PrimpResult<()> {
let mut client = self.client.write().expect("client lock was poisoned");
let headers = client.headers_mut();
headers.clear();
if let Some(new_headers) = new_headers {
for (k, v) in new_headers {
headers.insert_key_value(k, v)?;
}
}
Ok(())
crate::client_builder::client_set_headers(&self.client, new_headers)
}

pub fn headers_update(&self, new_headers: Option<IndexMapSSR>) -> PrimpResult<()> {
let mut client = self.client.write().expect("client lock was poisoned");
let headers = client.headers_mut();
if let Some(new_headers) = new_headers {
for (k, v) in new_headers {
headers.insert_key_value(k, v)?;
}
}
Ok(())
crate::client_builder::client_headers_update(&self.client, new_headers)
}

#[getter]
Expand All @@ -155,37 +134,21 @@ impl AsyncClient {

#[setter]
pub fn set_proxy(&mut self, proxy: String) -> PrimpResult<()> {
let rproxy = Proxy::all(proxy.clone())?;
let mut client = self.client.write().expect("client lock was poisoned");
let client_ref = &mut *client;
client_ref.set_proxies(vec![rproxy]);
self.proxy = Some(proxy);
self.proxy = Some(crate::client_builder::client_set_proxy(
&self.client,
proxy,
)?);
Ok(())
}

#[pyo3(signature = (url))]
fn get_cookies(&self, url: &str) -> PrimpResult<IndexMapSSR> {
let url = Url::parse(url).map_err(|e| PrimpErrorEnum::InvalidURL(e.to_string()))?;
let client = self.client.read().expect("client lock was poisoned");
let cookie = client
.get_cookies(&url)
.ok_or_else(|| PrimpErrorEnum::Custom("Failed to get cookies".to_string()))?;
let cookie_str = cookie
.to_str()
.map_err(|e| PrimpErrorEnum::Custom(e.to_string()))?;
Ok(parse_cookies_from_header(cookie_str))
crate::client_builder::client_get_cookies(&self.client, url)
}

#[pyo3(signature = (url, cookies))]
fn set_cookies(&self, url: &str, cookies: Option<IndexMapSSR>) -> PrimpResult<()> {
let url =
parse_url_or_domain(url).map_err(|e| PrimpErrorEnum::InvalidURL(e.to_string()))?;
if let Some(cookies) = cookies {
let header_values = cookies_to_header_values(&cookies);
let client = self.client.read().expect("client lock was poisoned");
client.set_cookies(&url, header_values);
}
Ok(())
crate::client_builder::client_set_cookies(&self.client, url, cookies)
}

/// Constructs an async HTTP request with the given method, URL, and optionally sets a timeout, headers, and query parameters.
Expand Down Expand Up @@ -248,7 +211,7 @@ impl AsyncClient {
if !client_cookies.is_empty() {
let url_parsed = Url::parse(&resolved_url).map_err(Into::<PrimpErrorEnum>::into)?;
let cookie_values = cookies_to_header_values(client_cookies);
let client_guard = self.client.read().expect("client lock was poisoned");
let client_guard = self.client.read().unwrap_or_else(|e| e.into_inner());
client_guard.set_cookies(&url_parsed, cookie_values);
}
}
Expand All @@ -257,13 +220,13 @@ impl AsyncClient {
if let Some(cookies) = cookies.filter(|c| !c.is_empty()) {
let url_parsed = Url::parse(&resolved_url).map_err(Into::<PrimpErrorEnum>::into)?;
let cookie_values = cookies_to_header_values(&cookies);
let client_guard = self.client.read().expect("client lock was poisoned");
let client_guard = self.client.read().unwrap_or_else(|e| e.into_inner());
client_guard.set_cookies(&url_parsed, cookie_values);
}

// Handle follow_redirects: set policy before cloning client
if let Some(fr) = follow_redirects {
let mut client_guard = self.client.write().expect("client lock was poisoned");
let mut client_guard = self.client.write().unwrap_or_else(|e| e.into_inner());
if fr {
client_guard.set_redirect_policy(::primp::redirect::Policy::limited(20));
} else {
Expand All @@ -273,7 +236,7 @@ impl AsyncClient {

// Clone the client before entering the async block to avoid holding RwLockGuard across await
let client = {
let client_guard = self.client.read().expect("client lock was poisoned");
let client_guard = self.client.read().unwrap_or_else(|e| e.into_inner());
client_guard.clone()
};

Expand Down Expand Up @@ -349,7 +312,7 @@ impl AsyncClient {

// Restore redirect policy if it was changed
if follow_redirects.is_some() {
let mut client_guard = self.client.write().expect("client lock was poisoned");
let mut client_guard = self.client.write().unwrap_or_else(|e| e.into_inner());
client_guard.set_redirect_policy(::primp::redirect::Policy::limited(20));
}

Expand Down
75 changes: 72 additions & 3 deletions crates/primp-python/src/client_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
//! This module eliminates code duplication by providing shared configuration
//! structures and functions used by both `Client` and `AsyncClient`.

use std::sync::{Arc, RwLock};
use std::time::Duration;

use foldhash::fast::RandomState;
use indexmap::IndexMap;
use primp::{
header::{HeaderMap, HeaderValue},
redirect::Policy,
ClientBuilder, Proxy, Url,
Client as PrimpClient, ClientBuilder, Proxy, Url,
};

use crate::error::PrimpResult;
use crate::error::{PrimpErrorEnum, PrimpResult};
use crate::impersonate::{
get_random_element, parse_impersonate_os_with_fallback, parse_impersonate_with_fallback,
IMPERSONATEOS_LIST,
};
use crate::traits::HeadersTraits;
use crate::traits::{HeaderMapExt, HeadersTraits};
use crate::utils::load_ca_certs;

/// Type alias for IndexMap with String keys and values.
Expand Down Expand Up @@ -211,3 +212,71 @@ pub fn headers_without_cookie(headers: &HeaderMap) -> IndexMapSSR {
headers_map.swap_remove("cookie");
headers_map
}

pub fn client_headers(client: &Arc<RwLock<PrimpClient>>) -> PrimpResult<IndexMapSSR> {
let c = client.read().unwrap_or_else(|e| e.into_inner());
Ok(headers_without_cookie(c.headers()))
}

pub fn client_set_headers(
client: &Arc<RwLock<PrimpClient>>,
new_headers: Option<IndexMapSSR>,
) -> PrimpResult<()> {
let mut c = client.write().unwrap_or_else(|e| e.into_inner());
let headers = c.headers_mut();
headers.clear();
if let Some(new_headers) = new_headers {
for (k, v) in new_headers {
headers.insert_key_value(k, v)?;
}
}
Ok(())
}

pub fn client_headers_update(
client: &Arc<RwLock<PrimpClient>>,
new_headers: Option<IndexMapSSR>,
) -> PrimpResult<()> {
let mut c = client.write().unwrap_or_else(|e| e.into_inner());
let headers = c.headers_mut();
if let Some(new_headers) = new_headers {
for (k, v) in new_headers {
headers.insert_key_value(k, v)?;
}
}
Ok(())
}

pub fn client_set_proxy(client: &Arc<RwLock<PrimpClient>>, proxy: String) -> PrimpResult<String> {
let rproxy = Proxy::all(proxy.clone())?;
let mut c = client.write().unwrap_or_else(|e| e.into_inner());
c.set_proxies(vec![rproxy]);
Ok(proxy)
}

pub fn client_get_cookies(
client: &Arc<RwLock<PrimpClient>>,
url: &str,
) -> PrimpResult<IndexMapSSR> {
let parsed = Url::parse(url).map_err(|e| PrimpErrorEnum::InvalidURL(e.to_string()))?;
let c = client.read().unwrap_or_else(|e| e.into_inner());
let cookie = c
.get_cookies(&parsed)
.ok_or_else(|| PrimpErrorEnum::Custom("No cookies found for URL".to_string()))?;
let cookie_str = cookie.to_str()?;
Ok(parse_cookies_from_header(cookie_str))
}

pub fn client_set_cookies(
client: &Arc<RwLock<PrimpClient>>,
url: &str,
cookies: Option<IndexMapSSR>,
) -> PrimpResult<()> {
let parsed = parse_url_or_domain(url).map_err(|e| PrimpErrorEnum::InvalidURL(e.to_string()))?;
if let Some(cookies) = cookies {
let header_values = cookies_to_header_values(&cookies);
let c = client.read().unwrap_or_else(|e| e.into_inner());
c.set_cookies(&parsed, header_values);
}
Ok(())
}
Loading
Loading