Skip to content

Commit

Permalink
Build with webpki-root-certs
Browse files Browse the repository at this point in the history
  • Loading branch information
deedy5 committed Dec 18, 2024
1 parent 2008cfd commit 040ef56
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 55 deletions.
37 changes: 23 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pyo3 = { version = "0.23", features = ["extension-module", "abi3-py38", "indexma
anyhow = "1"
log = "0.4"
pyo3-log = "0.12"
rquest = { version = "0.30", features = [
rquest = { version = "0.31", features = [
"cookies",
"multipart",
"json",
Expand All @@ -34,6 +34,8 @@ html2text = "0.13"
bytes = "1"
pythonize = "0.23"
serde_json = "1"
webpki-root-certs = "0.26"
rand = "0.8"

[profile.release]
codegen-units = 1
Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Provides precompiled wheels:</br>
- [I. Client](#i-client)
- [Client methods](#client-methods)
- [Response object](#response-object)
- [Devices](#devices)
- [Examples](#examples)
- [II. AsyncClient](#ii-asyncclient)

Expand Down Expand Up @@ -143,12 +144,23 @@ resp.text_rich # html is converted to rich text
resp.url
```

#### Devices

- Chrome: `Chrome100``Chrome101``Chrome104``Chrome105``Chrome106``Chrome107``Chrome108``Chrome109``Chrome114``Chrome116``Chrome117``Chrome118``Chrome119``Chrome120``Chrome123``Chrome124``Chrome126``Chrome127``Chrome128``Chrome129``Chrome130``Chrome131`

- Edge: `Edge101``Edge122``Edge127`

- Safari: `SafariIos17_2``SafariIos17_4_1``SafariIos16_5``Safari15_3``Safari15_5``Safari15_6_1``Safari16``Safari16_5``Safari17_0``Safari17_2_1``Safari17_4_1``Safari17_5``Safari18``SafariIPad18`

- OkHttp: `OkHttp3_9``OkHttp3_11``OkHttp3_13``OkHttp3_14``OkHttp4_9``OkHttp4_10``OkHttp5`

#### Examples

```python
import primp

client = primp.Client(impersonate="chrome_131")
# Impersonate
client = primp.Client(impersonate="chrome_131") # chrome_131

# GET request
resp = client.get("https://tls.peet.ws/api/all")
Expand Down Expand Up @@ -196,7 +208,8 @@ export PRIMP_PROXY="socks5://127.0.0.1:1080"
resp = primp.Client().get("https://tls.peet.ws/api/all")
print(resp.json())

# Using custom CA certificate store: file or certifi.where() or env var PRIMP_CA_BUNDLE
# Using custom CA certificate store: env var PRIMP_CA_BUNDLE
#(Primp built with the Mozilla's latest trusted root certificates, so maybe it's not necessary)
resp = primp.Client(ca_cert_file="/cert/cacert.pem").get("https://tls.peet.ws/api/all")
print(resp.json())
resp = primp.Client(ca_cert_file=certifi.where()).get("https://tls.peet.ws/api/all")
Expand All @@ -215,4 +228,3 @@ print(r.text)
### II. AsyncClient

TODO

58 changes: 22 additions & 36 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::fs;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
Expand All @@ -11,19 +10,21 @@ use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use pythonize::depythonize;
use rquest::boring::x509::{store::X509StoreBuilder, X509};
use rquest::header::{HeaderMap, HeaderName, HeaderValue, COOKIE};
use rquest::multipart;
use rquest::redirect::Policy;
use rquest::tls::Impersonate;
use rquest::Method;
use rquest::{
header::{HeaderMap, HeaderName, HeaderValue, COOKIE},
multipart,
redirect::Policy,
tls::Impersonate,
Method,
};
use serde_json::Value;
use tokio::runtime::{self, Runtime};

mod response;
use response::Response;

mod utils;
use utils::load_ca_certs;

// Tokio global one-thread runtime
static RUNTIME: LazyLock<Runtime> = LazyLock::new(|| {
Expand All @@ -32,12 +33,6 @@ static RUNTIME: LazyLock<Runtime> = LazyLock::new(|| {
.build()
.unwrap()
});
static PRIMP_CA_BUNDLE: LazyLock<Option<String>> = LazyLock::new(|| {
std::env::var("PRIMP_CA_BUNDLE")
.or(std::env::var("CA_CERT_FILE"))
.ok()
});
static PRIMP_PROXY: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("PRIMP_PROXY").ok());

#[pyclass]
/// HTTP client that can impersonate web browsers.
Expand Down Expand Up @@ -112,7 +107,7 @@ impl Client {
cookies: Option<IndexMap<String, String, RandomState>>,
cookie_store: Option<bool>,
referer: Option<bool>,
proxy: Option<&str>,
proxy: Option<String>,
timeout: Option<f64>,
impersonate: Option<&str>,
follow_redirects: Option<bool>,
Expand Down Expand Up @@ -160,8 +155,7 @@ impl Client {
}

// Proxy
let proxy = proxy.or(PRIMP_PROXY.as_deref());
if let Some(proxy_url) = proxy {
if let Some(proxy_url) = proxy.or_else(|| std::env::var("PRIMP_PROXY").ok()) {
let proxy = rquest::Proxy::all(proxy_url)?;
client_builder = client_builder.proxy(proxy);
}
Expand All @@ -179,24 +173,16 @@ impl Client {
client_builder = client_builder.redirect(Policy::none());
}

// Verify
let verify: bool = verify.unwrap_or(true);
if !verify {
client_builder = client_builder.danger_accept_invalid_certs(true);
// Ca_cert_file. BEFORE!!! verify (fn load_ca_certs() reads env var PRIMP_CA_BUNDLE)
if let Some(ca_bundle_path) = ca_cert_file {
std::env::set_var("PRIMP_CA_BUNDLE", ca_bundle_path);
}

// Ca_cert_file
let ca_cert_file = ca_cert_file.or(PRIMP_CA_BUNDLE.clone());
if let Some(ca_cert_file) = ca_cert_file {
client_builder = client_builder.ca_cert_store(move || {
let mut ca_store = X509StoreBuilder::new()?;
let cert_file = &fs::read(&ca_cert_file).expect("Failed to read ca_cert_file");
let certs = X509::stack_from_pem(&cert_file)?;
for cert in certs {
ca_store.add_cert(cert)?;
}
Ok(ca_store.build())
});
// Verify
if verify.unwrap_or(true) {
client_builder = client_builder.ca_cert_store(load_ca_certs);
} else {
client_builder = client_builder.danger_accept_invalid_certs(true);
}

// Http version: http1 || http2
Expand Down Expand Up @@ -275,12 +261,12 @@ impl Client {
"DELETE" => Ok(Method::DELETE),
_ => Err(PyValueError::new_err("Unrecognized HTTP method")),
}?;
let params = params.or(self.params.clone());
let cookies = cookies.or(self.cookies.clone());
let params = params.or_else(|| self.params.clone());
let cookies = cookies.or_else(|| self.cookies.clone());
let data_value: Option<Value> = data.map(|data| depythonize(&data)).transpose()?;
let json_value: Option<Value> = json.map(|json| depythonize(&json)).transpose()?;
let auth = auth.or(self.auth.clone());
let auth_bearer = auth_bearer.or(self.auth_bearer.clone());
let auth = auth.or_else(|| self.auth.clone());
let auth_bearer = auth_bearer.or_else(|| self.auth_bearer.clone());
if auth.is_some() && auth_bearer.is_some() {
return Err(PyValueError::new_err("Cannot provide both auth and auth_bearer").into());
}
Expand Down
2 changes: 1 addition & 1 deletion src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl Response {
return Ok(&self.encoding);
}
self.encoding = get_encoding_from_headers(&self.headers)
.or(get_encoding_from_content(&self.content.bind(py).as_bytes()))
.or_else(|| get_encoding_from_content(&self.content.bind(py).as_bytes()))
.unwrap_or("UTF-8".to_string());
Ok(&self.encoding)
}
Expand Down
Loading

0 comments on commit 040ef56

Please sign in to comment.