diff --git a/src/auth/apikey.rs b/src/auth/apikey.rs new file mode 100644 index 0000000..4f1f239 --- /dev/null +++ b/src/auth/apikey.rs @@ -0,0 +1,16 @@ +use crate::auth::types::AuthValidator; +use pingora_proxy::Session; +use subtle::ConstantTimeEq; + +pub struct ApiKeyAuth<'a>(pub(crate) &'a str); +#[async_trait::async_trait] +impl AuthValidator for ApiKeyAuth<'_> { + async fn validate(&self, session: &mut Session) -> bool { + if let Some(header) = session.get_header("x-api-key") { + if let Ok(h) = header.to_str() { + return h.as_bytes().ct_eq(self.0.as_bytes()).into(); + } + } + false + } +} diff --git a/src/auth/basic.rs b/src/auth/basic.rs new file mode 100644 index 0000000..50be8a0 --- /dev/null +++ b/src/auth/basic.rs @@ -0,0 +1,24 @@ +use crate::auth::types::AuthValidator; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use pingora_proxy::Session; +use subtle::ConstantTimeEq; + +pub struct BasicAuth<'a>(pub(crate) &'a str); +#[async_trait::async_trait] +impl AuthValidator for BasicAuth<'_> { + async fn validate(&self, session: &mut Session) -> bool { + if let Some(header) = session.get_header("authorization") { + if let Ok(h) = header.to_str() { + if let Some((_, val)) = h.split_once(' ') { + if let Ok(decoded) = STANDARD.decode(val) { + if decoded.as_slice().ct_eq(self.0.as_bytes()).into() { + return true; + } + } + } + } + } + false + } +} diff --git a/src/auth/forward.rs b/src/auth/forward.rs new file mode 100644 index 0000000..88d878f --- /dev/null +++ b/src/auth/forward.rs @@ -0,0 +1,131 @@ +use crate::auth::types::AuthValidator; +use crate::utils::tools::split_host_port; +use axum::http::StatusCode; +use pingora_core::connectors::http::Connector; +use pingora_core::prelude::HttpPeer; +use pingora_http::{RequestHeader, ResponseHeader}; +use pingora_proxy::Session; +use std::sync::LazyLock; + +static AUTH_CONNECTOR: LazyLock = LazyLock::new(|| Connector::new(None)); +pub struct ForwardAuth<'a>(pub(crate) &'a str); +#[async_trait::async_trait] +impl AuthValidator for ForwardAuth<'_> { + async fn validate(&self, session: &mut Session) -> bool { + let method = match session.req_header().method.as_str() { + "HEAD" => "HEAD", + _ => "GET", + }; + + let auth_url = self.0; + + let (plain, tls) = if let Some(p) = auth_url.strip_prefix("http://") { + (p, false) + } else if let Some(p) = auth_url.strip_prefix("https://") { + (p, true) + } else { + return false; + }; + + let (addr, uri) = if let Some(pos) = plain.find('/') { + (&plain[..pos], &plain[pos..]) + } else { + (plain, "/") + }; + + let hp = match split_host_port(addr, tls) { + Some(hp) => hp, + None => return false, + }; + + let peer = HttpPeer::new((hp.0, hp.1), tls, hp.0.to_string()); + + let (mut http_session, _) = match AUTH_CONNECTOR.get_http_session(&peer).await { + Ok(s) => s, + Err(e) => { + log::warn!("ForwardAuth: connect failed: {}", e); + return false; + } + }; + + let mut auth_req = match RequestHeader::build(method, uri.as_bytes(), None) { + Ok(r) => r, + Err(e) => { + log::warn!("ForwardAuth: failed to build request: {}", e); + return false; + } + }; + + // auth_req.headers = session.req_header().headers.clone(); + auth_req.insert_header("Host", addr).ok(); + auth_req.insert_header("X-Forwarded-Uri", uri).ok(); + auth_req.insert_header("X-Forwarded-Method", session.req_header().method.as_str()).ok(); + if let Some(auth) = session.req_header().headers.get("authorization") { + auth_req.insert_header("Authorization", auth.clone()).ok(); + } + + if let Some(cookie) = session.req_header().headers.get("cookie") { + auth_req.insert_header("Cookie", cookie.clone()).ok(); + } + + if tls { + auth_req.insert_header("X-Forwarded-Proto", "https").ok(); + } else { + auth_req.insert_header("X-Forwarded-Proto", "http").ok(); + } + + if let Err(e) = http_session.write_request_header(Box::new(auth_req)).await { + log::warn!("ForwardAuth: write failed: {}", e); + return false; + } + + let status = match http_session.read_response_header().await { + Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500), + Err(e) => { + log::warn!("ForwardAuth: read failed: {}", e); + return false; + } + }; + + let auth_headers_to_forward: Vec<(String, String)> = if let Some(resp_header) = http_session.response_header() { + resp_header + .headers + .iter() + .filter_map(|(name, value)| { + let name_str = name.as_str(); + if name_str.starts_with("x-") || name_str.starts_with("remote-") || name_str.starts_with("locat") { + value.to_str().ok().map(|v| (name_str.to_string(), v.to_string())) + } else { + None + } + }) + .collect() + } else { + Vec::new() + }; + + AUTH_CONNECTOR.release_http_session(http_session, &peer, None).await; + + if (200..300).contains(&status) { + for (name, value) in auth_headers_to_forward { + session.req_header_mut().insert_header(name, value).ok(); + } + true + } else if status == 302 || status == 301 { + let resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None); + match resp { + Ok(mut r) => { + for (name, value) in auth_headers_to_forward { + r.insert_header(name, value).ok(); + } + let _ = r.insert_header("Content-Length", "0"); + let _ = session.write_response_header(Box::new(r), true).await; + true + } + Err(_) => return false, + } + } else { + false + } + } +} diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs new file mode 100644 index 0000000..2c2fd0e --- /dev/null +++ b/src/auth/jwt.rs @@ -0,0 +1,42 @@ +use crate::auth::types::AuthValidator; +use crate::utils::jwt::{check_jwt, JWT_TOKEN}; +use pingora_proxy::Session; +use std::collections::HashMap; +use urlencoding::decode; + +pub struct JwtAuth(); +#[async_trait::async_trait] +impl AuthValidator for JwtAuth { + async fn validate(&self, session: &mut Session) -> bool { + if let Some(jwtsecret) = JWT_TOKEN.clone() { + if let Some(tok) = get_query_param(session, "araleztoken") { + return check_jwt(tok.as_str(), jwtsecret.as_ref()); + } + if let Some(auth_header) = session.get_header("authorization") { + if let Ok(header_str) = auth_header.to_str() { + if let Some((scheme, token)) = header_str.split_once(' ') { + if scheme.eq_ignore_ascii_case("bearer") { + return check_jwt(token, jwtsecret.as_ref()); + } + } + } + } + } + false + } +} + +fn get_query_param(session: &mut Session, key: &str) -> Option { + let query = session.req_header().uri.query()?; + + let params: HashMap<_, _> = query + .split('&') + .filter_map(|pair| { + let mut parts = pair.splitn(2, '='); + let k = parts.next()?; + let v = parts.next().unwrap_or(""); // Some params might have no value + Some((k, v)) + }) + .collect(); + params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string()) +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..3848e31 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,7 @@ +pub mod apikey; +pub mod basic; +pub mod forward; +pub mod jwt; +pub mod types; + +pub use types::*; diff --git a/src/auth/types.rs b/src/auth/types.rs new file mode 100644 index 0000000..f739125 --- /dev/null +++ b/src/auth/types.rs @@ -0,0 +1,23 @@ +use crate::auth::apikey::ApiKeyAuth; +use crate::auth::basic::BasicAuth; +use crate::auth::forward::ForwardAuth; +use crate::auth::jwt::JwtAuth; +use crate::utils::structs::InnerAuth; +use pingora_proxy::Session; + +#[async_trait::async_trait] +pub trait AuthValidator { + async fn validate(&self, session: &mut Session) -> bool; +} +pub async fn authenticate(auth: &InnerAuth, session: &mut Session) -> bool { + match &*auth.auth_type { + "basic" => BasicAuth(&*auth.auth_cred).validate(session).await, + "apikey" => ApiKeyAuth(&*auth.auth_cred).validate(session).await, + "jwt" => JwtAuth().validate(session).await, + "forward" => ForwardAuth(&*auth.auth_cred).validate(session).await, + _ => { + log::warn!("Unsupported authentication mechanism : {}", &*auth.auth_type); + false + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1007601 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod tls; +pub mod utils; +pub mod web; diff --git a/src/main.rs b/src/main.rs index 4041c76..80c719a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,4 @@ -mod tls; -mod utils; -mod web; +use aralez::web; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; diff --git a/src/utils/auth.rs b/src/utils/auth.rs index 5b3343d..7db5065 100644 --- a/src/utils/auth.rs +++ b/src/utils/auth.rs @@ -1,3 +1,4 @@ +/* use crate::utils::jwt::{check_jwt, JWT_TOKEN}; use crate::utils::structs::InnerAuth; pub(crate) use crate::utils::tools::split_host_port; @@ -224,23 +225,4 @@ pub fn get_query_param(session: &mut Session, key: &str) -> Option { .collect(); params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string()) } - -// #[allow(clippy::needless_return)] -// pub fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> { -// match addr.split_once(':') { -// Some((h, p)) => match p.parse::() { -// Ok(port) => return Some((h, port, tls, h)), -// Err(_) => { -// log::warn!("ForwardAuth: invalid port in {}", addr); -// return None; -// } -// }, -// None => { -// if tls { -// return Some((addr, 443u16, tls, addr)); -// } else { -// return Some((addr, 80u16, tls, addr)); -// } -// } -// }; -// } +*/ diff --git a/src/web/proxyhttp.rs b/src/web/proxyhttp.rs index 79ec698..a7ec6d2 100644 --- a/src/web/proxyhttp.rs +++ b/src/web/proxyhttp.rs @@ -1,4 +1,4 @@ -use crate::utils::auth::authenticate; +use crate::auth::authenticate; use crate::utils::lazylock::{CACHE_LOCK, CACHE_TTL, EVICTION, LOCALHOST, MEM_CACHE, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE}; use crate::utils::metrics::*; use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap}; diff --git a/src/web/webserver.rs b/src/web/webserver.rs index eb67ad0..76619ac 100644 --- a/src/web/webserver.rs +++ b/src/web/webserver.rs @@ -30,10 +30,10 @@ struct OutToken { } #[derive(Clone)] -pub(crate) struct AppState { +pub struct AppState { master_key: Option, - pub(crate) cert_creds: String, - pub(crate) certs_dir: String, + pub cert_creds: String, + pub certs_dir: String, upstreams_file: String, config_sender: mpsc::Sender, config_api_enabled: bool,