diff --git a/README.md b/README.md index 2224dd3..ac379a3 100644 --- a/README.md +++ b/README.md @@ -57,15 +57,26 @@ Built on Rust, on top of **Cloudflare’s Pingora engine**, **Gazan** delivers w ### 🔧 `main.yaml` -- `proxy_address_http`: `0.0.0.0:6193` (HTTP listener) -- `proxy_address_tls`: `0.0.0.0:6194` (TLS listener, optional) -- `config_address`: `0.0.0.0:3000` (HTTP API for remote config push) -- `upstreams_conf`: `etc/upstreams.yaml` (location of upstreams config) -- `log_level`: `info` (verbosity of logs) -- `hc_method`: `HEAD`, `hc_interval`: `2s` (upstream health checks) -- `user` Optional. Drop privileges to regular user. To bind to privileged ports. Requires to start as root. -- `group` Optional. Drop privileges to regular group -- Other defaults: thread count, keep-alive pool size, etc. +| Key | Example Value | Description | +|----------------------------------|--------------------------------------|--------------------------------------------------------------------------------------------------------| +| **threads** | 12 | Static Linux x86_64 binary, without any system dependency | +| **user** | gazan | Optional, Username for running gazan after dropping root privileges, requires program to start as root | +| **group** | gazan | Optional,Group for running gazan after dropping root privileges, requires program to start as root | +| **daemon** | false | Run in background (boolean) | +| **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections | +| **pid_file** | /tmp/gazan.pid | Path to PID file | +| **error_log** | /tmp/gazan_err.log | Path to error log file | +| **upgrade_sock** | /tmp/gazan.sock | Path to live upgrade socket file | +| **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location | +| **proxy_address_http** | 0.0.0.0:6193 | Gazan HTTP bind address | +| **proxy_address_tls** | 0.0.0.0:6194 | Gazan HTTPS bind address (Optional) | +| **tls_certificate** | etc/server.crt | TLS cerficate file path Mandatory if proxy_address_tls is set, else optional | +| **tls_key_file** | etc/key.pe | TLS Key file path Mandatory if proxy_address_tls is set, else optional | +| **upstreams_conf** | etc/upstreams.yaml | The location of upstreams file | +| **log_level** | info | Log level , possible values : info, warn, error, debug, trace, off | +| **hc_method** | HEAD | Healthcheck method (HEAD, GET, POST are supported) UPPERCASE | +| **hc_interval** | 2 | Interval for health checks in seconds | +| **master_key** | 5aeff7f9-7b94-447c-af60-e8c488544a3e | Mater key for working with API server and JWT Secret generation | ### 🌐 `upstreams.yaml` @@ -189,10 +200,11 @@ To enable TLS for A proxy server: Currently only OpenSSL is supported, working o ## 📡 Remote Config API -You can push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. +Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. +URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml` ```bash -curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf +curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MSATERKEY} ``` --- @@ -214,7 +226,7 @@ curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf ```bash PAYLOAD='{ - "masterkey": "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774", + "master_key": "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774", "owner": "valod", "valid": 10 }' diff --git a/src/utils/metrics.rs b/src/utils/metrics.rs index 00bef9c..b39a0c9 100644 --- a/src/utils/metrics.rs +++ b/src/utils/metrics.rs @@ -1,6 +1,13 @@ +use pingora_http::Version; use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, IntCounterVec}; use std::time::Duration; +pub struct MetricTypes { + pub method: String, + pub code: String, + pub latency: Duration, + pub version: Version, +} lazy_static::lazy_static! { pub static ref REQUEST_COUNT: IntCounter = register_int_counter!( "gazan_requests_total", @@ -26,12 +33,35 @@ lazy_static::lazy_static! { "Number of requests by HTTP method", &["method"] ).unwrap(); + pub static ref REQUESTS_BY_VERSION: IntCounterVec = register_int_counter_vec!( + "gazan_requests_by_version_total", + "Number of requests by HTTP versions", + &["version"] + ).unwrap(); pub static ref ERROR_COUNT: IntCounter = register_int_counter!( "gazan_errors_total", "Total number of errors" ).unwrap(); } +pub fn calc_metrics(metric_types: &MetricTypes) { + REQUEST_COUNT.inc(); + let timer = REQUEST_LATENCY.start_timer(); + timer.observe_duration(); + + let version_str = match &metric_types.version { + &Version::HTTP_11 => "HTTP/1.1", + &Version::HTTP_2 => "HTTP/2.0", + &Version::HTTP_3 => "HTTP/3.0", + &Version::HTTP_10 => "HTTP/1.0", + _ => "Unknown", + }; + REQUESTS_BY_VERSION.with_label_values(&[&version_str]).inc(); + RESPONSE_CODES.with_label_values(&[&metric_types.code.to_string()]).inc(); + REQUESTS_BY_METHOD.with_label_values(&[&metric_types.method]).inc(); + RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64()); +} +/* pub fn calc_metrics(method: String, code: u16, latency: Duration) { REQUEST_COUNT.inc(); let timer = REQUEST_LATENCY.start_timer(); @@ -41,7 +71,6 @@ pub fn calc_metrics(method: String, code: u16, latency: Duration) { RESPONSE_LATENCY.observe(latency.as_secs_f64()); } -/* tokio::spawn(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(5)); loop { diff --git a/src/web/proxyhttp.rs b/src/web/proxyhttp.rs index 7295e6d..84395d0 100644 --- a/src/web/proxyhttp.rs +++ b/src/web/proxyhttp.rs @@ -95,7 +95,7 @@ impl ProxyHttp for LB { if let Some(addr) = session.server_addr() { if let Some((host, _)) = addr.to_string().split_once(':') { let uri = session.req_header().uri.path_and_query().map_or("/", |pq| pq.as_str()); - let port = self.config.proxy_port_tls.unwrap_or(443); + let port = self.config.proxy_port_tls.unwrap_or(403); ctx.to_https = true; ctx.redirect_to = format!("https://{}:{}{}", host, port, uri); } @@ -186,11 +186,13 @@ impl ProxyHttp for LB { async fn logging(&self, session: &mut Session, _e: Option<&pingora::Error>, ctx: &mut Self::CTX) { let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16()); debug!("{}, response code: {response_code}", self.request_summary(session, ctx)); - - let method = session.req_header().method.to_string(); - let status = session.response_written().map(|resp| resp.status.as_u16()).unwrap_or(0); - let latency = ctx.start_time.elapsed(); - calc_metrics(method, status, latency); + let m = &MetricTypes { + method: session.req_header().method.to_string(), + code: session.response_written().map(|resp| resp.status.as_str().to_owned()).unwrap_or("0".to_string()), + latency: ctx.start_time.elapsed(), + version: session.req_header().version, + }; + calc_metrics(m); } } diff --git a/src/web/webserver.rs b/src/web/webserver.rs index f923ed1..291fb1e 100644 --- a/src/web/webserver.rs +++ b/src/web/webserver.rs @@ -1,6 +1,6 @@ use crate::utils::structs::Configuration; use axum::body::Body; -use axum::extract::State; +use axum::extract::{Query, State}; use axum::http::{Response, StatusCode}; use axum::response::IntoResponse; use axum::routing::{delete, get, head, post, put}; @@ -11,6 +11,7 @@ use jsonwebtoken::{encode, EncodingKey, Header}; use log::{error, info, warn}; use prometheus::{gather, Encoder, TextEncoder}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::net::TcpListener; @@ -26,9 +27,18 @@ struct OutToken { token: String, } +#[derive(Clone)] +struct AppState { + master_key: String, + config_sender: Sender, +} + #[allow(unused_mut)] pub async fn run_server(bindaddress: String, master_key: String, mut to_return: Sender) { - let mut tr = to_return.clone(); + let app_state = AppState { + master_key: master_key.clone(), + config_sender: to_return.clone(), + }; let app = Router::new() .route("/{*wildcard}", get(senderror)) .route("/{*wildcard}", post(senderror)) @@ -36,38 +46,30 @@ pub async fn run_server(bindaddress: String, master_key: String, mut to_return: .route("/{*wildcard}", head(senderror)) .route("/{*wildcard}", delete(senderror)) .route("/jwt", post(jwt_gen)) + .route("/conf", post(conf)) .route("/metrics", get(metrics)) - .with_state(master_key.clone()) - .route( - "/conf", - post(|up: String| async move { - let serverlist = crate::utils::parceyaml::load_configuration(up.as_str(), "content"); - - match serverlist { - Some(serverlist) => { - let _ = tr.send(serverlist).await.unwrap(); - Response::builder().status(StatusCode::CREATED).body(Body::from("Config, conf file, updated!\n")).unwrap() - } - None => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from("Failed to parce config file!\n")) - .unwrap(), - } - }) - .with_state("state"), - ); + .with_state(app_state); let listener = TcpListener::bind(bindaddress.clone()).await.unwrap(); info!("Starting the API server on: {}", bindaddress); axum::serve(listener, app).await.unwrap(); } -#[allow(dead_code)] -async fn senderror() -> impl IntoResponse { - Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("No live upstream found!\n")).unwrap() +async fn conf(State(mut st): State, Query(params): Query>, content: String) -> impl IntoResponse { + if let Some(s) = params.get("key") { + if s.to_owned() == st.master_key.to_owned() { + if let Some(serverlist) = crate::utils::parceyaml::load_configuration(content.as_str(), "content") { + st.config_sender.send(serverlist).await.unwrap(); + return Response::builder().status(StatusCode::OK).body(Body::from("Config, conf file, updated !\n")).unwrap(); + } else { + return Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("Failed to parse config!\n")).unwrap(); + }; + } + } + Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap() } -async fn jwt_gen(State(master_key): State, Json(payload): Json) -> (StatusCode, Json) { - if payload.master_key == master_key { +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.valid * 60); let a = now.duration_since(UNIX_EPOCH).unwrap().as_secs(); let claim = crate::utils::jwt::Claims { user: payload.owner, exp: a }; @@ -111,3 +113,8 @@ async fn metrics() -> impl IntoResponse { .body(Body::from(buffer)) .unwrap() } + +#[allow(dead_code)] +async fn senderror() -> impl IntoResponse { + Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("No live upstream found!\n")).unwrap() +}