4 Commits

Author SHA1 Message Date
Ara Sadoyan
f135106a44 Changes in authentication 2026-04-08 19:05:19 +02:00
Ara Sadoyan
389c12119a code cleanup and improvements. 2026-04-08 17:00:06 +02:00
Ara Sadoyan
93a8661281 Cargo cleanup, dependency merge 2026-04-08 15:14:46 +02:00
Ara Sadoyan
0505ce2849 split upstreams.yaml file 2026-03-30 19:04:32 +02:00
11 changed files with 847 additions and 893 deletions

1492
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ panic = "abort"
strip = true strip = true
[dependencies] [dependencies]
tokio = { version = "1.49.0", features = ["full"] } tokio = { version = "1.51.1", features = ["full"] }
pingora = { version = "0.8.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl pingora = { version = "0.8.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
dashmap = "7.0.0-rc2" dashmap = "7.0.0-rc2"
@@ -20,34 +20,27 @@ pingora-proxy = "0.8.0"
pingora-http = "0.8.0" pingora-http = "0.8.0"
pingora-limits = "0.8.0" pingora-limits = "0.8.0"
async-trait = "0.1.89" async-trait = "0.1.89"
env_logger = "0.11.9" env_logger = "0.11.10"
log = "0.4.29" log = "0.4.29"
futures = "0.3.32" futures = "0.3.32"
notify = "9.0.0-rc.2" notify = "9.0.0-rc.2"
axum = { version = "0.8.8" } axum = { version = "0.8.8" }
#axum-server = { version = "0.8.0" }
reqwest = { version = "0.13.2", features = ["json", "stream"] } reqwest = { version = "0.13.2", features = ["json", "stream"] }
serde_yaml = "0.9.34-deprecated" serde_yml = "0.0.12"
rand = "0.10.0" rand = "0.10.0"
base64 = "0.22.1" base64 = "0.22.1"
#jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] }
#jsonwebtoken = { version = "10.3.0", default-features = false, features = ["use_pem"] }
jsonwebtoken = { version = "10.3.0", default-features = false, features = ["use_pem", "rust_crypto"] } jsonwebtoken = { version = "10.3.0", default-features = false, features = ["use_pem", "rust_crypto"] }
tonic = "0.14.5" tonic = "0.14.5"
sha2 = { version = "0.11.0-rc.5", default-features = false } sha2 = { version = "0.11.0-rc.5", default-features = false }
base16ct = { version = "1.0.0", features = ["alloc"] } base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
arc-swap = "1.8.2" arc-swap = "1.9.1"
mimalloc = { version = "0.1.48", default-features = false } mimalloc = { version = "0.1.48", default-features = false }
prometheus = "0.14.0" prometheus = "0.14.0"
lazy_static = "1.5.0"
x509-parser = "0.18.1" x509-parser = "0.18.1"
rustls-pemfile = "2.2.0" rustls-pemfile = "2.2.0"
tower-http = { version = "0.6.8", features = ["fs"] } tower-http = { version = "0.6.8", features = ["fs"] }
once_cell = "1.21.3"
privdrop = "0.5.6" privdrop = "0.5.6"
ctrlc = "3.5.2" ctrlc = "3.5.2"
port_check = "0.3.0"
serde_json = "1.0.149" serde_json = "1.0.149"
http = "1.4.0" subtle = "2.6.1"
itoa = "1.0.14"

View File

@@ -122,13 +122,15 @@ Make the binary executable `chmod 755 ./aralez-VERSION` and run.
File names: File names:
| File Name | Description | | File Name | Description |
|---------------------------|--------------------------------------------------------------------------| |---------------------------------|--------------------------------------------------------------------------|
| `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency | | `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency |
| `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies | | `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies |
| `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency | | `aralez-x86_64-compat-musl.gz` | Static Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies | | `aralez-x86_64-compat-glibc.gz` | Dynamic Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `sadoyan/aralez` | Docker image on Debian 13 slim (https://hub.docker.com/r/sadoyan/aralez) | | `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies |
| `sadoyan/aralez` | Docker image on Debian 13 slim (https://hub.docker.com/r/sadoyan/aralez) |
**Via docker** **Via docker**

View File

@@ -4,6 +4,7 @@ use base64::Engine;
use pingora_proxy::Session; use pingora_proxy::Session;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use subtle::ConstantTimeEq;
use urlencoding::decode; use urlencoding::decode;
trait AuthValidator { trait AuthValidator {
@@ -16,10 +17,14 @@ struct JwtAuth<'a>(&'a str);
impl AuthValidator for BasicAuth<'_> { impl AuthValidator for BasicAuth<'_> {
fn validate(&self, session: &Session) -> bool { fn validate(&self, session: &Session) -> bool {
if let Some(header) = session.get_header("authorization") { if let Some(header) = session.get_header("authorization") {
if let Some((_, val)) = header.to_str().ok().unwrap().split_once(' ') { if let Some(h) = header.to_str().ok() {
let decoded = STANDARD.decode(val).ok().unwrap(); if let Some((_, val)) = h.split_once(' ') {
let decoded_str = String::from_utf8(decoded).ok().unwrap(); if let Some(decoded) = STANDARD.decode(val).ok() {
return decoded_str == self.0; if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true;
}
}
}
} }
} }
false false
@@ -29,7 +34,9 @@ impl AuthValidator for BasicAuth<'_> {
impl AuthValidator for ApiKeyAuth<'_> { impl AuthValidator for ApiKeyAuth<'_> {
fn validate(&self, session: &Session) -> bool { fn validate(&self, session: &Session) -> bool {
if let Some(header) = session.get_header("x-api-key") { if let Some(header) = session.get_header("x-api-key") {
return header.to_str().ok().unwrap() == self.0; if let Some(h) = header.to_str().ok() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into();
}
} }
false false
} }
@@ -53,27 +60,14 @@ impl AuthValidator for JwtAuth<'_> {
false false
} }
} }
fn validate(auth: &dyn AuthValidator, session: &Session) -> bool {
auth.validate(session)
}
// pub fn authenticate(c: &[Arc<str>], session: &Session) -> bool {
pub fn authenticate(auth_type: &Arc<str>, credentials: &Arc<str>, session: &Session) -> bool { pub fn authenticate(auth_type: &Arc<str>, credentials: &Arc<str>, session: &Session) -> bool {
match &*auth_type.clone() { match &**auth_type {
"basic" => { "basic" => BasicAuth(credentials).validate(session),
let auth = BasicAuth(&*credentials.clone()); "apikey" => ApiKeyAuth(credentials).validate(session),
validate(&auth, session) "jwt" => JwtAuth(credentials).validate(session),
}
"apikey" => {
let auth = ApiKeyAuth(&*credentials.clone());
validate(&auth, session)
}
"jwt" => {
let auth = JwtAuth(&*credentials.clone());
validate(&auth, session)
}
_ => { _ => {
println!("Unsupported authentication mechanism : {}", auth_type); log::warn!("Unsupported authentication mechanism : {}", auth_type);
false false
} }
} }
@@ -91,6 +85,5 @@ pub fn get_query_param(session: &Session, key: &str) -> Option<String> {
Some((k, v)) Some((k, v))
}) })
.collect(); .collect();
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
params.get(key).map(|v| decode(v).ok()).flatten().map(|s| s.to_string())
} }

View File

@@ -119,18 +119,11 @@ async fn http_request(url: &str, method: &str, payload: &str, client: &Client) -
} }
pub async fn ping_grpc(addr: &str) -> bool { pub async fn ping_grpc(addr: &str) -> bool {
let endpoint_result = Endpoint::from_shared(addr.to_owned()); let endpoint = match Endpoint::from_shared(addr.to_owned()) {
Ok(e) => e.timeout(Duration::from_secs(2)),
if let Ok(endpoint) = endpoint_result { Err(_) => return false,
let endpoint = endpoint.timeout(Duration::from_secs(2)); };
tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await.ok().and_then(Result::ok).is_some()
match tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await {
Ok(Ok(_channel)) => true,
_ => false,
}
} else {
false
}
} }
async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) { async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) {

View File

@@ -1,5 +1,5 @@
use http::method::Method; use pingora_http::Method;
use http::StatusCode; use pingora_http::StatusCode;
use pingora_http::Version; use pingora_http::Version;
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, IntCounterVec}; use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, IntCounterVec};
use std::sync::Arc; use std::sync::Arc;
@@ -12,46 +12,40 @@ pub struct MetricTypes {
pub latency: Duration, pub latency: Duration,
pub version: Version, pub version: Version,
} }
lazy_static::lazy_static! {
pub static ref REQUEST_COUNT: IntCounter = register_int_counter!( use std::sync::LazyLock;
"aralez_requests_total",
"Total number of requests handled by Aralez" pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap());
).unwrap();
pub static ref RESPONSE_CODES: IntCounterVec = register_int_counter_vec!( pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
"aralez_responses_total", LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap());
"Responses grouped by status code",
&["status"] pub static REQUEST_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
).unwrap(); register_histogram!(
pub static ref REQUEST_LATENCY: Histogram = register_histogram!(
"aralez_request_latency_seconds", "aralez_request_latency_seconds",
"Request latency in seconds", "Request latency in seconds",
vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0] vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
).unwrap(); )
pub static ref RESPONSE_LATENCY: Histogram = register_histogram!( .unwrap()
});
pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram!(
"aralez_response_latency_seconds", "aralez_response_latency_seconds",
"Response latency in seconds", "Response latency in seconds",
vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0] vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]
).unwrap(); )
pub static ref REQUESTS_BY_METHOD: IntCounterVec = register_int_counter_vec!( .unwrap()
"aralez_requests_by_method_total", });
"Number of requests by HTTP method",
&["method"] pub static REQUESTS_BY_METHOD: LazyLock<IntCounterVec> =
).unwrap(); LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_method_total", "Number of requests by HTTP method", &["method"]).unwrap());
pub static ref REQUESTS_BY_UPSTREAM: IntCounterVec = register_int_counter_vec!(
"aralez_requests_by_upstream", pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> =
"Number of requests by UPSTREAM server", LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_upstream", "Number of requests by UPSTREAM server", &["upstream"]).unwrap());
&["upstream"]
).unwrap(); pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> =
pub static ref REQUESTS_BY_VERSION: IntCounterVec = register_int_counter_vec!( LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).unwrap());
"aralez_requests_by_version_total",
"Number of requests by HTTP versions",
&["version"]
).unwrap();
pub static ref ERROR_COUNT: IntCounter = register_int_counter!(
"aralez_errors_total",
"Total number of errors"
).unwrap();
}
pub fn calc_metrics(metric_types: &MetricTypes) { pub fn calc_metrics(metric_types: &MetricTypes) {
REQUEST_COUNT.inc(); REQUEST_COUNT.inc();
@@ -66,7 +60,7 @@ pub fn calc_metrics(metric_types: &MetricTypes) {
_ => "Unknown", _ => "Unknown",
}; };
REQUESTS_BY_VERSION.with_label_values(&[&version_str]).inc(); REQUESTS_BY_VERSION.with_label_values(&[&version_str]).inc();
RESPONSE_CODES.with_label_values(&[metric_types.code.unwrap_or(http::StatusCode::GONE).as_str()]).inc(); RESPONSE_CODES.with_label_values(&[metric_types.code.unwrap_or(StatusCode::GONE).as_str()]).inc();
REQUESTS_BY_METHOD.with_label_values(&[&metric_types.method]).inc(); REQUESTS_BY_METHOD.with_label_values(&[&metric_types.method]).inc();
REQUESTS_BY_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc(); REQUESTS_BY_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc();
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64()); RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());

View File

@@ -57,7 +57,7 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>,
} }
}; };
let mut parsed: Config = match serde_yaml::from_str(&yaml_data) { let mut parsed: Config = match serde_yml::from_str(&yaml_data) {
Ok(cfg) => cfg, Ok(cfg) => cfg,
Err(e) => { Err(e) => {
error!("Failed to parse upstreams file: {}", e); error!("Failed to parse upstreams file: {}", e);
@@ -67,7 +67,7 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>,
if let Some(ref mut upstreams) = parsed.upstreams { if let Some(ref mut upstreams) = parsed.upstreams {
for uconf in conf_files { for uconf in conf_files {
let p: HashMap<String, HostConfig> = match serde_yaml::from_str(&uconf) { let p: HashMap<String, HostConfig> = match serde_yml::from_str(&uconf) {
Ok(ucfg) => ucfg, Ok(ucfg) => ucfg,
Err(e) => { Err(e) => {
error!("Failed to parse upstreams file: {}", e); error!("Failed to parse upstreams file: {}", e);
@@ -176,7 +176,7 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
server_list.push(Arc::from(InnerMap { server_list.push(Arc::from(InnerMap {
address: Arc::from(ip), address: Arc::from(ip),
port, port,
is_ssl: true, is_ssl: false,
is_http2: false, is_http2: false,
to_https: path_config.to_https.unwrap_or(false), to_https: path_config.to_https.unwrap_or(false),
rate_limit: path_config.rate_limit, rate_limit: path_config.rate_limit,
@@ -209,8 +209,8 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
pub fn parce_main_config(path: &str) -> AppConfig { pub fn parce_main_config(path: &str) -> AppConfig {
let data = fs::read_to_string(path).unwrap(); let data = fs::read_to_string(path).unwrap();
let reply = DashMap::new(); let reply = DashMap::new();
let cfg: HashMap<String, String> = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); let cfg: HashMap<String, String> = serde_yml::from_str(&*data).expect("Failed to parse main config file");
let mut cfo: AppConfig = serde_yaml::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");
log_builder(&cfo); log_builder(&cfo);
cfo.hc_method = cfo.hc_method.to_uppercase(); cfo.hc_method = cfo.hc_method.to_uppercase();
for (k, v) in cfg { for (k, v) in cfg {
@@ -265,7 +265,7 @@ fn parce_tls_grades(what: Option<String>) -> Option<String> {
}, },
None => { None => {
warn!("TLS grade not set, defaulting to: medium"); warn!("TLS grade not set, defaulting to: medium");
Some("b".to_string()) Some("medium".to_string())
} }
} }
} }

View File

@@ -1,12 +1,11 @@
use once_cell::sync::Lazy; use std::sync::{LazyLock, RwLock};
use std::sync::RwLock;
#[derive(Debug)] #[derive(Debug)]
pub struct SharedState { pub struct SharedState {
pub first_run: bool, pub first_run: bool,
} }
pub static GLOBAL_STATE: Lazy<RwLock<SharedState>> = Lazy::new(|| RwLock::new(SharedState { first_run: true })); pub static GLOBAL_STATE: LazyLock<RwLock<SharedState>> = LazyLock::new(|| RwLock::new(SharedState { first_run: true }));
pub fn mark_not_first_run() { pub fn mark_not_first_run() {
let mut state = GLOBAL_STATE.write().unwrap(); let mut state = GLOBAL_STATE.write().unwrap();

View File

@@ -4,7 +4,6 @@ use crate::utils::tls::CertificateConfig;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info}; use log::{error, info};
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use port_check::is_port_reachable;
use privdrop::PrivDrop; use privdrop::PrivDrop;
use serde_json::{json, Value}; use serde_json::{json, Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -12,6 +11,7 @@ use std::any::type_name;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::Write; use std::fmt::Write;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::net::TcpListener;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
@@ -227,13 +227,13 @@ pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> {
certificate_configs.push(y); certificate_configs.push(y);
} }
} }
for (_, v) in f.iter() { // for (_, v) in f.iter() {
let y = CertificateConfig { // let y = CertificateConfig {
cert_path: v[0].clone(), // cert_path: v[0].clone(),
key_path: v[1].clone(), // key_path: v[1].clone(),
}; // };
certificate_configs.push(y); // certificate_configs.push(y);
} // }
certificate_configs certificate_configs
} }
@@ -268,14 +268,14 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
loop { loop {
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
if is_port_reachable(http_addr.clone()) { if port_is_available(http_addr.clone()) {
break; break;
} }
} }
if let Some(tls_addr) = tls_addr { if let Some(tls_addr) = tls_addr {
loop { loop {
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
if is_port_reachable(tls_addr.clone()) { if port_is_available(tls_addr.clone()) {
break; break;
} }
} }
@@ -287,6 +287,13 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
} }
} }
fn port_is_available(addr: String) -> bool {
match TcpListener::bind(addr) {
Ok(_) => false,
Err(_) => true,
}
}
pub fn check_priv(addr: &str) { pub fn check_priv(addr: &str) {
let port = SocketAddr::from_str(addr).map(|sa| sa.port()).unwrap(); let port = SocketAddr::from_str(addr).map(|sa| sa.port()).unwrap();
match port < 1024 { match port < 1024 {

View File

@@ -1,16 +1,12 @@
use crate::utils::auth::authenticate; use crate::utils::auth::authenticate;
use crate::utils::metrics::*; use crate::utils::metrics::*;
use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
// use std::collections::BTreeMap;
use crate::web::gethosts::{GetHost, GetHostsReturHeaders}; use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use async_trait::async_trait; use async_trait::async_trait;
use axum::body::Bytes; use axum::body::Bytes;
use dashmap::DashMap; use dashmap::DashMap;
// use x509_parser::asn1_rs::ToDer;
use itoa::Buffer;
use log::{debug, error, warn}; use log::{debug, error, warn};
use once_cell::sync::Lazy;
use pingora::http::{RequestHeader, ResponseHeader, StatusCode}; use pingora::http::{RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*; use pingora::prelude::*;
use pingora::ErrorSource::Upstream; use pingora::ErrorSource::Upstream;
@@ -19,16 +15,19 @@ use pingora_core::prelude::HttpPeer;
// use pingora_core::protocols::TcpKeepalive; // use pingora_core::protocols::TcpKeepalive;
use pingora_limits::rate::Rate; use pingora_limits::rate::Rate;
use pingora_proxy::{ProxyHttp, Session}; use pingora_proxy::{ProxyHttp, Session};
// use prometheus::{register_int_counter, IntCounter};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt::Write; use std::fmt::Write;
use std::sync::Arc; use std::sync::{Arc, LazyLock};
use std::time::Duration; use std::time::Duration;
use tokio::time::Instant; use tokio::time::Instant;
static RATE_LIMITER: Lazy<Rate> = Lazy::new(|| Rate::new(Duration::from_secs(1))); // static RATE_LIMITER: Lazy<Rate> = Lazy::new(|| Rate::new(Duration::from_secs(1)));
static REVERSE_STORE: Lazy<DashMap<String, String>> = Lazy::new(|| DashMap::new()); // static REVERSE_STORE: Lazy<DashMap<String, String>> = Lazy::new(|| DashMap::new());
static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(|| DashMap::new());
thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));} thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));}
pub static RATE_LIMITER: LazyLock<Rate> = LazyLock::new(|| Rate::new(Duration::from_secs(1)));
#[derive(Clone)] #[derive(Clone)]
pub struct LB { pub struct LB {
@@ -71,7 +70,6 @@ impl ProxyHttp for LB {
let hostname = return_header_host_from_upstream(session, &self.ump_upst); let hostname = return_header_host_from_upstream(session, &self.ump_upst);
_ctx.hostname = hostname; _ctx.hostname = hostname;
let mut backend_id = None; let mut backend_id = None;
if _ctx.extraparams.sticky_sessions { if _ctx.extraparams.sticky_sessions {
if let Some(cookies) = session.req_header().headers.get("cookie") { if let Some(cookies) = session.req_header().headers.get("cookie") {
if let Ok(cookie_str) = cookies.to_str() { if let Ok(cookie_str) = cookies.to_str() {
@@ -102,12 +100,7 @@ impl ProxyHttp for LB {
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip()); 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); let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
if curr_window_requests > rate { if curr_window_requests > rate {
let mut buf = Buffer::new(); let header = ResponseHeader::build(429, None)?;
let rate_str = buf.format(rate);
let mut header = ResponseHeader::build(429, None)?;
header.insert_header("X-Rate-Limit-Limit", rate_str)?;
header.insert_header("X-Rate-Limit-Remaining", "0")?;
header.insert_header("X-Rate-Limit-Reset", "1")?;
session.set_keepalive(None); session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?; session.write_response_header(Box::new(header), true).await?;
debug!("Rate limited: {:?}, {}", rate_key, rate); debug!("Rate limited: {:?}, {}", rate_key, rate);
@@ -245,7 +238,7 @@ impl ProxyHttp for LB {
let mut buf = buffer.borrow_mut(); let mut buf = buffer.borrow_mut();
buf.clear(); buf.clear();
write!(buf, "{}", client_ip).unwrap_or(()); write!(buf, "{}", client_ip).unwrap_or(());
upstream_request.append_header("X-Forward-For", buf.as_str()).unwrap_or(false); upstream_request.append_header("X-Forwarded-For", buf.as_str()).unwrap_or(false);
}); });
} }

View File

@@ -3,7 +3,7 @@ use crate::utils::structs::{Config, Configuration, UpstreamsDashMap};
use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json}; use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json};
use axum::body::Body; use axum::body::Body;
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::http::{Response, StatusCode}; use axum::http::{header::HeaderMap, Response, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::{Json, Router}; use axum::{Json, Router};
@@ -18,6 +18,7 @@ use std::collections::HashMap;
// use std::net::SocketAddr; // use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use subtle::ConstantTimeEq;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
@@ -88,23 +89,18 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }
async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>, content: String) -> impl IntoResponse { async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>, headers: HeaderMap, content: String) -> impl IntoResponse {
if !st.config_api_enabled { if !st.config_api_enabled {
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap(); return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap();
} }
if let Some(s) = params.get("key") { 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.to_owned() == st.master_key { if s.as_bytes().ct_eq(st.master_key.as_bytes()).into() {
let strcontent = content.as_str(); let strcontent = content.as_str();
let parsed = serde_yaml::from_str::<Config>(strcontent); let parsed = serde_yml::from_str::<Config>(strcontent);
match parsed { match parsed {
Ok(_) => { Ok(_) => {
if let Some(s) = params.get("key") { let _ = tokio::spawn(async move { apply_config(content.as_str(), st).await });
if s.to_owned() == st.master_key { return Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap();
let _ = tokio::spawn(async move { apply_config(content.as_str(), st).await });
return Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap();
}
}
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap();
} }
Err(err) => { Err(err) => {
error!("Failed to parse upstreams file: {}", err); error!("Failed to parse upstreams file: {}", err);