diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..02b30e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM debian:trixie-slim + +RUN apt-get update && apt-get install -y ca-certificates curl net-tools iputils-ping +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY aralez /usr/local/bin/aralez + +RUN chmod +x /usr/local/bin/aralez +RUN mkdir -p /etc/aralez/certs/upstreams + +WORKDIR /etc/aralez + +ENTRYPOINT ["/usr/local/bin/aralez", "-c", "/etc/aralez/main.yaml"] diff --git a/README.md b/README.md index 5c4bafa..ffa66c2 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,7 @@ For getting the best performance on newer hardware use `aralez-x86_64-*.gz`. **Via docker** ```shell -docker run -d \ - -v /local/path/to/config:/etc/aralez:ro \ - -p 80:80 \ - -p 443:443 \ - sadoyan/aralez +docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 sadoyan/aralez ``` ## Running the Proxy diff --git a/src/utils/auth.rs b/src/utils/auth.rs index 143f601..3d937c6 100644 --- a/src/utils/auth.rs +++ b/src/utils/auth.rs @@ -1,21 +1,17 @@ -use crate::utils::jwt::check_jwt; -// use reqwest::Client; +use crate::utils::jwt::{check_jwt, JWT_TOKEN}; +use crate::utils::structs::InnerAuth; use axum::http::StatusCode; use base64::engine::general_purpose::STANDARD; use base64::Engine; -use pingora_proxy::Session; -use std::collections::HashMap; -use std::sync::{Arc, LazyLock}; -use subtle::ConstantTimeEq; -use urlencoding::decode; - -// use pingora::http::{RequestHeader, ResponseHeader, StatusCode}; use pingora::http::RequestHeader; -// --------------------------------- // use pingora_core::connectors::http::Connector; use pingora_core::upstreams::peer::HttpPeer; use pingora_http::ResponseHeader; -// --------------------------------- // +use pingora_proxy::Session; +use std::collections::HashMap; +use std::sync::LazyLock; +use subtle::ConstantTimeEq; +use urlencoding::decode; #[async_trait::async_trait] trait AuthValidator { @@ -23,7 +19,7 @@ trait AuthValidator { } struct BasicAuth<'a>(&'a str); struct ApiKeyAuth<'a>(&'a str); -struct JwtAuth<'a>(&'a str); +struct JwtAuth(); struct ForwardAuth<'a>(&'a str); pub static AUTH_CONNECTOR: LazyLock = LazyLock::new(|| Connector::new(None)); @@ -180,17 +176,18 @@ impl AuthValidator for ApiKeyAuth<'_> { } #[async_trait::async_trait] -impl AuthValidator for JwtAuth<'_> { +impl AuthValidator for JwtAuth { async fn validate(&self, session: &mut Session) -> bool { - let jwtsecret = self.0; - if let Some(tok) = get_query_param(session, "araleztoken") { - return check_jwt(tok.as_str(), jwtsecret); - } - 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); + 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()); + } } } } @@ -199,14 +196,14 @@ impl AuthValidator for JwtAuth<'_> { } } -pub async fn authenticate(auth_type: &Arc, credentials: &Arc, session: &mut Session) -> bool { - match &**auth_type { - "basic" => BasicAuth(credentials).validate(session).await, - "apikey" => ApiKeyAuth(credentials).validate(session).await, - "jwt" => JwtAuth(credentials).validate(session).await, - "forward" => ForwardAuth(credentials).validate(session).await, +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_type); + log::warn!("Unsupported authentication mechanism : {}", &*auth.auth_type); false } } diff --git a/src/utils/discovery.rs b/src/utils/discovery.rs index c69bb6e..43e37fe 100644 --- a/src/utils/discovery.rs +++ b/src/utils/discovery.rs @@ -9,13 +9,10 @@ use std::sync::Arc; pub struct APIUpstreamProvider { pub config_api_enabled: bool, pub address: String, - pub masterkey: String, + pub masterkey: Option, pub certs_dir: String, pub config_dir: String, pub upstreams_file: String, - // pub tls_address: Option, - // pub tls_certificate: Option, - // pub tls_key_file: Option, pub file_server_address: Option, pub file_server_folder: Option, pub current_upstreams: Arc, diff --git a/src/utils/healthcheck.rs b/src/utils/healthcheck.rs index 19e9922..b4d9a01 100644 --- a/src/utils/healthcheck.rs +++ b/src/utils/healthcheck.rs @@ -69,6 +69,7 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie is_http2: is_h2, to_https: upstream.to_https, rate_limit: upstream.rate_limit, + x4xx_limit: upstream.x4xx_limit, healthcheck: upstream.healthcheck, redirect_to: upstream.redirect_to.clone(), authorization: upstream.authorization.clone(), diff --git a/src/utils/httpclient.rs b/src/utils/httpclient.rs index 04f4c9c..af8c3d8 100644 --- a/src/utils/httpclient.rs +++ b/src/utils/httpclient.rs @@ -32,6 +32,7 @@ pub async fn for_consul(url: String, token: Option, conf: &GlobalService is_http2: false, to_https: conf.to_https.unwrap_or(false), rate_limit: conf.rate_limit, + x4xx_limit: conf.x4xx_limit, redirect_to: None, healthcheck: None, authorization: None, @@ -68,6 +69,7 @@ pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> O is_http2: false, to_https: conf.to_https.unwrap_or(false), rate_limit: conf.rate_limit, + x4xx_limit: conf.x4xx_limit, healthcheck: None, redirect_to: None, authorization: None, diff --git a/src/utils/jwt.rs b/src/utils/jwt.rs index 55d7ea3..e1b003b 100644 --- a/src/utils/jwt.rs +++ b/src/utils/jwt.rs @@ -4,8 +4,9 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use moka::sync::Cache; use moka::Expiry; use serde::{Deserialize, Serialize}; +use std::env; use std::hash::{Hash, Hasher}; -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; use std::time::{Duration, Instant, SystemTime}; #[derive(Debug, Serialize, Deserialize)] @@ -23,6 +24,11 @@ struct Expired { static JWT_VALIDATION: LazyLock = LazyLock::new(|| Validation::new(Algorithm::HS256)); +pub static JWT_TOKEN: LazyLock>> = LazyLock::new(|| match env::var("JWT_KEY") { + Ok(key) if !key.is_empty() => Some(Arc::from(key.as_str())), + _ => None, +}); + static JWT_CACHE: LazyLock> = LazyLock::new(|| Cache::builder().max_capacity(100_000).expire_after(JwtExpiry).build()); struct JwtExpiry; impl Expiry for JwtExpiry { diff --git a/src/utils/parceyaml.rs b/src/utils/parceyaml.rs index f082d92..0180285 100644 --- a/src/utils/parceyaml.rs +++ b/src/utils/parceyaml.rs @@ -11,10 +11,10 @@ use log4rs::{ encode::pattern::PatternEncoder, }; use std::collections::HashMap; -use std::fs; use std::path::Path; use std::sync::atomic::AtomicUsize; use std::sync::{Arc, LazyLock}; +use std::{env, fs}; pub static DOMAINS: LazyLock> = LazyLock::new(DashMap::new); @@ -85,6 +85,7 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option, let mut parsed: Config = match serde_yml::from_str(&yaml_data) { Ok(cfg) => cfg, Err(e) => { + println!("================================================"); error!("Failed to parse upstreams file: {}", e); return (None, e.to_string()); } @@ -136,6 +137,7 @@ async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) } } } + let global_headers: DashMap, Vec<(String, Arc)>> = DashMap::new(); global_headers.insert(Arc::from("/"), ch); config.client_headers.insert(Arc::from("GLOBAL_CLIENT_HEADERS"), global_headers); @@ -154,6 +156,7 @@ async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) config.extraparams.to_https = parsed.to_https; config.extraparams.sticky_sessions = parsed.sticky_sessions; config.extraparams.rate_limit = parsed.rate_limit; + config.extraparams.x4xx_limit = parsed.x4xx_limit; if let Some(rate) = &parsed.rate_limit { info!("Applied Global Rate Limit : {} request per second", rate); @@ -162,7 +165,7 @@ async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) if let Some(pa) = &parsed.authorization { let y: InnerAuth = InnerAuth { auth_type: Arc::from(pa.auth_type.clone()), - auth_cred: Arc::from(pa.auth_cred.clone()), + auth_cred: Arc::from(pa.auth_cred.clone().unwrap_or_default()), }; config.extraparams.authentication = Some(Arc::from(y)); } @@ -191,10 +194,11 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) { if let Some(pa) = &path_config.authorization { let y: InnerAuth = InnerAuth { auth_type: Arc::from(pa.auth_type.clone()), - auth_cred: Arc::from(pa.auth_cred.clone()), + auth_cred: Arc::from(pa.auth_cred.clone().unwrap_or_default()), }; path_auth = Some(Arc::from(y)); } + let redirect_link = path_config.redirect_to.as_ref().map(|www| Arc::from(www.as_str())); if let Some((ip, port_str)) = server.split_once(':') { if let Ok(port) = port_str.parse::() { @@ -205,6 +209,7 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) { is_http2: false, to_https: path_config.to_https.unwrap_or(false), rate_limit: path_config.rate_limit, + x4xx_limit: path_config.x4xx_limit, healthcheck: path_config.healthcheck, redirect_to: redirect_link, authorization: path_auth, @@ -236,6 +241,11 @@ pub fn parce_main_config(path: &str) -> AppConfig { let reply = DashMap::new(); let cfg: HashMap = serde_yml::from_str(&data).expect("Failed to parse main config file"); let mut cfo: AppConfig = serde_yml::from_str(&data).expect("Failed to parse main config file"); + + if let Ok(jwt_key) = env::var("JWT_KEY") { + cfo.master_key = Some(jwt_key); + }; + log_builder(&cfo, &cfo.log_file); cfo.hc_method = cfo.hc_method.to_uppercase(); for (k, v) in cfg { @@ -287,27 +297,6 @@ fn parce_tls_grades(what: Option) -> Option { } } -/* -fn log_builder1(conf: &AppConfig) { - let log_level = conf.log_level.clone(); - unsafe { - match log_level.as_str() { - "info" => env::set_var("RUST_LOG", "info"), - "error" => env::set_var("RUST_LOG", "error"), - "warn" => env::set_var("RUST_LOG", "warn"), - "debug" => env::set_var("RUST_LOG", "debug"), - "trace" => env::set_var("RUST_LOG", "trace"), - "off" => env::set_var("RUST_LOG", "off"), - _ => { - println!("Error reading log level, defaulting to: INFO"); - env::set_var("RUST_LOG", "info") - } - } - } - env_logger::builder().init(); -} -*/ - pub fn build_headers(path_config: &Option>, _config: &Configuration, hl: &mut Vec<(String, Arc)>) { if let Some(headers) = &path_config { for header in headers { diff --git a/src/utils/structs.rs b/src/utils/structs.rs index 7f31b0b..1a64ef9 100644 --- a/src/utils/structs.rs +++ b/src/utils/structs.rs @@ -14,9 +14,10 @@ pub type Headers = DashMap, DashMap, Vec<(String, Arc)>>> #[derive(Clone, Debug, Default)] pub struct Extraparams { pub to_https: Option, - pub sticky_sessions: bool, + pub sticky_sessions: Option, pub authentication: Option>, pub rate_limit: Option, + pub x4xx_limit: Option, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -25,8 +26,9 @@ pub struct GlobalServiceMapping { pub hostname: String, pub path: Option, pub to_https: Option, - pub sticky_sessions: Option, + pub sticky_sessions: Option, pub rate_limit: Option, + pub x4xx_limit: Option, pub client_headers: Option>, pub server_headers: Option>, } @@ -48,7 +50,7 @@ pub struct Consul { pub struct Config { pub provider: String, pub to_https: Option, - pub sticky_sessions: bool, + pub sticky_sessions: Option, #[serde(default)] pub upstreams: Option>, #[serde(default)] @@ -65,28 +67,30 @@ pub struct Config { pub kubernetes: Option, #[serde(default)] pub rate_limit: Option, + pub x4xx_limit: Option, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct HostConfig { pub paths: HashMap, pub rate_limit: Option, + pub x4xx_limit: Option, } #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct Auth { #[serde(rename = "type")] pub auth_type: String, #[serde(rename = "data")] - pub auth_cred: String, + pub auth_cred: Option, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct PathConfig { pub servers: Vec, pub to_https: Option, - pub sticky_sessions: Option, pub client_headers: Option>, pub server_headers: Option>, pub rate_limit: Option, + pub x4xx_limit: Option, pub healthcheck: Option, pub redirect_to: Option, pub authorization: Option, @@ -108,7 +112,7 @@ pub struct AppConfig { pub hc_method: String, pub upstreams_conf: String, pub log_level: String, - pub master_key: String, + pub master_key: Option, pub config_address: String, pub proxy_address_http: String, pub config_api_enabled: bool, @@ -142,6 +146,7 @@ pub struct InnerMap { pub is_http2: bool, pub to_https: bool, pub rate_limit: Option, + pub x4xx_limit: Option, pub healthcheck: Option, pub redirect_to: Option>, pub authorization: Option>, @@ -157,6 +162,7 @@ impl InnerMap { is_http2: Default::default(), to_https: Default::default(), rate_limit: Default::default(), + x4xx_limit: Default::default(), healthcheck: Default::default(), redirect_to: Default::default(), authorization: Default::default(), @@ -172,6 +178,7 @@ pub struct InnerMapForJson { pub is_http2: bool, pub to_https: bool, pub rate_limit: Option, + pub x4xx_limit: Option, pub healthcheck: Option, } #[derive(Debug, Default, Serialize, Deserialize)] diff --git a/src/utils/tools.rs b/src/utils/tools.rs index e8ffa5d..0fcc199 100644 --- a/src/utils/tools.rs +++ b/src/utils/tools.rs @@ -29,13 +29,14 @@ pub fn print_upstreams(upstreams: &UpstreamsDashMap) { for f in path_entry.value().0.clone() { writeln!( out, - " IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}, Rate Limit: {}", + " IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}, Rate Limit: {}, 4xx Limit: {}", f.address, f.port, f.is_ssl, f.is_http2, f.to_https, - f.rate_limit.unwrap_or(0) + f.rate_limit.unwrap_or(0), + f.x4xx_limit.unwrap_or(0) ) .unwrap(); } @@ -152,13 +153,14 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) { let mut id = String::new(); write!( &mut id, - "{}:{}:{}:{}:{}:{}:{}:{:?}", + "{}:{}:{}:{}:{}:{}:{}:{}:{:?}", outer_entry.key(), x.address, x.port, x.is_http2, x.to_https, x.rate_limit.unwrap_or_default(), + x.x4xx_limit.unwrap_or_default(), x.healthcheck.unwrap_or_default(), x.authorization ) @@ -177,6 +179,7 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) { is_http2: false, to_https: false, rate_limit: None, + x4xx_limit: None, healthcheck: None, redirect_to: None, authorization: None, @@ -303,6 +306,7 @@ pub fn upstreams_to_json(upstreams: &UpstreamsDashMap) -> serde_json::Result> = LazyLock::new(DashMap::new); thread_local! {static IP_BUFFER: RefCell = RefCell::new(String::with_capacity(50));} pub static RATE_LIMITER: LazyLock = LazyLock::new(|| Rate::new(Duration::from_secs(1))); +pub static REQUESTS_4XX: LazyLock> = LazyLock::new(|| Cache::builder().time_to_live(Duration::from_secs(1)).build()); pub static LOCALHOST: LazyLock> = LazyLock::new(|| Arc::from("localhost")); #[derive(Clone)] @@ -39,12 +42,12 @@ pub struct LB { pub struct Context { backend_id: Option, - sticky_sessions: bool, start_time: Instant, hostname: Option>, upstream_peer: Option>, extraparams: arc_swap::Guard>, client_headers: Option)>>, + x4xx_limit: Option, } #[async_trait] @@ -53,12 +56,12 @@ impl ProxyHttp for LB { fn new_ctx(&self) -> Self::CTX { Context { backend_id: None, - sticky_sessions: false, start_time: Instant::now(), hostname: None, upstream_peer: None, extraparams: self.extraparams.load(), client_headers: None, + x4xx_limit: None, } } async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result { @@ -66,7 +69,7 @@ impl ProxyHttp for LB { let hostname = return_header_host_from_upstream(session, &self.ump_upst); _ctx.hostname = hostname; let mut backend_id = None; - if _ctx.extraparams.sticky_sessions { + if let Some(_) = _ctx.extraparams.sticky_sessions { if let Some(cookies) = session.req_header().headers.get("cookie") { if let Ok(cookie_str) = cookies.to_str() { if let Some(pos) = cookie_str.find("backend_id=") { @@ -85,13 +88,28 @@ impl ProxyHttp for LB { None => return Ok(false), Some(ref innermap) => { if let Some(auth) = _ctx.extraparams.authentication.as_ref().or(innermap.authorization.as_ref()) { - if !authenticate(&auth.auth_type, &auth.auth_cred, session).await { + if !authenticate(&auth, session).await { let _ = session.respond_error(401).await; warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path()); return Ok(true); } } - + if let Some(rate) = innermap.x4xx_limit.or(_ctx.extraparams.x4xx_limit) { + _ctx.x4xx_limit = innermap.x4xx_limit; + let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip()); + if let Some(rk) = rate_key { + let count = REQUESTS_4XX.get(&rk).unwrap_or(0); + if count > rate { + let header = ResponseHeader::build(429, None)?; + session.set_keepalive(None); + session.write_response_header(Box::new(header), true).await?; + if let (Some(oi), Some(oa)) = (&_ctx.hostname, rate_key) { + warn!("Limit 4XX: {}-rps exceed on {} from {}", rate, oi, oa); + } + return Ok(true); + } + } + } if let Some(rate) = innermap.rate_limit.or(_ctx.extraparams.rate_limit) { let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip()); let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1); @@ -99,7 +117,9 @@ impl ProxyHttp for LB { let header = ResponseHeader::build(429, None)?; session.set_keepalive(None); session.write_response_header(Box::new(header), true).await?; - debug!("Rate limited: {:?}, {}", rate_key, rate); + if let (Some(oi), Some(oa)) = (&_ctx.hostname, rate_key) { + warn!("Limit: {}-rps exceed on {} from {}", rate, oi, oa); + } return Ok(true); } } @@ -161,7 +181,7 @@ impl ProxyHttp for LB { peer.options.verify_cert = false; peer.options.verify_hostname = false; } - if ctx.extraparams.sticky_sessions { + if let Some(_) = ctx.extraparams.sticky_sessions { let mut s = String::with_capacity(64); write!( &mut s, @@ -177,7 +197,6 @@ impl ProxyHttp for LB { ) .unwrap_or(()); ctx.backend_id = Some(s); - ctx.sticky_sessions = true; } Ok(peer) } @@ -237,7 +256,7 @@ impl ProxyHttp for LB { Ok(()) } async fn response_filter(&self, _session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> { - if ctx.sticky_sessions { + if let Some(val) = ctx.extraparams.sticky_sessions { if let Some(bid) = &ctx.backend_id { let tt = if let Some(existing) = REVERSE_STORE.get(bid) { existing.value().clone() @@ -255,7 +274,11 @@ impl ProxyHttp for LB { let mut buf = String::with_capacity(80); buf.push_str("backend_id="); buf.push_str(&tt); - buf.push_str("; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax"); + buf.push_str("; Path=/; Max-Age="); + buf.push_str(&val.to_string()); + buf.push_str("; HttpOnly; SameSite=Lax"); + // buf.push_str("; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax"); + // println!("{}", buf); let _ = _upstream_response.insert_header("set-cookie", buf.as_str()); } } @@ -280,6 +303,14 @@ impl ProxyHttp for LB { }; calc_metrics(m); ACTIVE_SESSIONS.dec(); + if let Some(_) = ctx.x4xx_limit.or(ctx.extraparams.x4xx_limit) { + if 400 <= response_code && response_code <= 499 { + if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) { + let current = REQUESTS_4XX.get(&ip).unwrap_or(0); + REQUESTS_4XX.insert(ip, current + 1); + } + } + } } } diff --git a/src/web/start.rs b/src/web/start.rs index 0bde5bf..f620b20 100644 --- a/src/web/start.rs +++ b/src/web/start.rs @@ -33,9 +33,10 @@ pub fn run() { let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams { to_https: None, - sticky_sessions: false, + sticky_sessions: None, authentication: None, rate_limit: None, + x4xx_limit: None, })); let cfg = Arc::new(maincfg); diff --git a/src/web/webserver.rs b/src/web/webserver.rs index 7d9971e..75e747d 100644 --- a/src/web/webserver.rs +++ b/src/web/webserver.rs @@ -8,7 +8,7 @@ use crate::utils::structs::{Config, Configuration, UpstreamsDashMap}; use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json}; use axum::body::Body; use axum::extract::{Query, State}; -use axum::http::{header::HeaderMap, Response, StatusCode}; +use axum::http::{Response, StatusCode}; use axum::response::IntoResponse; use axum::routing::{any, get, post}; use axum::{Json, Router}; @@ -21,7 +21,6 @@ use serde::Serialize; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use subtle::ConstantTimeEq; use tokio::net::TcpListener; use tower_http::services::ServeDir; @@ -32,7 +31,7 @@ struct OutToken { #[derive(Clone)] struct AppState { - master_key: String, + master_key: Option, cert_creds: String, certs_dir: String, upstreams_file: String, @@ -95,32 +94,27 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender, Query(params): Query>, headers: HeaderMap, content: String) -> impl IntoResponse { +async fn conf(State(st): State, Query(params): Query>, content: String) -> impl IntoResponse { if !st.config_api_enabled { return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap(); } - // if let Some(s) = headers.get("x-api-key").and_then(|v| v.to_str().ok()).or(params.get("key").map(|s| s.as_str())) { - if key_authorization(&headers, ¶ms, &st.master_key) { - let strcontent = content.as_str(); - let parsed = serde_yml::from_str::(strcontent); - match parsed { - Ok(_) => { - if let Some(_) = params.get("save") { - drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await })); - } else { - drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await })); - } - // apply_config(content.as_str(), st).await; - return Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap(); - } - Err(err) => { - error!("Failed to parse upstreams file: {}", err); - return Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from(format!("Failed: {}\n", err))).unwrap(); + let strcontent = content.as_str(); + let parsed = serde_yml::from_str::(strcontent); + match parsed { + Ok(_) => { + if let Some(_) = params.get("save") { + drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await })); + } else { + drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await })); } + Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap() + } + Err(err) => { + error!("Failed to parse upstreams file: {}", err); + Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from(format!("Failed: {}\n", err))).unwrap() } } - Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap() } async fn apply_config(content: &str, mut st: AppState, save: bool) { @@ -137,34 +131,42 @@ async fn apply_config(content: &str, mut st: AppState, save: bool) { } async fn jwt_gen(State(state): State, Json(payload): Json) -> (StatusCode, Json) { - if payload.master_key == state.master_key { - let now = SystemTime::now() + Duration::from_secs(payload.exp * 60); - let expire = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + if let Some(master_key) = &state.master_key { + if &payload.master_key == master_key { + let now = SystemTime::now() + Duration::from_secs(payload.exp * 60); + let expire = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); - let claim = Claims { - master_key: String::new(), - owner: payload.owner, - exp: expire, - random: payload.random, - }; - match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) { - Ok(t) => { - let tok = OutToken { token: t }; - debug!("Generating token: {:?}", tok.token); - (StatusCode::CREATED, Json(tok)) - } - Err(e) => { - let tok = OutToken { token: "ERROR".to_string() }; - error!("Failed to generate token: {:?}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(tok)) + let claim = Claims { + master_key: String::new(), + owner: payload.owner, + exp: expire, + random: payload.random, + }; + match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) { + Ok(t) => { + let tok = OutToken { token: t }; + debug!("Generating token: {:?}", tok.token); + (StatusCode::CREATED, Json(tok)) + } + Err(e) => { + let tok = OutToken { token: "ERROR".to_string() }; + error!("Failed to generate token: {:?}", e); + (StatusCode::INTERNAL_SERVER_ERROR, Json(tok)) + } } + } else { + let tok = OutToken { + token: "Unauthorised".to_string(), + }; + warn!("Unauthorised JWT generate request: {:?}", tok); + (StatusCode::FORBIDDEN, Json(tok)) } } else { let tok = OutToken { - token: "Unauthorised".to_string(), + token: "ERROR Getting JWT_KEY environment variable".to_string(), }; - warn!("Unauthorised JWT generate request: {:?}", tok); - (StatusCode::FORBIDDEN, Json(tok)) + error!("ERROR Getting JWT_KEY environment variable"); + (StatusCode::INTERNAL_SERVER_ERROR, Json(tok)) } } @@ -221,11 +223,7 @@ async fn status(State(st): State, Query(params): Query, Query(params): Query>, headers: HeaderMap) -> impl IntoResponse { - if !key_authorization(&headers, ¶ms, &state.master_key) { - return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap(); - } - +async fn acme_create(State(state): State) -> impl IntoResponse { match account::load_or_create(state.cert_creds.as_str()).await { Ok(txt) => { return Response::builder() @@ -243,16 +241,7 @@ async fn acme_create(State(state): State, Query(params): Query, - axum::extract::Path(domain): axum::extract::Path, - Query(params): Query>, - headers: HeaderMap, -) -> impl IntoResponse { - if !key_authorization(&headers, ¶ms, &state.master_key) { - return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap(); - } - +async fn acme_order(State(state): State, axum::extract::Path(domain): axum::extract::Path) -> impl IntoResponse { let domain_clean = domain.trim_matches('/'); match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await { Ok(txt) => { @@ -292,13 +281,13 @@ pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path, masterkey: &str) -> bool { - if let Some(s) = headers.get("x-api-key").and_then(|v| v.to_str().ok()).or(params.get("key").map(|s| s.as_str())) { - if s.as_bytes().ct_eq(masterkey.as_bytes()).into() { - return true; - } - } - false -} +// fn key_authorization(headers: &HeaderMap, params: &HashMap, masterkey: &str) -> bool { +// if let Some(s) = headers.get("x-api-key").and_then(|v| v.to_str().ok()).or(params.get("key").map(|s| s.as_str())) { +// if s.as_bytes().ct_eq(masterkey.as_bytes()).into() { +// return true; +// } +// } +// false +// } // -- ⚝ by Dave -- in NeoVim ⚝ --