diff --git a/Cargo.toml b/Cargo.toml index 4eee9e9..d7fa6d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ authors = ["hatoo "] license = "MIT" categories = ["network-programming", "web-programming::http-server"] keywords = ["http", "proxy", "http-proxy"] +resolver = "2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -35,7 +36,6 @@ hyper-util = { version = "0.1.7", features = ["tokio"] } native-tls = { version = "0.2.12", features = ["alpn"] } thiserror = "2.0.2" moka = { version = "0.12.8", features = ["sync"] } -winnow = { version = "0.6.20", optional = true } [dev-dependencies] axum = { version = "0.7.2", features = ["http2"] } @@ -44,6 +44,3 @@ rcgen = { version = "0.13.1", features = ["x509-parser"] } reqwest = { version = "0.12.7", features = ["native-tls-alpn"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } winnow = "0.6.20" - -[[example]] -name = "websocket" diff --git a/examples/reqwest_proxy.rs b/examples/reqwest_proxy.rs new file mode 100644 index 0000000..93d1161 --- /dev/null +++ b/examples/reqwest_proxy.rs @@ -0,0 +1,150 @@ +/// This example demonstrates how to use `http-mitm-proxy` with `reqwest`. +use std::path::PathBuf; + +use bytes::Bytes; +use clap::{Args, Parser}; +use http_mitm_proxy::MitmProxy; +use hyper::{body::Body, service::service_fn}; +use moka::sync::Cache; +use tracing_subscriber::EnvFilter; + +#[derive(Parser)] +struct Opt { + #[clap(flatten)] + external_cert: Option, +} + +#[derive(Args, Debug)] +struct ExternalCert { + #[arg(required = false)] + cert: PathBuf, + #[arg(required = false)] + private_key: PathBuf, +} + +fn make_root_cert() -> rcgen::CertifiedKey { + let mut param = rcgen::CertificateParams::default(); + + param.distinguished_name = rcgen::DistinguishedName::new(); + param.distinguished_name.push( + rcgen::DnType::CommonName, + rcgen::DnValue::Utf8String("".to_string()), + ); + param.key_usages = vec![ + rcgen::KeyUsagePurpose::KeyCertSign, + rcgen::KeyUsagePurpose::CrlSign, + ]; + param.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); + + let key_pair = rcgen::KeyPair::generate().unwrap(); + let cert = param.self_signed(&key_pair).unwrap(); + + rcgen::CertifiedKey { cert, key_pair } +} + +#[tokio::main] +async fn main() { + let opt = Opt::parse(); + + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + let root_cert = if let Some(external_cert) = opt.external_cert { + // Use existing key + let param = rcgen::CertificateParams::from_ca_cert_pem( + &std::fs::read_to_string(&external_cert.cert).unwrap(), + ) + .unwrap(); + let key_pair = + rcgen::KeyPair::from_pem(&std::fs::read_to_string(&external_cert.private_key).unwrap()) + .unwrap(); + + let cert = param.self_signed(&key_pair).unwrap(); + + rcgen::CertifiedKey { cert, key_pair } + } else { + make_root_cert() + }; + + let root_cert_pem = root_cert.cert.pem(); + let root_cert_key = root_cert.key_pair.serialize_pem(); + + let proxy = MitmProxy::new( + // This is the root cert that will be used to sign the fake certificates + Some(root_cert), + Some(Cache::new(128)), + ); + + let client = reqwest::Client::new(); + let server = proxy + .bind( + ("127.0.0.1", 3003), + service_fn(move |req| { + let client = client.clone(); + async move { + let uri = req.uri().clone(); + + // You can modify request here + // or You can just return response anywhere + + let req = to_reqwest(req); + let res = client.execute(req).await?; + + println!("{} -> {}", uri, res.status()); + + // You can modify response here + + Ok::<_, reqwest::Error>(from_reqwest(res)) + } + }), + ) + .await + .unwrap(); + + println!("HTTP Proxy is listening on http://127.0.0.1:3003"); + + println!(); + println!("Trust this cert if you want to use HTTPS"); + println!(); + println!("{}", root_cert_pem); + println!(); + + /* + Save this cert to ca.crt and use it with curl like this: + curl https://www.google.com -x http://127.0.0.1:3003 --cacert ca.crt + */ + + println!("Private key"); + println!("{}", root_cert_key); + + server.await; +} + +fn to_reqwest(req: hyper::Request) -> reqwest::Request +where + T: Body + Send + Sync + 'static, + T::Data: Into, + T::Error: Into>, +{ + let (parts, body) = req.into_parts(); + let url = reqwest::Url::parse(&parts.uri.to_string()).unwrap(); + let mut req = reqwest::Request::new(parts.method, url); + *req.headers_mut() = parts.headers; + req.body_mut().replace(reqwest::Body::wrap(body)); + *req.version_mut() = parts.version; + + req +} + +fn from_reqwest(res: reqwest::Response) -> hyper::Response { + let mut hres = hyper::Response::builder() + .status(res.status()) + .version(res.version()); + + *hres.headers_mut().unwrap() = res.headers().clone(); + + let body = reqwest::Body::from(res); + + hres.body(body).unwrap() +}