diff --git a/Cargo.lock b/Cargo.lock index 4d17950..5775168 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,7 @@ dependencies = [ "dashmap", "env_logger", "futures", + "instant-acme", "jsonwebtoken", "log", "mimalloc", @@ -143,6 +144,7 @@ dependencies = [ "privdrop", "prometheus 0.14.0", "rand 0.10.1", + "rcgen", "reqwest", "rustls-pemfile", "serde", @@ -241,6 +243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -1496,7 +1499,9 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", + "rustls-platform-verifier", "tokio", "tokio-rustls", "tower-service", @@ -1697,6 +1702,32 @@ dependencies = [ "libc", ] +[[package]] +name = "instant-acme" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f05ad37c421b962354c358d347d4a6130151df9407978372d3ad7f0c8f71a64" +dependencies = [ + "async-trait", + "aws-lc-rs", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "httpdate", + "hyper", + "hyper-rustls", + "hyper-util", + "rcgen", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -3049,6 +3080,21 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "aws-lc-rs", + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3151,7 +3197,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -3315,7 +3361,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -4089,6 +4135,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -4698,11 +4750,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs", + "aws-lc-rs", "data-encoding", "der-parser", "lazy_static", "nom", "oid-registry", + "ring", "rusticata-macros", "thiserror 2.0.18", "time", @@ -4714,6 +4768,15 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 71c6a5d..e9ab929 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,3 +46,5 @@ serde_json = "1.0.149" subtle = "2.6.1" moka = { version = "0.12.1", features = ["sync"] } ahash = "0.8.12" +instant-acme = "0.8.5" +rcgen = "0.14.7" diff --git a/src/tls.rs b/src/tls.rs index d15875c..d94b74b 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,2 +1,3 @@ +pub mod acme; pub mod grades; pub mod load; diff --git a/src/tls/acme.rs b/src/tls/acme.rs new file mode 100644 index 0000000..e2a28e0 --- /dev/null +++ b/src/tls/acme.rs @@ -0,0 +1,2 @@ +pub mod account; +pub mod order; diff --git a/src/tls/acme/account.rs b/src/tls/acme/account.rs new file mode 100644 index 0000000..8b90375 --- /dev/null +++ b/src/tls/acme/account.rs @@ -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 = OnceLock::new(); + +pub async fn get_account(file: &str) -> Result<&'static Account, Box> { + 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> { + 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> { + let account = get_account(file).await?; + Ok(account.id().to_string() + "\n") +} + +fn save_credentials(credentials: &AccountCredentials, file: &str) -> Result<(), Box> { + 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 { + if !Path::new(file).exists() { + return None; + } + let json = fs::read_to_string(file).ok()?; + serde_json::from_str(&json).ok() +} diff --git a/src/tls/acme/order.rs b/src/tls/acme/order.rs new file mode 100644 index 0000000..e1c7d39 --- /dev/null +++ b/src/tls/acme/order.rs @@ -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>> = 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> { + 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 = 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> { + 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) +} diff --git a/src/tls/load.rs b/src/tls/load.rs index 2f3ce59..fbe48bc 100644 --- a/src/tls/load.rs +++ b/src/tls/load.rs @@ -17,14 +17,14 @@ pub struct CertificateConfig { } #[derive(Debug)] -struct CertificateInfo { - common_names: Vec, - alt_names: Vec, - ssl_context: SslContext, +pub struct CertificateInfo { + pub common_names: Vec, + pub alt_names: Vec, + pub ssl_context: SslContext, #[allow(dead_code)] - cert_path: String, // Only used for logging + pub cert_path: String, // Only used for logging #[allow(dead_code)] - key_path: String, // Only used for logging + pub key_path: String, // Only used for logging } #[derive(Debug)] @@ -105,7 +105,7 @@ impl Certificates { } } -fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option { +pub fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option { let mut common_names = HashSet::new(); let mut alt_names = HashSet::new(); diff --git a/src/utils.rs b/src/utils.rs index 64ea747..df140ba 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,5 @@ pub mod auth; pub mod discovery; -pub mod dnsclient; mod filewatch; pub mod fordebug; pub mod healthcheck; diff --git a/src/utils/discovery.rs b/src/utils/discovery.rs index 14b46a0..a6233f9 100644 --- a/src/utils/discovery.rs +++ b/src/utils/discovery.rs @@ -10,6 +10,8 @@ pub struct APIUpstreamProvider { pub config_api_enabled: bool, pub address: String, pub masterkey: String, + pub certs_dir: String, + pub config_dir: String, // pub tls_address: Option, // pub tls_certificate: Option, // pub tls_key_file: Option, diff --git a/src/utils/dnsclient.rs b/src/utils/dnsclient.rs deleted file mode 100644 index d3d25e3..0000000 --- a/src/utils/dnsclient.rs +++ /dev/null @@ -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; - -pub struct DnsClientPool { - clients: Vec>, -} - -struct DnsClient { - client: Client, -} - -pub async fn start2(mut toreturn: Sender, config: Arc) { - 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 { - 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, AtomicUsize)>, DnsError> { - let upstreams: DashMap, 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, 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()), - } - } -} -*/ diff --git a/src/utils/parceyaml.rs b/src/utils/parceyaml.rs index aa74714..9612c0f 100644 --- a/src/utils/parceyaml.rs +++ b/src/utils/parceyaml.rs @@ -7,16 +7,35 @@ use log::{error, info, warn}; use std::collections::HashMap; use std::path::Path; use std::sync::atomic::AtomicUsize; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use std::{env, fs}; +pub static DOMAINS: LazyLock> = LazyLock::new(|| DashMap::new()); + pub async fn load_configuration(d: &str, kind: &str) -> (Option, String) { let mut conf_files = Vec::new(); let yaml_data = match kind { "filepath" => match fs::read_to_string(d) { Ok(data) => { 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> = 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"); + if let Ok(entries) = fs::read_dir(&confdir) { let mut paths: Vec<_> = entries .flatten() diff --git a/src/utils/structs.rs b/src/utils/structs.rs index 2a82961..beaf730 100644 --- a/src/utils/structs.rs +++ b/src/utils/structs.rs @@ -119,7 +119,7 @@ pub struct AppConfig { pub proxy_port_tls: Option, pub proxy_port: Option, pub local_server: Option<(String, u16)>, - pub proxy_certificates: Option, + pub proxy_configs: Option, pub proxy_tls_grade: Option, pub file_server_address: Option, pub file_server_folder: Option, diff --git a/src/web/bgservice.rs b/src/web/bgservice.rs index 1a723d4..8f63454 100644 --- a/src/web/bgservice.rs +++ b/src/web/bgservice.rs @@ -1,3 +1,4 @@ +use crate::tls::acme::order::refresh_order; use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider}; use crate::utils::parceyaml::load_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 { address: self.config.config_address.clone(), masterkey: self.config.master_key.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_certificate: self.config.config_tls_certificate.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(), full_upstreams: self.ump_full.clone(), }; + // let crtdir = api_load.certs_dir.clone(); // let tx_api = tx.clone(); 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 (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 { refresh_order(certdir, confdir).await }); loop { tokio::select! { diff --git a/src/web/start.rs b/src/web/start.rs index a34f533..a3ab4ec 100644 --- a/src/web/start.rs +++ b/src/web/start.rs @@ -64,7 +64,8 @@ pub fn run() { Some(bind_address_tls) => { check_priv(bind_address_tls.as_str()); let (tx, rx): (Sender>, Receiver>) = 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 || { watch_folder(certs_path, tx).unwrap(); }); diff --git a/src/web/webserver.rs b/src/web/webserver.rs index 5c9eacd..fdd6bd5 100644 --- a/src/web/webserver.rs +++ b/src/web/webserver.rs @@ -1,5 +1,8 @@ -use crate::utils::discovery::APIUpstreamProvider; // 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::jwt::Claims; use crate::utils::structs::{Config, Configuration, UpstreamsDashMap}; use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json}; @@ -7,9 +10,8 @@ use axum::body::Body; use axum::extract::{Query, State}; use axum::http::{header::HeaderMap, Response, StatusCode}; use axum::response::IntoResponse; -use axum::routing::{get, post}; +use axum::routing::{any, get, post}; use axum::{Json, Router}; -// use axum_server::tls_openssl::OpenSSLConfig; use futures::channel::mpsc::Sender; use futures::SinkExt; use jsonwebtoken::{encode, EncodingKey, Header}; @@ -31,6 +33,8 @@ struct OutToken { #[derive(Clone)] struct AppState { master_key: String, + cert_creds: String, + certs_dir: String, config_sender: Sender, config_api_enabled: bool, current_upstreams: Arc, @@ -39,8 +43,11 @@ struct AppState { #[allow(unused_mut)] pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender, upstreams_curr: Arc, upstreams_full: Arc) { + let credsfile = config.config_dir.clone() + "/acme_credentials.json"; let app_state = AppState { master_key: config.masterkey.clone(), + cert_creds: credsfile, + certs_dir: config.certs_dir.clone(), config_sender: to_return.clone(), config_api_enabled: config.config_api_enabled.clone(), current_upstreams: upstreams_curr, @@ -54,6 +61,9 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender, Query(params): Query(strcontent); - match parsed { - Ok(_) => { - 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(); - } + // if let Some(s) = headers.get("x-api-key").and_then(|v| v.to_str().ok()).or(params.get("key").map(|s| s.as_str())) { + if key_authorization(&headers, ¶ms, &st.master_key) { + let strcontent = content.as_str(); + let parsed = serde_yml::from_str::(strcontent); + match parsed { + Ok(_) => { + 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(); } } } @@ -192,6 +201,85 @@ async fn status(State(st): State, Query(params): Query, Query(params): Query>, headers: HeaderMap) -> impl IntoResponse { + if !key_authorization(&headers, ¶ms, &state.master_key) { + return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap(); + } + + 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, + axum::extract::Path(domain): axum::extract::Path, + Query(params): Query>, + headers: HeaderMap, +) -> impl IntoResponse { + if !key_authorization(&headers, ¶ms, &state.master_key) { + return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap(); + } + + 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) -> 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, 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 +}