From e4973cfffa302582b79141a5036e1efd6b71275c Mon Sep 17 00:00:00 2001 From: blackswan Date: Sun, 16 Jun 2024 11:09:38 +0800 Subject: [PATCH 1/2] Support HTTP Basic Auth (#21) * support HTTP Basic auth * Revert "support HTTP Basic auth" This reverts commit ea036f2790a033b945377fdad533a2c2629cd43b. * support HTTP Basic Auth * update usage * remove a single used variable * add option: --no-httpauth to disable HTTP authenticaiton by default * update usage: --no-httpauth * fix --no-httpauth parser on true/false --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + README.md | 2 ++ src/main.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 64 insertions(+), 4 deletions(-) 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/README.md b/README.md index 8e416b2..b01a403 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Options: --listen-ip [default: 0.0.0.0] -u, --username Socks5 username -P, --password Socks5 password + --http-basic HTTP Basic Auth + --no-httpauth <1/0> Ignore HTTP Basic Auth, [default: 1] -s, --socks-address Socks5 proxy address [default: 127.0.0.1:1080] --allowed-domains Comma-separated list of allowed domains -h, --help Print help information diff --git a/src/main.rs b/src/main.rs index 576111c..3d85b8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod auth; use crate::auth::Auth; -use clap::{Args, Parser}; +use clap::{Args, Parser, value_parser}; use color_eyre::eyre::Result; use tokio_socks::tcp::Socks5Stream; @@ -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,14 @@ 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)] + http_basic: Option, + + /// Disable HTTP authentication [default: 1] + #[arg(long, value_parser = value_parser ! (u8).range(0..=1), default_value_t = 1)] + no_httpauth: u8, } #[tokio::main] @@ -72,6 +83,9 @@ 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 no_httpauth = args.no_httpauth == 1; let listener = TcpListener::bind(addr).await?; info!("Listening on http://{}", addr); @@ -80,7 +94,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, no_httpauth)); tokio::task::spawn(async move { if let Err(err) = http1::Builder::new() @@ -100,11 +114,47 @@ async fn proxy( req: Request, socks_addr: SocketAddr, auth: &'static Option, + http_basic: &Option, allowed_domains: &Option>, + no_httpauth: bool, ) -> Result>, hyper::Error> { - let uri = req.uri(); + let mut http_authed = false; + let hm = req.headers(); + + if no_httpauth { + http_authed = true; + } else 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); + debug!("Proxying request: {} {}", method, req.uri()); if let (Some(allowed_domains), Some(request_domain)) = (allowed_domains, req.uri().host()) { let domain = request_domain.to_owned(); if !allowed_domains.contains(&domain) { From c7d0c8f5ed2b4147fa3fb7cb1bcaf2dba7646edf Mon Sep 17 00:00:00 2001 From: Karan Gauswami Date: Sun, 16 Jun 2024 10:47:16 +0530 Subject: [PATCH 2/2] remove no_httpauth flag from cli and some cleanup (#23) Signed-off-by: KaranGauswami --- README.md | 1 - src/main.rs | 83 +++++++++++++++++++++++++++-------------------------- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index b01a403..97c019b 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ Options: -u, --username Socks5 username -P, --password Socks5 password --http-basic HTTP Basic Auth - --no-httpauth <1/0> Ignore HTTP Basic Auth, [default: 1] -s, --socks-address Socks5 proxy address [default: 127.0.0.1:1080] --allowed-domains Comma-separated list of allowed domains -h, --help Print help information diff --git a/src/main.rs b/src/main.rs index 3d85b8f..5442a89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod auth; use crate::auth::Auth; -use clap::{Args, Parser, value_parser}; +use clap::{Args, Parser}; use color_eyre::eyre::Result; use tokio_socks::tcp::Socks5Stream; @@ -10,16 +10,16 @@ use tracing_subscriber::EnvFilter; use std::net::{Ipv4Addr, SocketAddr}; +use base64::engine::general_purpose; +use base64::Engine; use bytes::Bytes; use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use hyper::client::conn::http1::Builder; +use hyper::header::{HeaderValue, PROXY_AUTHENTICATE}; 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; @@ -60,10 +60,6 @@ struct Cli { /// HTTP Basic Auth credentials in the format "user:passwd" #[arg(long)] http_basic: Option, - - /// Disable HTTP authentication [default: 1] - #[arg(long, value_parser = value_parser ! (u8).range(0..=1), default_value_t = 1)] - no_httpauth: u8, } #[tokio::main] @@ -83,18 +79,30 @@ 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 = args + .http_basic + .map(|hb| format!("Basic {}", general_purpose::STANDARD.encode(hb))) + .map(|hb| HeaderValue::from_str(&hb)) + .transpose()?; let http_basic = &*Box::leak(Box::new(http_basic)); - let no_httpauth = args.no_httpauth == 1; let listener = TcpListener::bind(addr).await?; info!("Listening on http://{}", addr); loop { - let (stream, _) = listener.accept().await?; + let (stream, client_addr) = listener.accept().await?; let io = TokioIo::new(stream); - let serve_connection = service_fn(move |req| proxy(req, socks_addr, auth, &http_basic, allowed_domains, no_httpauth)); + let serve_connection = service_fn(move |req| { + proxy( + req, + client_addr, + socks_addr, + auth, + http_basic, + allowed_domains, + ) + }); tokio::task::spawn(async move { if let Err(err) = http1::Builder::new() @@ -112,44 +120,39 @@ async fn main() -> Result<()> { async fn proxy( req: Request, + client_addr: SocketAddr, socks_addr: SocketAddr, auth: &'static Option, - http_basic: &Option, + basic_http_header: &Option, allowed_domains: &Option>, - no_httpauth: bool, ) -> Result>, hyper::Error> { - let mut http_authed = false; + let mut authenticated = false; let hm = req.headers(); - if no_httpauth { - http_authed = true; - } else if hm.contains_key("proxy-authorization") { - let config_auth = match http_basic { - Some(value) => value.clone(), - None => String::new(), + if let Some(basic_http_header) = basic_http_header { + let Some(http_auth) = hm.get("proxy-authorization") 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); }; - let http_auth = hm.get("proxy-authorization").unwrap(); - if http_auth == &HeaderValue::from_str(&config_auth).unwrap() { - http_authed = true; + if http_auth == basic_http_header { + authenticated = 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); + authenticated = true; } - 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; + if !authenticated { + warn!("Failed auth attempt from: {}", client_addr); + // http response code reference taken from tinyproxy + let mut resp = Response::new(full("Unauthorized")); + *resp.status_mut() = http::StatusCode::UNAUTHORIZED; return Ok(resp); } @@ -194,7 +197,7 @@ async fn proxy( } else { let host = req.uri().host().expect("uri has no host"); let port = req.uri().port_u16().unwrap_or(80); - let addr = format!("{}:{}", host, port); + let addr = (host, port); let stream = match auth { Some(auth) => Socks5Stream::connect_with_password(