12 Commits

Author SHA1 Message Date
Ara Sadoyan
a0b6de9759 updated . config example 2026-04-30 18:13:37 +02:00
Ara Sadoyan
a70eb53bc1 Let's Encrypt auto certificate HTTP-01 challenge #16 2026-04-30 18:04:25 +02:00
Ara Sadoyan
bee307793c restructurisation grades 2026-04-27 15:28:54 +02:00
Ara Sadoyan
6e83775127 restructurisation 2026-04-27 15:22:31 +02:00
Ara Sadoyan
baded40e6e Cache for JWT tokens, to minimize crypto. BRAKING: Claims key "valid" renamed to "exp" 2026-04-17 17:53:31 +02:00
Ara Sadoyan
c0a419f6f7 completed implementation of #17 2026-04-15 18:23:57 +02:00
Ara Sadoyan
8aff2fa875 Standardizing implementation of #17 2026-04-14 16:11:24 +02:00
Ara Sadoyan
9b4ee26a2b Working on #17 2026-04-13 20:06:57 +02:00
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
26 changed files with 1643 additions and 1223 deletions

1641
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.52.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,31 @@ 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.3"
axum = { version = "0.8.8" } axum = { version = "0.8.9" }
#axum-server = { version = "0.8.0" } reqwest = { version = "0.13.2", features = ["json", "stream", "blocking"] }
reqwest = { version = "0.13.2", features = ["json", "stream"] } serde_yml = "0.0.12"
serde_yaml = "0.9.34-deprecated" rand = "0.10.1"
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.50", 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" moka = { version = "0.12.1", features = ["sync"] }
ahash = "0.8.12"
instant-acme = "0.8.5"
rcgen = "0.14.7"

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

@@ -1,7 +1,7 @@
# Main configuration file, applied on startup # Main configuration file, applied on startup
threads: 12 # Number of daemon threads default setting threads: 12 # Number of daemon threads default setting
#runuser: pastor # Username for running aralez after dropping root privileges, requires program to start as root runuser: aralez # Username for running aralez after dropping root privileges, requires program to start as root
#rungroup: pastor # Group for running aralez after dropping root privileges, requires program to start as root rungroup: aralez # Group for running aralez after dropping root privileges, requires program to start as root
daemon: false # Run in background daemon: false # Run in background
upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections
pid_file: /tmp/aralez.pid # Path to PID file pid_file: /tmp/aralez.pid # Path to PID file
@@ -9,17 +9,14 @@ error_log: /tmp/aralez_err.log # Path to error log
upgrade_sock: /tmp/aralez.sock # Path to socket file upgrade_sock: /tmp/aralez.sock # Path to socket file
config_api_enabled: true # Boolean to enable/disable remote config push capability. config_api_enabled: true # Boolean to enable/disable remote config push capability.
config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location
config_tls_address: 0.0.0.0:3001 # HTTP TLS API address for pushing upstreams.yaml from remote location
config_tls_certificate: /etc/server.crt # Mandatory if config_tls_address is set
config_tls_key_file: /etc/key.pem # Mandatory if config_tls_address is set
proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address
proxy_certificates: /etc/certs # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key. proxy_configs: /opt/aralez/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
proxy_tls_grade: a+ # Grade of TLS suite for proxy (a+, a, b, c, unsafe), matching grades of Qualys SSL Labs proxy_tls_grade: a+ # Grade of TLS suite for proxy (a+, a, b, c, unsafe), matching grades of Qualys SSL Labs
upstreams_conf: /etc/upstreams.yaml # the location of upstreams file upstreams_conf: /opt/aralez/etc/upstreams.yaml # the location of upstreams file
file_server_folder: /opt/storage # Optional, local folder to serve file_server_folder: /opt/storage # Optional, local folder to serve
file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access. file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access.
log_level: info # info, warn, error, debug, trace, off log_level: info # info, warn, error, debug, trace, off
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE
hc_interval: 2 #Interval for health checks in seconds hc_interval: 2 #Interval for health checks in seconds
master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret

View File

@@ -1,3 +1,4 @@
mod tls;
mod utils; mod utils;
mod web; mod web;

3
src/tls.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod acme;
pub mod grades;
pub mod load;

2
src/tls/acme.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod account;
pub mod order;

58
src/tls/acme/account.rs Normal file
View File

@@ -0,0 +1,58 @@
use instant_acme::{Account, AccountCredentials, LetsEncrypt, NewAccount};
use log::info;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
static ACCOUNT: OnceLock<Account> = OnceLock::new();
pub async fn get_account(file: &str) -> Result<&'static Account, Box<dyn std::error::Error>> {
if let Some(account) = ACCOUNT.get() {
return Ok(account);
}
if let Some(credentials) = load_credentials(file) {
let acc_builder = Account::builder()?;
let account = acc_builder.from_credentials(credentials).await?;
let _ = ACCOUNT.set(account);
info!("Loaded existing ACME account");
} else {
info!("No existing credentials found, creating new account");
create_account(file).await?;
}
ACCOUNT.get().ok_or("Failed to initialize account".into())
}
async fn create_account(file: &str) -> Result<(), Box<dyn std::error::Error>> {
let new_account = NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
};
let acc_builder = Account::builder()?;
let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Production.url().to_string(), None).await?;
// let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Staging.url().to_string(), None).await?;
info!("Account created: {:?}", account.id());
save_credentials(&credentials, file)?;
let _ = ACCOUNT.set(account);
Ok(())
}
pub async fn load_or_create(file: &str) -> Result<String, Box<dyn std::error::Error>> {
let account = get_account(file).await?;
Ok(account.id().to_string() + "\n")
}
fn save_credentials(credentials: &AccountCredentials, file: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(credentials)?;
fs::write(file, json)?;
info!("ACME credentials saved to {}", file);
Ok(())
}
fn load_credentials(file: &str) -> Option<AccountCredentials> {
if !Path::new(file).exists() {
return None;
}
let json = fs::read_to_string(file).ok()?;
serde_json::from_str(&json).ok()
}

94
src/tls/acme/order.rs Normal file
View File

@@ -0,0 +1,94 @@
use crate::tls::acme::account::get_account;
use crate::utils::parceyaml::DOMAINS;
use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy};
use log::{error, info};
use pingora::prelude::sleep;
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
use std::collections::HashMap;
use std::fs;
use std::sync::{LazyLock, RwLock};
use std::time::Duration;
use x509_parser::prelude::*;
pub static CHALLENGES: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
pub async fn refresh_order(certs_dir: String, autoconf_dir: String) {
let credsfile = autoconf_dir + "/acme_credentials.json";
loop {
for item in DOMAINS.iter() {
let _what = order(item.key(), credsfile.as_str(), certs_dir.clone()).await;
}
sleep(Duration::from_secs(12 * 3600)).await;
}
}
pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<String, Box<dyn std::error::Error>> {
let crt = certs_dir.clone() + "/" + domain + ".crt";
let key = certs_dir.clone() + "/" + domain + ".key";
if let None = DOMAINS.get(domain) {
DOMAINS.insert(domain.to_string(), true);
let mut newlist: Vec<String> = Vec::new();
for item in DOMAINS.iter() {
newlist.push(item.key().to_string());
}
if let Ok(json_content) = serde_json::to_string_pretty(&newlist) {
let autocfg_file = credsfile.replace("/acme_credentials.json", "/domains.json");
if let Err(err) = std::fs::write(&autocfg_file, json_content) {
error!("Error Updating domains for certificates: {} : {}", domain, err);
return Err(Box::from(err));
}
}
}
let _ = match cert_expiry(crt.as_str()) {
Ok(expiry) => {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
if expiry > now + 30 * 24 * 3600 {
// println!("Fresh certificate exists. Not renewing !");
return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
}
}
Err(_) => {}
};
let account = get_account(credsfile).await?;
let mut order = account.new_order(&NewOrder::new(&[Identifier::Dns(domain.to_string())])).await?;
let mut authorizations = order.authorizations();
while let Some(auth) = authorizations.next().await {
let mut auth = auth?;
let mut challenge_handle = auth.challenge(ChallengeType::Http01).ok_or("no http01 challenge found")?;
let key_auth = challenge_handle.key_authorization();
let key_auth_str = key_auth.as_str().to_string();
let token = key_auth_str.split('.').next().ok_or("invalid key authorization")?.to_string();
CHALLENGES.write().unwrap().insert(token, key_auth_str);
challenge_handle.set_ready().await?;
}
let status = order.poll_ready(&RetryPolicy::default()).await?;
info!("ACME poll_ready status: {:?}", status);
let mut params = CertificateParams::new(vec![domain.to_owned()])?;
params.distinguished_name = DistinguishedName::new();
let private_key = KeyPair::generate()?;
let signing_request = params.serialize_request(&private_key)?;
let csr_der = signing_request.der();
order.finalize_csr(&csr_der).await?;
// poll for certificate
let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;
CHALLENGES.write().unwrap().clear();
let private_key_pem = private_key.serialize_pem();
fs::write(crt, cert_chain_pem)?;
fs::write(key, private_key_pem)?;
Ok("Certificate is successfully generated \n".to_string())
}
fn cert_expiry(path: &str) -> Result<u64, Box<dyn std::error::Error>> {
let pem = fs::read(path)?;
let (_, pem) = parse_x509_pem(&pem)?;
let (_, cert) = parse_x509_certificate(&pem.contents)?;
let expiry = cert.validity().not_after.timestamp() as u64;
Ok(expiry)
}

75
src/tls/grades.rs Normal file
View File

@@ -0,0 +1,75 @@
use log::{info, warn};
use pingora::tls::ssl::{select_next_proto, AlpnError, SslRef, SslVersion};
use pingora_core::listeners::tls::TlsSettings;
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
HIGH,
MEDIUM,
LEGACY,
}
impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::HIGH),
"medium" => Some(TlsGrade::MEDIUM),
"unsafe" => Some(TlsGrade::LEGACY),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade);
match config_grade {
Some(TlsGrade::HIGH) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
// let _ = tls_settings.set_ciphersuites(CIPHERS.high);
let _ = tls_settings.set_cipher_list(CIPHERS.high);
info!("TLS grade: {:?}, => HIGH", tls_settings.options());
}
Some(TlsGrade::MEDIUM) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
info!("TLS grade: {:?}, => MEDIUM", tls_settings.options());
}
Some(TlsGrade::LEGACY) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
// let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to MEDIUM
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top MEDIUM");
}
}
}

View File

@@ -1,7 +1,7 @@
use crate::tls::grades;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info, warn}; use log::error;
use pingora::tls::ssl::{select_next_proto, AlpnError, NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef, SslVersion}; use pingora::tls::ssl::{NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef};
use pingora_core::listeners::tls::TlsSettings;
use rustls_pemfile::{read_one, Item}; use rustls_pemfile::{read_one, Item};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashSet; use std::collections::HashSet;
@@ -10,7 +10,6 @@ use std::io::BufReader;
use x509_parser::extensions::GeneralName; use x509_parser::extensions::GeneralName;
use x509_parser::nom::Err as NomErr; use x509_parser::nom::Err as NomErr;
use x509_parser::prelude::*; use x509_parser::prelude::*;
#[derive(Clone, Deserialize, Debug)] #[derive(Clone, Deserialize, Debug)]
pub struct CertificateConfig { pub struct CertificateConfig {
pub cert_path: String, pub cert_path: String,
@@ -18,14 +17,14 @@ pub struct CertificateConfig {
} }
#[derive(Debug)] #[derive(Debug)]
struct CertificateInfo { pub struct CertificateInfo {
common_names: Vec<String>, pub common_names: Vec<String>,
alt_names: Vec<String>, pub alt_names: Vec<String>,
ssl_context: SslContext, pub ssl_context: SslContext,
#[allow(dead_code)] #[allow(dead_code)]
cert_path: String, // Only used for logging pub cert_path: String, // Only used for logging
#[allow(dead_code)] #[allow(dead_code)]
key_path: String, // Only used for logging pub key_path: String, // Only used for logging
} }
#[derive(Debug)] #[derive(Debug)]
@@ -106,7 +105,7 @@ impl Certificates {
} }
} }
fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> { pub fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> {
let mut common_names = HashSet::new(); let mut common_names = HashSet::new();
let mut alt_names = HashSet::new(); let mut alt_names = HashSet::new();
@@ -180,79 +179,7 @@ fn create_ssl_context(cert_path: &str, key_path: &str) -> Result<SslContext, Box
let mut ctx = SslContext::builder(SslMethod::tls())?; let mut ctx = SslContext::builder(SslMethod::tls())?;
ctx.set_certificate_chain_file(cert_path)?; ctx.set_certificate_chain_file(cert_path)?;
ctx.set_private_key_file(key_path, SslFiletype::PEM)?; ctx.set_private_key_file(key_path, SslFiletype::PEM)?;
ctx.set_alpn_select_callback(prefer_h2); ctx.set_alpn_select_callback(grades::prefer_h2);
let built = ctx.build(); let built = ctx.build();
Ok(built) Ok(built)
} }
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
HIGH,
MEDIUM,
LEGACY,
}
impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::HIGH),
"medium" => Some(TlsGrade::MEDIUM),
"unsafe" => Some(TlsGrade::LEGACY),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade);
match config_grade {
Some(TlsGrade::HIGH) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
// let _ = tls_settings.set_ciphersuites(CIPHERS.high);
let _ = tls_settings.set_cipher_list(CIPHERS.high);
info!("TLS grade: {:?}, => HIGH", tls_settings.options());
}
Some(TlsGrade::MEDIUM) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
info!("TLS grade: {:?}, => MEDIUM", tls_settings.options());
}
Some(TlsGrade::LEGACY) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
// let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to MEDIUM
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top MEDIUM");
}
}
}

View File

@@ -1,6 +1,5 @@
pub mod auth; pub mod auth;
pub mod discovery; pub mod discovery;
pub mod dnsclient;
mod filewatch; mod filewatch;
pub mod fordebug; pub mod fordebug;
pub mod healthcheck; pub mod healthcheck;
@@ -11,6 +10,5 @@ pub mod metrics;
pub mod parceyaml; pub mod parceyaml;
pub mod state; pub mod state;
pub mod structs; pub mod structs;
pub mod tls;
pub mod tools; pub mod tools;
// pub mod watchksecret; // pub mod watchksecret;

View File

@@ -1,42 +1,187 @@
use crate::utils::jwt::check_jwt; use crate::utils::jwt::check_jwt;
// use reqwest::Client;
use axum::http::StatusCode;
use base64::engine::general_purpose::STANDARD; use base64::engine::general_purpose::STANDARD;
use base64::Engine; 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, LazyLock};
use subtle::ConstantTimeEq;
use urlencoding::decode; 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 { trait AuthValidator {
fn validate(&self, session: &Session) -> bool; async fn validate(&self, session: &mut Session) -> bool;
} }
struct BasicAuth<'a>(&'a str); struct BasicAuth<'a>(&'a str);
struct ApiKeyAuth<'a>(&'a str); struct ApiKeyAuth<'a>(&'a str);
struct JwtAuth<'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<'_> { 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(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
} }
} }
#[async_trait::async_trait]
impl AuthValidator for ApiKeyAuth<'_> { 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") { 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
} }
} }
#[async_trait::async_trait]
impl AuthValidator for JwtAuth<'_> { impl AuthValidator for JwtAuth<'_> {
fn validate(&self, session: &Session) -> bool { async fn validate(&self, session: &mut Session) -> bool {
let jwtsecret = self.0; let jwtsecret = self.0;
if let Some(tok) = get_query_param(session, "araleztoken") { if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret); return check_jwt(tok.as_str(), jwtsecret);
@@ -53,33 +198,21 @@ 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 async fn authenticate(auth_type: &Arc<str>, credentials: &Arc<str>, session: &mut Session) -> bool {
pub fn authenticate(auth_type: &Arc<str>, credentials: &Arc<str>, session: &Session) -> bool { match &**auth_type {
match &*auth_type.clone() { "basic" => BasicAuth(credentials).validate(session).await,
"basic" => { "apikey" => ApiKeyAuth(credentials).validate(session).await,
let auth = BasicAuth(&*credentials.clone()); "jwt" => JwtAuth(credentials).validate(session).await,
validate(&auth, session) "forward" => ForwardAuth(credentials).validate(session).await,
}
"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
} }
} }
} }
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 query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query let params: HashMap<_, _> = query
@@ -91,6 +224,24 @@ 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()) }
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));
}
}
};
} }

View File

@@ -10,6 +10,8 @@ pub struct APIUpstreamProvider {
pub config_api_enabled: bool, pub config_api_enabled: bool,
pub address: String, pub address: String,
pub masterkey: String, pub masterkey: String,
pub certs_dir: String,
pub config_dir: String,
// pub tls_address: Option<String>, // pub tls_address: Option<String>,
// pub tls_certificate: Option<String>, // pub tls_certificate: Option<String>,
// pub tls_key_file: Option<String>, // pub tls_key_file: Option<String>,

View File

@@ -1,159 +0,0 @@
/*
use crate::utils::structs::InnerMap;
use dashmap::DashMap;
use hickory_client::client::{Client, ClientHandle};
use hickory_client::proto::rr::{DNSClass, Name, RecordType};
use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::udp::UdpClientStream;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::atomic::AtomicUsize;
use std::time::Duration;
use tokio::sync::Mutex;
type DnsError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub struct DnsClientPool {
clients: Vec<Mutex<DnsClient>>,
}
struct DnsClient {
client: Client,
}
pub async fn start2(mut toreturn: Sender<Configuration>, config: Arc<Configuration>) {
let k8s = config.kubernetes.clone();
match k8s {
Some(k8s) => {
let dnserver = k8s.servers.unwrap_or(vec!["127.0.0.1:53".to_string()]);
let headers = DashMap::new();
let end = dnserver.len() - 1;
let mut num = 0;
if end > 0 {
num = rand::rng().random_range(0..end);
}
let srv = dnserver.get(num).unwrap().to_string();
let pool = DnsClientPool::new(5, srv.clone()).await;
let u = UpstreamsDashMap::new();
if let Some(whitelist) = k8s.services {
loop {
let upstreams = UpstreamsDashMap::new();
for service in whitelist.iter() {
let ret = pool.query_srv(service.real.as_str(), srv.clone()).await;
match ret {
Ok(r) => {
upstreams.insert(service.proxy.clone(), r);
}
Err(e) => eprintln!("DNS query failed for {:?}: {:?}", service, e),
}
}
if !compare_dashmaps(&u, &upstreams) {
headers.clear();
for (k, v) in config.headers.clone() {
headers.insert(k.to_string(), v);
}
let mut tosend: Configuration = Configuration {
upstreams: Default::default(),
headers: Default::default(),
consul: None,
kubernetes: None,
typecfg: "".to_string(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(&upstreams, &u);
clone_dashmap_into(&upstreams, &tosend.upstreams);
tosend.headers = headers.clone();
tosend.extraparams.authentication = config.extraparams.authentication.clone();
tosend.typecfg = config.typecfg.clone();
tosend.consul = config.consul.clone();
print_upstreams(&tosend.upstreams);
toreturn.send(tosend).await.unwrap();
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
None => {}
}
}
impl DnsClient {
pub async fn new(server: String) -> Result<Self, DnsError> {
let server_details = server;
let server: SocketAddr = server_details.parse().expect("Unable to parse socket address");
let conn = UdpClientStream::builder(server, TokioRuntimeProvider::default()).build();
let (client, bg) = Client::connect(conn).await.unwrap();
tokio::spawn(bg);
Ok(Self { client })
}
pub async fn query_srv(&mut self, name: &str) -> Result<DashMap<String, (Vec<InnerMap>, AtomicUsize)>, DnsError> {
let upstreams: DashMap<String, (Vec<InnerMap>, AtomicUsize)> = DashMap::new();
let mut values = Vec::new();
match tokio::time::timeout(Duration::from_secs(5), self.client.query(Name::from_str(name)?, DNSClass::IN, RecordType::SRV)).await {
Ok(Ok(response)) => {
for answer in response.answers() {
if let hickory_client::proto::rr::RData::SRV(srv) = answer.data() {
let to_add = InnerMap {
address: srv.target().to_string(),
port: srv.port(),
is_ssl: false,
is_http2: false,
to_https: false,
sticky_sessions: false,
rate_limit: None,
};
values.push(to_add);
}
}
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0)));
Ok(upstreams)
}
Ok(Err(e)) => Err(Box::new(e)),
Err(_) => Err("DNS query timed out".into()),
}
}
}
impl DnsClientPool {
pub async fn new(pool_size: usize, server: String) -> Self {
let mut clients = Vec::with_capacity(pool_size);
for _ in 0..pool_size {
if let Ok(client) = DnsClient::new(server.clone()).await {
clients.push(Mutex::new(client));
}
}
Self { clients }
}
pub async fn query_srv(&self, name: &str, server: String) -> Result<DashMap<String, (Vec<InnerMap>, AtomicUsize)>, DnsError> {
// Try to get an available client
for client_mutex in &self.clients {
if let Ok(mut client) = client_mutex.try_lock() {
let vay = client.query_srv(name).await;
match vay {
Ok(_) => return vay,
Err(_) => {
// If query fails, drop this client and create a new one
*client = match DnsClient::new(server).await {
Ok(c) => c,
Err(e) => return Err(e),
};
// Retry with the new client
return client.query_srv(name).await;
}
}
}
}
// If all clients are busy, wait for the first one with a timeout
match tokio::time::timeout(Duration::from_secs(2), self.clients[0].lock()).await {
Ok(mut client) => client.query_srv(name).await,
Err(_) => Err("All DNS clients are busy and timeout reached".into()),
}
}
}
*/

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,16 +1,87 @@
use ahash::AHasher;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use moka::sync::Cache;
use moka::Expiry;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
use std::sync::LazyLock;
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Claims { pub struct Claims {
pub(crate) user: String, pub master_key: String,
pub(crate) exp: u64, 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); #[derive(Debug, Deserialize)]
let token_data = decode::<Claims>(&input, &DecodingKey::from_secret(secret.as_ref()), &validation); struct Expired {
match token_data { exp: Option<u64>,
Ok(_) => true, }
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, 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()
}

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

@@ -7,16 +7,35 @@ use log::{error, info, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::{Arc, LazyLock};
use std::{env, fs}; use std::{env, fs};
pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(|| DashMap::new());
pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) { pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) {
let mut conf_files = Vec::new(); let mut conf_files = Vec::new();
let yaml_data = match kind { let yaml_data = match kind {
"filepath" => match fs::read_to_string(d) { "filepath" => match fs::read_to_string(d) {
Ok(data) => { Ok(data) => {
let mut confdir = Path::new(d).parent().unwrap().to_path_buf(); let mut confdir = Path::new(d).parent().unwrap().to_path_buf();
let mut autocfg = Path::new(d).parent().unwrap().to_path_buf();
autocfg.push("autoconfigs");
if !fs::metadata(autocfg.clone()).is_ok() {
fs::create_dir_all(autocfg.clone()).ok();
}
autocfg.push("domains.json");
if autocfg.exists() {
let json: Option<Vec<String>> = fs::read_to_string(autocfg).ok().and_then(|s| serde_json::from_str(&s).ok());
if let Some(domains) = json {
for domain in domains {
DOMAINS.insert(domain, true);
}
}
}
confdir.push("conf.d"); confdir.push("conf.d");
if let Ok(entries) = fs::read_dir(&confdir) { if let Ok(entries) = fs::read_dir(&confdir) {
let mut paths: Vec<_> = entries let mut paths: Vec<_> = entries
.flatten() .flatten()
@@ -57,7 +76,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 +86,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 +195,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 +228,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 +284,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

@@ -76,7 +76,7 @@ pub struct HostConfig {
pub struct Auth { pub struct Auth {
#[serde(rename = "type")] #[serde(rename = "type")]
pub auth_type: String, pub auth_type: String,
#[serde(rename = "creds")] #[serde(rename = "data")]
pub auth_cred: String, pub auth_cred: String,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
@@ -119,7 +119,7 @@ pub struct AppConfig {
pub proxy_port_tls: Option<String>, pub proxy_port_tls: Option<String>,
pub proxy_port: Option<String>, pub proxy_port: Option<String>,
pub local_server: Option<(String, u16)>, pub local_server: Option<(String, u16)>,
pub proxy_certificates: Option<String>, pub proxy_configs: Option<String>,
pub proxy_tls_grade: Option<String>, pub proxy_tls_grade: Option<String>,
pub file_server_address: Option<String>, pub file_server_address: Option<String>,
pub file_server_folder: Option<String>, pub file_server_folder: Option<String>,

View File

@@ -1,10 +1,9 @@
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::{InnerMap, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::structs::{InnerMap, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::tls;
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};
@@ -207,9 +207,9 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
info!("Upstreams are fully populated. Ready to server requests"); info!("Upstreams are fully populated. Ready to server requests");
} }
pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> { pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
let mut f = HashMap::new(); let mut f = HashMap::new();
let mut certificate_configs: Vec<tls::CertificateConfig> = vec![]; let mut certificate_configs: Vec<load::CertificateConfig> = vec![];
let paths = fs::read_dir(dir).unwrap(); let paths = fs::read_dir(dir).unwrap();
for path in paths { for path in paths {
let path_str = path.unwrap().path().to_str().unwrap().to_owned(); let path_str = path.unwrap().path().to_str().unwrap().to_owned();
@@ -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,3 +1,4 @@
use crate::tls::acme::order::refresh_order;
use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider}; use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::utils::parceyaml::load_configuration; use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration; use crate::utils::structs::Configuration;
@@ -50,10 +51,16 @@ impl BackgroundService for LB {
} }
} }
let confdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/autoconfigs";
let certdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/certificates";
let api_load = APIUpstreamProvider { let api_load = APIUpstreamProvider {
address: self.config.config_address.clone(), address: self.config.config_address.clone(),
masterkey: self.config.master_key.clone(), masterkey: self.config.master_key.clone(),
config_api_enabled: self.config.config_api_enabled.clone(), config_api_enabled: self.config.config_api_enabled.clone(),
// certs_dir: self.config.proxy_certificates.clone().unwrap_or_else(|| "/tmp".to_string()),
config_dir: confdir.clone(),
certs_dir: certdir.clone(),
// tls_address: self.config.config_tls_address.clone(), // tls_address: self.config.config_tls_address.clone(),
// tls_certificate: self.config.config_tls_certificate.clone(), // tls_certificate: self.config.config_tls_certificate.clone(),
// tls_key_file: self.config.config_tls_key_file.clone(), // tls_key_file: self.config.config_tls_key_file.clone(),
@@ -62,6 +69,7 @@ impl BackgroundService for LB {
current_upstreams: self.ump_upst.clone(), current_upstreams: self.ump_upst.clone(),
full_upstreams: self.ump_full.clone(), full_upstreams: self.ump_full.clone(),
}; };
// let crtdir = api_load.certs_dir.clone();
// let tx_api = tx.clone(); // let tx_api = tx.clone();
let _ = tokio::spawn(async move { api_load.start(tx_api).await }); let _ = tokio::spawn(async move { api_load.start(tx_api).await });
@@ -70,6 +78,7 @@ impl BackgroundService for LB {
let im = self.ump_byid.clone(); let im = self.ump_byid.clone();
let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval); let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval);
let _ = tokio::spawn(async move { healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await }); let _ = tokio::spawn(async move { healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await });
let _ = tokio::spawn(async move { refresh_order(certdir, confdir).await });
loop { loop {
tokio::select! { tokio::select! {

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() {
@@ -91,7 +89,7 @@ impl ProxyHttp for LB {
None => return Ok(false), None => return Ok(false),
Some(ref innermap) => { Some(ref innermap) => {
if let Some(auth) = _ctx.extraparams.authentication.as_ref().or(innermap.authorization.as_ref()) { 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; let _ = session.respond_error(401).await;
warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path()); warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path());
return Ok(true); 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 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

@@ -1,7 +1,8 @@
// use rustls::crypto::ring::default_provider; // use rustls::crypto::ring::default_provider;
use crate::tls::grades;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::Extraparams; use crate::utils::structs::Extraparams;
use crate::utils::tls;
use crate::utils::tls::CertificateConfig;
use crate::utils::tools::*; use crate::utils::tools::*;
use crate::web::proxyhttp::LB; use crate::web::proxyhttp::LB;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
@@ -63,32 +64,33 @@ pub fn run() {
Some(bind_address_tls) => { Some(bind_address_tls) => {
check_priv(bind_address_tls.as_str()); check_priv(bind_address_tls.as_str());
let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel(); let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel();
let certs_path = cfg.proxy_certificates.clone().unwrap(); // let certs_path = cfg.proxy_certificates.clone().unwrap();
let certs_path = cfg.proxy_configs.clone().unwrap() + "/certificates";
thread::spawn(move || { thread::spawn(move || {
watch_folder(certs_path, tx).unwrap(); watch_folder(certs_path, tx).unwrap();
}); });
let certificate_configs = rx.recv().unwrap(); let certificate_configs = rx.recv().unwrap();
let first_set = tls::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info")); let first_set = load::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
let certificates = Arc::new(ArcSwap::from_pointee(first_set)); let certificates = Arc::new(ArcSwap::from_pointee(first_set));
let certs_for_callback = certificates.clone(); let certs_for_callback = certificates.clone();
let certs_for_watcher = certificates.clone(); let certs_for_watcher = certificates.clone();
let new_certs = tls::Certificates::new(&certificate_configs, grade.as_str()); let new_certs = load::Certificates::new(&certificate_configs, grade.as_str());
certs_for_watcher.store(Arc::new(new_certs.unwrap())); certs_for_watcher.store(Arc::new(new_certs.unwrap()));
let mut tls_settings = let mut tls_settings =
TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).expect("unable to load or parse cert/key"); TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).expect("unable to load or parse cert/key");
tls::set_tsl_grade(&mut tls_settings, grade.as_str()); grades::set_tsl_grade(&mut tls_settings, grade.as_str());
tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert)); tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert));
tls_settings.set_alpn_select_callback(tls::prefer_h2); tls_settings.set_alpn_select_callback(grades::prefer_h2);
proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings); proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings);
let certs_for_watcher = certificates.clone(); let certs_for_watcher = certificates.clone();
thread::spawn(move || { thread::spawn(move || {
while let Ok(new_configs) = rx.recv() { while let Ok(new_configs) = rx.recv() {
let new_certs = tls::Certificates::new(&new_configs, grade.as_str()); let new_certs = load::Certificates::new(&new_configs, grade.as_str());
match new_certs { match new_certs {
Some(new_certs) => { Some(new_certs) => {
certs_for_watcher.store(Arc::new(new_certs)); certs_for_watcher.store(Arc::new(new_certs));

View File

@@ -1,33 +1,30 @@
// use std::net::SocketAddr;
use crate::tls::acme::order::CHALLENGES;
// use axum_server::tls_openssl::OpenSSLConfig;
use crate::tls::acme::{account, order};
use crate::utils::discovery::APIUpstreamProvider; use crate::utils::discovery::APIUpstreamProvider;
use crate::utils::jwt::Claims;
use crate::utils::structs::{Config, Configuration, UpstreamsDashMap}; 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::{any, get, post};
use axum::{Json, Router}; use axum::{Json, Router};
// use axum_server::tls_openssl::OpenSSLConfig;
use futures::channel::mpsc::Sender; use futures::channel::mpsc::Sender;
use futures::SinkExt; use futures::SinkExt;
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{encode, EncodingKey, Header};
use log::{error, info, warn}; use log::{error, info, warn};
use prometheus::{gather, Encoder, TextEncoder}; use prometheus::{gather, Encoder, TextEncoder};
use serde::{Deserialize, Serialize}; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
// 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;
#[derive(Deserialize)]
struct InputKey {
master_key: String,
owner: String,
valid: u64,
}
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
struct OutToken { struct OutToken {
token: String, token: String,
@@ -36,6 +33,8 @@ struct OutToken {
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
master_key: String, master_key: String,
cert_creds: String,
certs_dir: String,
config_sender: Sender<Configuration>, config_sender: Sender<Configuration>,
config_api_enabled: bool, config_api_enabled: bool,
current_upstreams: Arc<UpstreamsDashMap>, current_upstreams: Arc<UpstreamsDashMap>,
@@ -44,8 +43,11 @@ struct AppState {
#[allow(unused_mut)] #[allow(unused_mut)]
pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) { pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) {
let credsfile = config.config_dir.clone() + "/acme_credentials.json";
let app_state = AppState { let app_state = AppState {
master_key: config.masterkey.clone(), master_key: config.masterkey.clone(),
cert_creds: credsfile,
certs_dir: config.certs_dir.clone(),
config_sender: to_return.clone(), config_sender: to_return.clone(),
config_api_enabled: config.config_api_enabled.clone(), config_api_enabled: config.config_api_enabled.clone(),
current_upstreams: upstreams_curr, current_upstreams: upstreams_curr,
@@ -59,6 +61,9 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
// .route("/{*wildcard}", delete(senderror)) // .route("/{*wildcard}", delete(senderror))
// .nest_service("/static", static_files) // .nest_service("/static", static_files)
.route("/jwt", post(jwt_gen)) .route("/jwt", post(jwt_gen))
.route("/acme_create", any(acme_create))
.route("/acme_order/{*domain}", any(acme_order))
.route("/.well-known/acme-challenge/{*token}", any(http01_challenge))
.route("/conf", post(conf)) .route("/conf", post(conf))
.route("/metrics", get(metrics)) .route("/metrics", get(metrics))
.route("/status", get(status)) .route("/status", get(status))
@@ -88,28 +93,22 @@ 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 key_authorization(&headers, &params, &st.master_key) {
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(); Err(err) => {
} error!("Failed to parse upstreams file: {}", err);
} return Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from(format!("Failed: {}\n", err))).unwrap();
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\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();
}
} }
} }
} }
@@ -123,15 +122,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 { if payload.master_key == state.master_key {
let now = SystemTime::now() + Duration::from_secs(payload.valid * 60); let now = SystemTime::now() + Duration::from_secs(payload.exp * 60);
let a = now.duration_since(UNIX_EPOCH).unwrap().as_secs(); let expire = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
let claim = crate::utils::jwt::Claims { user: payload.owner, exp: a };
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())) { match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) {
Ok(t) => { Ok(t) => {
let tok = OutToken { token: t }; let tok = OutToken { token: t };
info!("Generating token: {:?}", tok); info!("Generating token: {:?}", tok.token);
(StatusCode::CREATED, Json(tok)) (StatusCode::CREATED, Json(tok))
} }
Err(e) => { Err(e) => {
@@ -196,6 +201,85 @@ async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String,
} }
Response::builder() Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Parameter mismatch"))) .body(Body::from(format!("{}", "Parameter mismatch")))
.unwrap() .unwrap()
} }
async fn acme_create(State(state): State<AppState>, Query(params): Query<HashMap<String, String>>, headers: HeaderMap) -> impl IntoResponse {
if !key_authorization(&headers, &params, &state.master_key) {
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap();
}
let _ = match account::load_or_create(state.cert_creds.as_str()).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to create account: {}", e)))
.unwrap()
}
};
}
async fn acme_order(
State(state): State<AppState>,
axum::extract::Path(domain): axum::extract::Path<String>,
Query(params): Query<HashMap<String, String>>,
headers: HeaderMap,
) -> impl IntoResponse {
if !key_authorization(&headers, &params, &state.master_key) {
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap();
}
let domain_clean = domain.trim_matches('/');
let _ = match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to order a certificate: {}", e)))
.unwrap()
}
};
}
pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse {
if let Ok(challenges) = CHALLENGES.read() {
// for k in challenges.iter() {
// println!(" ==> {} : {}", k.0, k.1);
// }
if let Some(key_authorization) = challenges.get(&token) {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(key_authorization.clone()))
.unwrap();
}
}
Response::builder()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", "text/plain")
.body(Body::from("Not found"))
.unwrap()
}
fn key_authorization(headers: &HeaderMap, params: &HashMap<String, String>, 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
}