// 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}; use axum::body::Body; use axum::extract::{Query, State}; use axum::http::{header::HeaderMap, Response, StatusCode}; use axum::response::IntoResponse; use axum::routing::{any, get, post}; use axum::{Json, Router}; use futures::channel::mpsc::Sender; use futures::SinkExt; use jsonwebtoken::{encode, EncodingKey, Header}; use log::{debug, error, info, warn}; use prometheus::{gather, Encoder, TextEncoder}; use serde::Serialize; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use subtle::ConstantTimeEq; use tokio::net::TcpListener; use tower_http::services::ServeDir; #[derive(Serialize, Debug)] struct OutToken { token: String, } #[derive(Clone)] struct AppState { master_key: String, cert_creds: String, certs_dir: String, upstreams_file: String, config_sender: Sender, config_api_enabled: bool, current_upstreams: Arc, full_upstreams: Arc, } #[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(), upstreams_file: config.upstreams_file.clone(), config_sender: to_return.clone(), config_api_enabled: config.config_api_enabled, current_upstreams: upstreams_curr, full_upstreams: upstreams_full, }; let app = Router::new() // .route("/{*wildcard}", get(senderror)) // .route("/{*wildcard}", post(senderror)) // .route("/{*wildcard}", put(senderror)) // .route("/{*wildcard}", head(senderror)) // .route("/{*wildcard}", delete(senderror)) // .nest_service("/static", static_files) .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("/metrics", get(metrics)) .route("/status", get(status)) .with_state(app_state); // if let Some(value) = &config.tls_address { // let cf = OpenSSLConfig::from_pem_file(config.tls_certificate.clone().unwrap(), config.tls_key_file.clone().unwrap()).unwrap(); // let addr: SocketAddr = value.parse().expect("Unable to parse socket address"); // let tls_app = app.clone(); // tokio::spawn(async move { // if let Err(e) = axum_server::bind_openssl(addr, cf).serve(tls_app.into_make_service()).await { // eprintln!("TLS server failed: {}", e); // } // }); // info!("Starting the TLS API server on: {}", value); // } if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) { let static_files = ServeDir::new(folder); let static_serve: Router = Router::new().fallback_service(static_files); let static_listen = TcpListener::bind(address).await.unwrap(); drop(tokio::spawn(async move { axum::serve(static_listen, static_serve).await.unwrap() })); } let listener = TcpListener::bind(config.address.clone()).await.unwrap(); info!("Starting the API server on: {}", config.address); axum::serve(listener, app).await.unwrap(); } async fn conf(State(st): State, Query(params): Query>, headers: HeaderMap, content: String) -> impl IntoResponse { if !st.config_api_enabled { return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap(); } // if let Some(s) = headers.get("x-api-key").and_then(|v| v.to_str().ok()).or(params.get("key").map(|s| s.as_str())) { if key_authorization(&headers, ¶ms, &st.master_key) { let strcontent = content.as_str(); let parsed = serde_yml::from_str::(strcontent); match parsed { Ok(_) => { if let Some(_) = params.get("save") { drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await })); } else { drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await })); } // apply_config(content.as_str(), st).await; return Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap(); } Err(err) => { error!("Failed to parse upstreams file: {}", err); return Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from(format!("Failed: {}\n", err))).unwrap(); } } } Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap() } async fn apply_config(content: &str, mut st: AppState, save: bool) { let sl = crate::utils::parceyaml::load_configuration(content, "content").await; if let Some(serverlist) = sl.0 { if save { info!("Saving new upstreams to: {}", st.upstreams_file); if let Err(err) = std::fs::write(&st.upstreams_file, content) { error!("Error saving to: {} : {}", st.upstreams_file, err); } } let _ = st.config_sender.send(serverlist).await; } } async fn jwt_gen(State(state): State, Json(payload): Json) -> (StatusCode, Json) { if payload.master_key == state.master_key { let now = SystemTime::now() + Duration::from_secs(payload.exp * 60); let expire = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); let claim = Claims { master_key: String::new(), owner: payload.owner, exp: expire, random: payload.random, }; match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) { Ok(t) => { let tok = OutToken { token: t }; debug!("Generating token: {:?}", tok.token); (StatusCode::CREATED, Json(tok)) } Err(e) => { let tok = OutToken { token: "ERROR".to_string() }; error!("Failed to generate token: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(tok)) } } } else { let tok = OutToken { token: "Unauthorised".to_string(), }; warn!("Unauthorised JWT generate request: {:?}", tok); (StatusCode::FORBIDDEN, Json(tok)) } } async fn metrics() -> impl IntoResponse { let metric_families = gather(); let encoder = TextEncoder::new(); let mut buffer = Vec::new(); if let Err(e) = encoder.encode(&metric_families, &mut buffer) { // encoding error fallback return Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(format!("Failed to encode metrics: {}", e))) .unwrap(); } Response::builder() .status(StatusCode::OK) .header("Content-Type", encoder.format_type()) .body(Body::from(buffer)) .unwrap() } #[allow(clippy::needless_return)] async fn status(State(st): State, Query(params): Query>) -> impl IntoResponse { if params.contains_key("live") { let r = upstreams_liveness_json(&st.full_upstreams, &st.current_upstreams); return Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(format!("{}", r))) .unwrap(); } if params.contains_key("all") { let resp = upstreams_to_json(&st.current_upstreams); match resp { Ok(j) => { return Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(j)) .unwrap() } Err(e) => { return Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(format!("Failed to get status: {}", e))) .unwrap(); } } } Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from("Parameter mismatch")) .unwrap() } #[allow(clippy::needless_return)] async fn acme_create(State(state): State, 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(); } 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() } }; } #[allow(clippy::needless_return)] 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('/'); 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 } // -- ⚝ by Dave -- in NeoVim ⚝ --