mirror of
https://github.com/sadoyan/aralez.git
synced 2026-04-30 23:08:40 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baded40e6e | ||
|
|
c0a419f6f7 | ||
|
|
8aff2fa875 | ||
|
|
9b4ee26a2b | ||
|
|
f135106a44 | ||
|
|
389c12119a | ||
|
|
93a8661281 | ||
|
|
0505ce2849 |
1547
Cargo.lock
generated
1547
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -11,7 +11,7 @@ panic = "abort"
|
||||
strip = true
|
||||
|
||||
[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
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
dashmap = "7.0.0-rc2"
|
||||
@@ -20,34 +20,29 @@ pingora-proxy = "0.8.0"
|
||||
pingora-http = "0.8.0"
|
||||
pingora-limits = "0.8.0"
|
||||
async-trait = "0.1.89"
|
||||
env_logger = "0.11.9"
|
||||
env_logger = "0.11.10"
|
||||
log = "0.4.29"
|
||||
futures = "0.3.32"
|
||||
notify = "9.0.0-rc.2"
|
||||
axum = { version = "0.8.8" }
|
||||
#axum-server = { version = "0.8.0" }
|
||||
reqwest = { version = "0.13.2", features = ["json", "stream"] }
|
||||
serde_yaml = "0.9.34-deprecated"
|
||||
reqwest = { version = "0.13.2", features = ["json", "stream", "blocking"] }
|
||||
serde_yml = "0.0.12"
|
||||
rand = "0.10.0"
|
||||
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"] }
|
||||
tonic = "0.14.5"
|
||||
sha2 = { version = "0.11.0-rc.5", default-features = false }
|
||||
base16ct = { version = "1.0.0", features = ["alloc"] }
|
||||
urlencoding = "2.1.3"
|
||||
arc-swap = "1.8.2"
|
||||
arc-swap = "1.9.1"
|
||||
mimalloc = { version = "0.1.48", default-features = false }
|
||||
prometheus = "0.14.0"
|
||||
lazy_static = "1.5.0"
|
||||
x509-parser = "0.18.1"
|
||||
rustls-pemfile = "2.2.0"
|
||||
tower-http = { version = "0.6.8", features = ["fs"] }
|
||||
once_cell = "1.21.3"
|
||||
privdrop = "0.5.6"
|
||||
ctrlc = "3.5.2"
|
||||
port_check = "0.3.0"
|
||||
serde_json = "1.0.149"
|
||||
http = "1.4.0"
|
||||
itoa = "1.0.14"
|
||||
subtle = "2.6.1"
|
||||
moka = { version = "0.12.1", features = ["sync"] }
|
||||
ahash = "0.8.12"
|
||||
|
||||
16
README.md
16
README.md
@@ -122,13 +122,15 @@ Make the binary executable `chmod 755 ./aralez-VERSION` and run.
|
||||
|
||||
File names:
|
||||
|
||||
| File Name | Description |
|
||||
|---------------------------|--------------------------------------------------------------------------|
|
||||
| `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-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) |
|
||||
| File Name | Description |
|
||||
|---------------------------------|--------------------------------------------------------------------------|
|
||||
| `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-compat-musl.gz` | Static Linux x86_64 binary, compatible with old pre Haswell CPUs |
|
||||
| `aralez-x86_64-compat-glibc.gz` | Dynamic Linux x86_64 binary, compatible with old pre Haswell CPUs |
|
||||
| `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**
|
||||
|
||||
|
||||
@@ -1,42 +1,187 @@
|
||||
use crate::utils::jwt::check_jwt;
|
||||
// use reqwest::Client;
|
||||
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;
|
||||
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;
|
||||
// --------------------------------- //
|
||||
|
||||
#[async_trait::async_trait]
|
||||
trait AuthValidator {
|
||||
fn validate(&self, session: &Session) -> bool;
|
||||
async fn validate(&self, session: &mut Session) -> bool;
|
||||
}
|
||||
struct BasicAuth<'a>(&'a str);
|
||||
struct ApiKeyAuth<'a>(&'a str);
|
||||
struct JwtAuth<'a>(&'a str);
|
||||
struct ForwardAuth<'a>(&'a str);
|
||||
|
||||
pub static AUTH_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AuthValidator for BasicAuth<'_> {
|
||||
fn validate(&self, session: &Session) -> bool {
|
||||
async fn validate(&self, session: &mut Session) -> bool {
|
||||
if let Some(header) = session.get_header("authorization") {
|
||||
if let Some((_, val)) = header.to_str().ok().unwrap().split_once(' ') {
|
||||
let decoded = STANDARD.decode(val).ok().unwrap();
|
||||
let decoded_str = String::from_utf8(decoded).ok().unwrap();
|
||||
return decoded_str == self.0;
|
||||
if let Some(h) = header.to_str().ok() {
|
||||
if let Some((_, val)) = h.split_once(' ') {
|
||||
if let Some(decoded) = STANDARD.decode(val).ok() {
|
||||
if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AuthValidator for ApiKeyAuth<'_> {
|
||||
fn validate(&self, session: &Session) -> bool {
|
||||
async fn validate(&self, session: &mut Session) -> bool {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AuthValidator for JwtAuth<'_> {
|
||||
fn validate(&self, session: &Session) -> bool {
|
||||
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);
|
||||
@@ -53,33 +198,21 @@ impl AuthValidator for JwtAuth<'_> {
|
||||
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 {
|
||||
match &*auth_type.clone() {
|
||||
"basic" => {
|
||||
let auth = BasicAuth(&*credentials.clone());
|
||||
validate(&auth, session)
|
||||
}
|
||||
"apikey" => {
|
||||
let auth = ApiKeyAuth(&*credentials.clone());
|
||||
validate(&auth, session)
|
||||
}
|
||||
"jwt" => {
|
||||
let auth = JwtAuth(&*credentials.clone());
|
||||
validate(&auth, session)
|
||||
}
|
||||
pub async fn authenticate(auth_type: &Arc<str>, credentials: &Arc<str>, 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,
|
||||
_ => {
|
||||
println!("Unsupported authentication mechanism : {}", auth_type);
|
||||
log::warn!("Unsupported authentication mechanism : {}", auth_type);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_query_param(session: &Session, key: &str) -> Option<String> {
|
||||
pub fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
|
||||
let query = session.req_header().uri.query()?;
|
||||
|
||||
let params: HashMap<_, _> = query
|
||||
@@ -91,6 +224,24 @@ pub fn get_query_param(session: &Session, key: &str) -> Option<String> {
|
||||
Some((k, v))
|
||||
})
|
||||
.collect();
|
||||
|
||||
params.get(key).map(|v| decode(v).ok()).flatten().map(|s| s.to_string())
|
||||
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> {
|
||||
match addr.split_once(':') {
|
||||
Some((h, p)) => match p.parse::<u16>() {
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,18 +119,11 @@ async fn http_request(url: &str, method: &str, payload: &str, client: &Client) -
|
||||
}
|
||||
|
||||
pub async fn ping_grpc(addr: &str) -> bool {
|
||||
let endpoint_result = Endpoint::from_shared(addr.to_owned());
|
||||
|
||||
if let Ok(endpoint) = endpoint_result {
|
||||
let endpoint = endpoint.timeout(Duration::from_secs(2));
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await {
|
||||
Ok(Ok(_channel)) => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
let endpoint = match Endpoint::from_shared(addr.to_owned()) {
|
||||
Ok(e) => e.timeout(Duration::from_secs(2)),
|
||||
Err(_) => return false,
|
||||
};
|
||||
tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await.ok().and_then(Result::ok).is_some()
|
||||
}
|
||||
|
||||
async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) {
|
||||
|
||||
@@ -1,16 +1,87 @@
|
||||
use ahash::AHasher;
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
use moka::sync::Cache;
|
||||
use moka::Expiry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::LazyLock;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct Claims {
|
||||
pub(crate) user: String,
|
||||
pub(crate) exp: u64,
|
||||
pub struct Claims {
|
||||
pub master_key: String,
|
||||
pub owner: String,
|
||||
pub exp: u64,
|
||||
pub random: Option<String>,
|
||||
}
|
||||
pub fn check_jwt(input: &str, secret: &str) -> bool {
|
||||
let validation = Validation::new(Algorithm::HS256);
|
||||
let token_data = decode::<Claims>(&input, &DecodingKey::from_secret(secret.as_ref()), &validation);
|
||||
match token_data {
|
||||
Ok(_) => true,
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Expired {
|
||||
exp: Option<u64>,
|
||||
}
|
||||
|
||||
static JWT_VALIDATION: LazyLock<Validation> = LazyLock::new(|| Validation::new(Algorithm::HS256));
|
||||
|
||||
static JWT_CACHE: LazyLock<Cache<u64, u64>> = LazyLock::new(|| Cache::builder().max_capacity(100_000).expire_after(JwtExpiry).build());
|
||||
struct JwtExpiry;
|
||||
impl Expiry<u64, u64> for JwtExpiry {
|
||||
fn expire_after_create(&self, _key: &u64, value: &u64, _current_time: Instant) -> Option<Duration> {
|
||||
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
if *value > now {
|
||||
Some(Duration::from_secs(value - now))
|
||||
} else {
|
||||
Some(Duration::ZERO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_jwt(token: &str, secret: &str) -> bool {
|
||||
let key = hash_token(token, secret);
|
||||
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
if let Some(exp) = JWT_CACHE.get(&key) {
|
||||
if exp < now {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
match is_expired(token, now) {
|
||||
Ok(true) => return false,
|
||||
Ok(false) => {}
|
||||
Err(_) => return false,
|
||||
}
|
||||
|
||||
match decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) {
|
||||
Ok(data) => {
|
||||
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
if data.claims.exp > now {
|
||||
JWT_CACHE.insert(key, data.claims.exp);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_expired(token: &str, now: u64) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
let parts: Vec<&str> = token.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return Err("Invalid JWT format".into());
|
||||
}
|
||||
let decoded = URL_SAFE_NO_PAD.decode(parts[1])?;
|
||||
let claims: Expired = serde_json::from_slice(&decoded)?;
|
||||
if let Some(exp) = claims.exp {
|
||||
Ok(exp < now)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_token(token: &str, secret: &str) -> u64 {
|
||||
let mut hasher = AHasher::default();
|
||||
token.hash(&mut hasher);
|
||||
secret.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use http::method::Method;
|
||||
use http::StatusCode;
|
||||
use pingora_http::Method;
|
||||
use pingora_http::StatusCode;
|
||||
use pingora_http::Version;
|
||||
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, IntCounterVec};
|
||||
use std::sync::Arc;
|
||||
@@ -12,46 +12,40 @@ pub struct MetricTypes {
|
||||
pub latency: Duration,
|
||||
pub version: Version,
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref REQUEST_COUNT: IntCounter = register_int_counter!(
|
||||
"aralez_requests_total",
|
||||
"Total number of requests handled by Aralez"
|
||||
).unwrap();
|
||||
pub static ref RESPONSE_CODES: IntCounterVec = register_int_counter_vec!(
|
||||
"aralez_responses_total",
|
||||
"Responses grouped by status code",
|
||||
&["status"]
|
||||
).unwrap();
|
||||
pub static ref REQUEST_LATENCY: Histogram = register_histogram!(
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap());
|
||||
|
||||
pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
|
||||
LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap());
|
||||
|
||||
pub static REQUEST_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
|
||||
register_histogram!(
|
||||
"aralez_request_latency_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]
|
||||
).unwrap();
|
||||
pub static ref RESPONSE_LATENCY: Histogram = register_histogram!(
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
|
||||
register_histogram!(
|
||||
"aralez_response_latency_seconds",
|
||||
"Response latency in seconds",
|
||||
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!(
|
||||
"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",
|
||||
"Number of requests by UPSTREAM server",
|
||||
&["upstream"]
|
||||
).unwrap();
|
||||
pub static ref REQUESTS_BY_VERSION: IntCounterVec = register_int_counter_vec!(
|
||||
"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();
|
||||
}
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub static REQUESTS_BY_METHOD: LazyLock<IntCounterVec> =
|
||||
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_method_total", "Number of requests by HTTP method", &["method"]).unwrap());
|
||||
|
||||
pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> =
|
||||
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_upstream", "Number of requests by UPSTREAM server", &["upstream"]).unwrap());
|
||||
|
||||
pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> =
|
||||
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).unwrap());
|
||||
|
||||
pub fn calc_metrics(metric_types: &MetricTypes) {
|
||||
REQUEST_COUNT.inc();
|
||||
@@ -66,7 +60,7 @@ pub fn calc_metrics(metric_types: &MetricTypes) {
|
||||
_ => "Unknown",
|
||||
};
|
||||
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_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc();
|
||||
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());
|
||||
|
||||
@@ -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,
|
||||
Err(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 {
|
||||
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,
|
||||
Err(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 {
|
||||
address: Arc::from(ip),
|
||||
port,
|
||||
is_ssl: true,
|
||||
is_ssl: false,
|
||||
is_http2: false,
|
||||
to_https: path_config.to_https.unwrap_or(false),
|
||||
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 {
|
||||
let data = fs::read_to_string(path).unwrap();
|
||||
let reply = DashMap::new();
|
||||
let cfg: HashMap<String, String> = serde_yaml::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 cfg: HashMap<String, String> = 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");
|
||||
log_builder(&cfo);
|
||||
cfo.hc_method = cfo.hc_method.to_uppercase();
|
||||
for (k, v) in cfg {
|
||||
@@ -265,7 +265,7 @@ fn parce_tls_grades(what: Option<String>) -> Option<String> {
|
||||
},
|
||||
None => {
|
||||
warn!("TLS grade not set, defaulting to: medium");
|
||||
Some("b".to_string())
|
||||
Some("medium".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SharedState {
|
||||
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() {
|
||||
let mut state = GLOBAL_STATE.write().unwrap();
|
||||
|
||||
@@ -76,7 +76,7 @@ pub struct HostConfig {
|
||||
pub struct Auth {
|
||||
#[serde(rename = "type")]
|
||||
pub auth_type: String,
|
||||
#[serde(rename = "creds")]
|
||||
#[serde(rename = "data")]
|
||||
pub auth_cred: String,
|
||||
}
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::utils::tls::CertificateConfig;
|
||||
use dashmap::DashMap;
|
||||
use log::{error, info};
|
||||
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use port_check::is_port_reachable;
|
||||
use privdrop::PrivDrop;
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -12,6 +11,7 @@ use std::any::type_name;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpListener;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
@@ -227,13 +227,13 @@ pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> {
|
||||
certificate_configs.push(y);
|
||||
}
|
||||
}
|
||||
for (_, v) in f.iter() {
|
||||
let y = CertificateConfig {
|
||||
cert_path: v[0].clone(),
|
||||
key_path: v[1].clone(),
|
||||
};
|
||||
certificate_configs.push(y);
|
||||
}
|
||||
// for (_, v) in f.iter() {
|
||||
// let y = CertificateConfig {
|
||||
// cert_path: v[0].clone(),
|
||||
// key_path: v[1].clone(),
|
||||
// };
|
||||
// certificate_configs.push(y);
|
||||
// }
|
||||
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));
|
||||
loop {
|
||||
thread::sleep(time::Duration::from_millis(10));
|
||||
if is_port_reachable(http_addr.clone()) {
|
||||
if port_is_available(http_addr.clone()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(tls_addr) = tls_addr {
|
||||
loop {
|
||||
thread::sleep(time::Duration::from_millis(10));
|
||||
if is_port_reachable(tls_addr.clone()) {
|
||||
if port_is_available(tls_addr.clone()) {
|
||||
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) {
|
||||
let port = SocketAddr::from_str(addr).map(|sa| sa.port()).unwrap();
|
||||
match port < 1024 {
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
use crate::utils::auth::authenticate;
|
||||
use crate::utils::metrics::*;
|
||||
use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
|
||||
// use std::collections::BTreeMap;
|
||||
use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
|
||||
use arc_swap::ArcSwap;
|
||||
use async_trait::async_trait;
|
||||
use axum::body::Bytes;
|
||||
use dashmap::DashMap;
|
||||
// use x509_parser::asn1_rs::ToDer;
|
||||
use itoa::Buffer;
|
||||
use log::{debug, error, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use pingora::http::{RequestHeader, ResponseHeader, StatusCode};
|
||||
use pingora::prelude::*;
|
||||
use pingora::ErrorSource::Upstream;
|
||||
@@ -19,16 +15,19 @@ use pingora_core::prelude::HttpPeer;
|
||||
// use pingora_core::protocols::TcpKeepalive;
|
||||
use pingora_limits::rate::Rate;
|
||||
use pingora_proxy::{ProxyHttp, Session};
|
||||
// use prometheus::{register_int_counter, IntCounter};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
|
||||
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 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: LazyLock<DashMap<String, String>> = LazyLock::new(|| DashMap::new());
|
||||
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)]
|
||||
pub struct LB {
|
||||
@@ -71,7 +70,6 @@ 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(cookies) = session.req_header().headers.get("cookie") {
|
||||
if let Ok(cookie_str) = cookies.to_str() {
|
||||
@@ -91,7 +89,7 @@ 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) {
|
||||
if !authenticate(&auth.auth_type, &auth.auth_cred, session).await {
|
||||
let _ = session.respond_error(401).await;
|
||||
warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path());
|
||||
return Ok(true);
|
||||
@@ -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 curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
|
||||
if curr_window_requests > rate {
|
||||
let mut buf = Buffer::new();
|
||||
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")?;
|
||||
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);
|
||||
@@ -245,7 +238,7 @@ impl ProxyHttp for LB {
|
||||
let mut buf = buffer.borrow_mut();
|
||||
buf.clear();
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::utils::discovery::APIUpstreamProvider;
|
||||
// use std::net::SocketAddr;
|
||||
use crate::utils::jwt::Claims;
|
||||
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::{Response, StatusCode};
|
||||
use axum::http::{header::HeaderMap, Response, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
@@ -13,21 +15,14 @@ use futures::SinkExt;
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use log::{error, info, warn};
|
||||
use prometheus::{gather, Encoder, TextEncoder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
// use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use subtle::ConstantTimeEq;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct InputKey {
|
||||
master_key: String,
|
||||
owner: String,
|
||||
valid: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct OutToken {
|
||||
token: String,
|
||||
@@ -88,23 +83,18 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
|
||||
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 {
|
||||
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap();
|
||||
}
|
||||
if let Some(s) = params.get("key") {
|
||||
if s.to_owned() == st.master_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.as_bytes().ct_eq(st.master_key.as_bytes()).into() {
|
||||
let strcontent = content.as_str();
|
||||
let parsed = serde_yaml::from_str::<Config>(strcontent);
|
||||
let parsed = serde_yml::from_str::<Config>(strcontent);
|
||||
match parsed {
|
||||
Ok(_) => {
|
||||
if let Some(s) = params.get("key") {
|
||||
if s.to_owned() == st.master_key {
|
||||
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();
|
||||
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();
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to parse upstreams file: {}", err);
|
||||
@@ -123,15 +113,21 @@ async fn apply_config(content: &str, mut st: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<InputKey>) -> (StatusCode, Json<OutToken>) {
|
||||
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<Claims>) -> (StatusCode, Json<OutToken>) {
|
||||
if payload.master_key == state.master_key {
|
||||
let now = SystemTime::now() + Duration::from_secs(payload.valid * 60);
|
||||
let a = now.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
let claim = crate::utils::jwt::Claims { user: payload.owner, exp: a };
|
||||
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 };
|
||||
info!("Generating token: {:?}", tok);
|
||||
info!("Generating token: {:?}", tok.token);
|
||||
(StatusCode::CREATED, Json(tok))
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user