From ea036f2790a033b945377fdad533a2c2629cd43b Mon Sep 17 00:00:00 2001 From: GoogleGeek Date: Mon, 10 Jun 2024 16:36:40 +0800 Subject: [PATCH] support HTTP Basic auth --- .idea/.gitignore | 5 + .idea/modules.xml | 8 ++ .idea/socks-to-http-proxy.iml | 11 ++ .idea/vcs.xml | 6 + Cargo.lock | 7 + Cargo.toml | 1 + src/main.rs | 45 ++++++- src/main2.rs | 180 ++++++++++++++++++++++++++ src/main3.rs | 233 ++++++++++++++++++++++++++++++++++ 9 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/socks-to-http-proxy.iml create mode 100644 .idea/vcs.xml create mode 100644 src/main2.rs create mode 100644 src/main3.rs diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c11f3c3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/socks-to-http-proxy.iml b/.idea/socks-to-http-proxy.iml new file mode 100644 index 0000000..cf84ae4 --- /dev/null +++ b/.idea/socks-to-http-proxy.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7d3f7f0..b8c3254 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bytes" version = "1.5.0" @@ -567,6 +573,7 @@ dependencies = [ name = "sthp" version = "0.4.0" dependencies = [ + "base64", "bytes", "clap", "color-eyre", diff --git a/Cargo.toml b/Cargo.toml index 276e99d..8396cae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ http-body-util = "0.1.0-rc.2" tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } hyper-util = { git = "https://github.com/hyperium/hyper-util.git", rev = "229757e565e0935a7a3b1d0f9e9ab88d9310e779" } +base64 = "0.22.1" diff --git a/src/main.rs b/src/main.rs index 576111c..fb4167e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,9 @@ use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::upgrade::Upgraded; use hyper::{Method, Request, Response}; +use hyper::header::{HeaderValue, PROXY_AUTHENTICATE}; +use base64::engine::general_purpose; +use base64::Engine; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; @@ -53,6 +56,10 @@ struct Cli { /// Comma-separated list of allowed domains #[arg(long, value_delimiter = ',')] allowed_domains: Option>, + + /// HTTP Basic Auth credentials in the format "user:passwd" + #[arg(long, default_value = "")] + http_basic: Option, } #[tokio::main] @@ -72,6 +79,8 @@ async fn main() -> Result<()> { let addr = SocketAddr::from((args.listen_ip, port)); let allowed_domains = args.allowed_domains; let allowed_domains = &*Box::leak(Box::new(allowed_domains)); + let http_basic = args.http_basic.map(|hb| format!("Basic {}", general_purpose::STANDARD.encode(hb))); + let http_basic = &*Box::leak(Box::new(http_basic)); let listener = TcpListener::bind(addr).await?; info!("Listening on http://{}", addr); @@ -80,7 +89,7 @@ async fn main() -> Result<()> { let (stream, _) = listener.accept().await?; let io = TokioIo::new(stream); - let serve_connection = service_fn(move |req| proxy(req, socks_addr, auth, allowed_domains)); + let serve_connection = service_fn(move |req| proxy(req, socks_addr, auth, &http_basic, allowed_domains)); tokio::task::spawn(async move { if let Err(err) = http1::Builder::new() @@ -100,9 +109,43 @@ async fn proxy( req: Request, socks_addr: SocketAddr, auth: &'static Option, + http_basic: &Option, allowed_domains: &Option>, ) -> Result>, hyper::Error> { let uri = req.uri(); + let mut http_authed = false; + let hm = req.headers(); + + if hm.contains_key("proxy-authorization") { + let config_auth = match http_basic { + Some(value) => value.clone(), + None => String::new(), + }; + let http_auth = hm.get("proxy-authorization").unwrap(); + if http_auth == &HeaderValue::from_str(&config_auth).unwrap() { + http_authed = true; + } + } else { + // When the request does not contain a Proxy-Authorization header, + // send a 407 response code and a Proxy-Authenticate header + let mut response = Response::new(full("Proxy authentication required")); + *response.status_mut() = http::StatusCode::PROXY_AUTHENTICATION_REQUIRED; + response.headers_mut().insert( + PROXY_AUTHENTICATE, + HeaderValue::from_static("Basic realm=\"proxy\""), + ); + return Ok(response); + } + + if !http_authed { + warn!("Failed to authenticate: {:?}", hm); + let mut resp = Response::new(full( + "Authorization failed, you are not allowed through the proxy.", + )); + *resp.status_mut() = http::StatusCode::FORBIDDEN; + return Ok(resp); + } + let method = req.method(); debug!("Proxying request: {} {}", method, uri); if let (Some(allowed_domains), Some(request_domain)) = (allowed_domains, req.uri().host()) { diff --git a/src/main2.rs b/src/main2.rs new file mode 100644 index 0000000..a673fd5 --- /dev/null +++ b/src/main2.rs @@ -0,0 +1,180 @@ +mod auth; + +use crate::auth::Auth; +use clap::{Args, Parser}; +use color_eyre::eyre::Result; +use tokio_socks::tcp::Socks5Stream; +use tracing::{debug, info, warn}; +use tracing_subscriber::EnvFilter; + +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; + +use bytes::Bytes; +use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; +use hyper::client::conn::http1::Builder; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::upgrade::Upgraded; +use hyper::{Method, Request, Response}; + +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; +use base64::encode; + +#[derive(Debug, Args)] +#[group()] +struct Auths { + /// Socks5 username + #[arg(short = 'u', long, required = false)] + username: String, + + /// Socks5 password + #[arg(short = 'P', long, required = false)] + password: String, +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// port where Http proxy should listen + #[arg(short, long, default_value_t = 8080)] + port: u16, + + #[arg(long, default_value = "0.0.0.0")] + listen_ip: Ipv4Addr, + + #[command(flatten)] + auth: Option, + + /// Socks5 proxy address + #[arg(short, long, default_value = "127.0.0.1:1080")] + socks_address: SocketAddr, + + /// Comma-separated list of allowed domains + #[arg(long, value_delimiter = ',')] + allowed_domains: Option>, + + /// HTTP Basic Auth in the format "user:passwd" + #[arg(long, required = false)] + httpbasic: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("sthp=debug")); + tracing_subscriber::fmt().with_env_filter(filter).init(); + color_eyre::install()?; + + let args = Cli::parse(); + + let socks_addr = args.socks_address; + let port = args.port; + let auth = args.auth.map(|a| Auth::new(a.username, a.password)); + let httpbasic = args.httpbasic.clone(); + + let listener = TcpListener::bind((args.listen_ip, port)).await?; + info!("Listening on {}:{}", args.listen_ip, port); + + loop { + let (stream, _) = listener.accept().await?; + let auth = auth.clone(); + let socks_addr = socks_addr; + let httpbasic = httpbasic.clone(); + + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection( + stream, + service_fn(|req| handle_request(req, socks_addr, &auth, &httpbasic)), + ) + .await + { + warn!("Error serving connection: {:?}", err); + } + }); + } +} + +async fn handle_request( + req: Request, + socks_addr: SocketAddr, + auth: &Option, + httpbasic: &Option, +) -> Result>, hyper::Error> { + let host = match host_addr(req.uri()) { + Some(host) => host, + None => return Ok(Response::builder().status(400).body(empty()).unwrap()), + }; + + let addr = format!("{}:{}", host, req.uri().port_u16().unwrap_or(80)); + debug!("Proxying request to {} via SOCKS5 proxy at {}", addr, socks_addr); + + let stream = match auth { + Some(auth) => Socks5Stream::connect_with_password(socks_addr, addr, &auth.username, &auth.password) + .await + .unwrap(), + None => Socks5Stream::connect(socks_addr, addr).await.unwrap(), + }; + + let io = TokioIo::new(stream); + + let (mut sender, conn) = Builder::new() + .preserve_header_case(true) + .title_case_headers(true) + .handshake(io) + .await?; + tokio::task::spawn(async move { + if let Err(err) = conn.await { + warn!("Connection failed: {:?}", err); + } + }); + + let mut req = req; + + if let Some(httpbasic) = httpbasic { + let encoded = encode(httpbasic); + let auth_header_value = format!("Basic {}", encoded); + req.headers_mut().insert("Authorization", auth_header_value.parse().unwrap()); + } + + let resp = sender.send_request(req).await?; + Ok(resp.map(|b| b.boxed())) +} + +fn host_addr(uri: &http::Uri) -> Option { + uri.authority().map(|auth| auth.to_string()) +} + +fn empty() -> BoxBody { + Empty::::new().map_err(|never| match never {}).boxed() +} + +fn full>(chunk: T) -> BoxBody { + Full::new(chunk.into()).map_err(|never| match never {}).boxed() +} + +async fn tunnel( + upgraded: Upgraded, + addr: String, + socks_addr: SocketAddr, + auth: &Option, +) -> Result<()> { + let mut stream = match auth { + Some(auth) => Socks5Stream::connect_with_password(socks_addr, addr, &auth.username, &auth.password).await?, + None => Socks5Stream::connect(socks_addr, addr).await?, + }; + + let mut upgraded = TokioIo::new(upgraded); + + // Proxying data + let (from_client, from_server) = + tokio::io::copy_bidirectional(&mut upgraded, &mut stream).await?; + + // Print message when done + debug!( + "client wrote {} bytes and received {} bytes", + from_client, from_server + ); + Ok(()) +} diff --git a/src/main3.rs b/src/main3.rs new file mode 100644 index 0000000..704ab8f --- /dev/null +++ b/src/main3.rs @@ -0,0 +1,233 @@ +mod auth; + +use crate::auth::Auth; +use clap::{Args, Parser}; +use color_eyre::eyre::Result; + +use tokio_socks::tcp::Socks5Stream; +use tracing::{debug, info, warn}; +use tracing_subscriber::EnvFilter; + +use std::net::{Ipv4Addr, SocketAddr}; + +use bytes::Bytes; +use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; +use hyper::client::conn::http1::Builder; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::upgrade::Upgraded; +use hyper::{Method, Request, Response}; +use hyper::header::{HeaderValue, PROXY_AUTHENTICATE}; + +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; + +#[derive(Debug, Args)] +#[group()] +struct Auths { + /// Socks5 username + #[arg(short = 'u', long, required = false)] + username: String, + + /// Socks5 password + #[arg(short = 'P', long, required = false)] + password: String, +} + +#[derive(Parser, Debug)] +#[command(author, version, about,long_about=None)] +struct Cli { + /// port where Http proxy should listen + #[arg(short, long, default_value_t = 8080)] + port: u16, + + #[arg(long, default_value = "0.0.0.0")] + listen_ip: Ipv4Addr, + + #[command(flatten)] + auth: Option, + + /// Socks5 proxy address + #[arg(short, long, default_value = "127.0.0.1:1080")] + socks_address: SocketAddr, + + /// Comma-separated list of allowed domains + #[arg(long, value_delimiter = ',')] + allowed_domains: Option>, + + /// HTTP Basic Auth in the format "user:passwd" + #[arg(long, required = false)] + httpbasic: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("sthp=debug")); + tracing_subscriber::fmt().with_env_filter(filter).init(); + color_eyre::install()?; + + let args = Cli::parse(); + + let socks_addr = args.socks_address; + let port = args.port; + let auth = args + .auth + .map(|auth| Auth::new(auth.username, auth.password)); + let auth = &*Box::leak(Box::new(auth)); + let addr = SocketAddr::from((args.listen_ip, port)); + let allowed_domains = args.allowed_domains; + let allowed_domains = &*Box::leak(Box::new(allowed_domains)); + let httpbasic = args.httpbasic.map(|hb| format!("Basic {}", base64::encode(hb))); + let httpbasic = &*Box::leak(Box::new(httpbasic)); + + let listener = TcpListener::bind(addr).await?; + info!("Listening on {}", addr); + + loop { + let (stream, _) = listener.accept().await?; + tokio::task::spawn(proxy( + stream, + socks_addr, + auth, + allowed_domains, + httpbasic, + )); + } +} + +async fn proxy( + stream: tokio::net::TcpStream, + socks_addr: SocketAddr, + auth: &Option, + allowed_domains: &Option>, + httpbasic: &Option, +) -> Result<()> { + let service = service_fn(move |req| { + let uri = req.uri(); + // hack for HTTP Basic Auth + let hm = req.headers(); + let mut authed = false; + + if let Some(config_auth) = httpbasic { + if hm.contains_key("proxy-authorization") { + let http_auth = hm.get("proxy-authorization").unwrap(); + if http_auth == config_auth { + authed = true; + } + } else { + // 当请求没有包含Proxy-Authorization头部时,发送407响应码和Proxy-Authenticate头部 + let mut response = Response::new(full("Proxy authentication required")); + *response.status_mut() = http::StatusCode::PROXY_AUTHENTICATION_REQUIRED; + response.headers_mut().insert( + PROXY_AUTHENTICATE, + HeaderValue::from_static("Basic realm=\"proxy\""), + ); + return Box::pin(async move { Ok(response) }); + } + + if !authed { + warn!("Failed to authenticate: {:?}", hm); + let mut resp = Response::new(full( + "Authorization failed, you are not allowed through the proxy.", + )); + *resp.status_mut() = http::StatusCode::FORBIDDEN; + return Box::pin(async move { Ok(resp) }); + } + } + + let method = req.method(); + debug!("Proxying request: {} {}", method, uri); + if let (Some(allowed_domains), Some(request_domain)) = (allowed_domains, req.uri().host()) { + if !allowed_domains.contains(&request_domain.to_string()) { + warn!("Domain not allowed: {}", request_domain); + return Box::pin(async move { + Ok(Response::builder().status(403).body(empty()).unwrap()) + }); + } + } + + let host = match host_addr(req.uri()) { + Some(host) => host, + None => return Box::pin(async move { + Ok(Response::builder().status(400).body(empty()).unwrap()) + }), + }; + + let addr = format!("{}:{}", host, req.uri().port_u16().unwrap_or(80)); + debug!("Proxying request to {} via SOCKS5 proxy at {}", addr, socks_addr); + + let stream = match auth { + Some(auth) => Socks5Stream::connect_with_password(socks_addr, addr, &auth.username, &auth.password) + .await + .unwrap(), + None => Socks5Stream::connect(socks_addr, addr).await.unwrap(), + }; + + let io = TokioIo::new(stream); + + let (mut sender, conn) = Builder::new() + .preserve_header_case(true) + .title_case_headers(true) + .handshake(io) + .await?; + tokio::task::spawn(async move { + if let Err(err) = conn.await { + warn!("Connection failed: {:?}", err); + } + }); + + let resp = sender.send_request(req).await?; + Ok(resp.map(|b| b.boxed())) + }); + + http1::Builder::new() + .preserve_header_case(true) + .title_case_headers(true) + .serve_connection(stream, service) + .await?; + Ok(()) +} + +fn host_addr(uri: &http::Uri) -> Option { + uri.authority().map(|auth| auth.to_string()) +} + +fn empty() -> BoxBody { + Empty::::new() + .map_err(|never| match never {}) + .boxed() +} + +fn full>(chunk: T) -> BoxBody { + Full::new(chunk.into()) + .map_err(|never| match never {}) + .boxed() +} + +async fn tunnel( + upgraded: Upgraded, + addr: String, + socks_addr: SocketAddr, + auth: &Option, +) -> Result<()> { + let mut stream = match auth { + Some(auth) => { + Socks5Stream::connect_with_password(socks_addr, addr, &auth.username, &auth.password) + .await? + } + None => Socks5Stream::connect(socks_addr, addr).await?, + }; + + let mut upgraded = TokioIo::new(upgraded); + + // Proxying data + let (from_client, from_server) = + tokio::io::copy_bidirectional(&mut upgraded, &mut stream).await?; + + // Print message when done + debug!( + "client wrote {} bytes and received {} bytes", + from_client, from_server + ); + Ok(()) +}