diff --git a/README.md b/README.md index a0b917c..be01b7f 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ Built on Rust, on top of **Cloudflare’s Pingora engine**, **Gazan** delivers w - 🔁 **Hot Reloading:** Modify upstreams on the fly via `upstreams.yaml` — no restart needed - 🔮 **Automatic WebSocket Support:** Zero config — connection upgrades are handled seamlessly - 🔮 **Automatic GRPC Support:** Zero config, Requires `ssl` to proxy, gRPC is handled seamlessly +- 🔮 **Upstreams Session Stickiness:** Enable/Disable Sticky session support with single parameter in config file - 🔐 **TLS Termination:** Fully supports TLS for incoming and upstream traffic -- 🛡️ **Built-in Auth Support:** +- 🛡️ **Built-in Auth Support:** Basic Auth, JWT, API key - 🧠 **CORS & Header Injection:** Global and per-route header configuration - 🧪 **Health Checks:** Pluggable health check methods for upstreams - 🛰️ **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems @@ -211,5 +212,6 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/ - Designed for edge proxying, internal routing, or hybrid cloud scenarios. - Transparent, fully automatic WebSocket upgrade support. - Transparent, fully automatic gRPC proxy. +- Sticky session support. - HTTP2 ready. - Upcoming Kubernetes integration \ No newline at end of file diff --git a/etc/main.yaml b/etc/main.yaml index 266bfd3..7dfefff 100644 --- a/etc/main.yaml +++ b/etc/main.yaml @@ -1,20 +1,20 @@ -# Default configuration file for Pingora, read only once at startup -threads: 8 # Pingora default setting +# Main configuration file , applied on startup +threads: 8 # Nubber of daemon threads default setting #user: pastor # Username for running gazan after dropping root privileges, requires program to start as root #group: pastor # Group for running gazan after dropping root privileges, requires program to start as root daemon: false # Run in background -upstream_keepalive_pool_size: 500 # Pingora default setting -pid_file: /tmp/gazan.pid # Pingora default setting -error_log: /tmp/gazan_err.log # Pingora default setting -upgrade_sock: /tmp/gazan.sock # Pingora default setting +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 +upgrade_sock: /tmp/gazan.sock # Path to 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 # Pingora default setting -proxy_address_tls: 0.0.0.0:6194 # Optional +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 tls_certificate: etc/server.crt # Mandatory if proxy_address_tls is set tls_key_file: etc/key.pem # Mandatory if proxy_address_tls is set upstreams_conf: etc/upstreams.yaml # the location of upstreams file log_level: info # info, warn, error, debug, trace, off hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE -hc_interval: 2 #Intervak for Healthcheck in seconds -master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 -sticky_sessions: # If key exists, the sticky_sessions will be enabled, regardless what is in value. Comment out/delete the line to disable. \ No newline at end of file +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 +sticky_sessions: true # Globally enables/disables Sticky session . Boolean will panic on any other than true/false . \ No newline at end of file diff --git a/src/utils/parceyaml.rs b/src/utils/parceyaml.rs index f6fb708..4fac233 100644 --- a/src/utils/parceyaml.rs +++ b/src/utils/parceyaml.rs @@ -164,13 +164,31 @@ pub fn load_configuration(d: &str, kind: &str) -> Option { } } -pub fn parce_main_config(path: &str) -> DashMap { +#[derive(Debug, Deserialize)] +pub struct AppConfig { + pub sticky_sessions: bool, + pub hc_interval: u16, + pub hc_method: String, + pub upstreams_conf: String, + pub log_level: String, + pub config_address: String, + pub proxy_address_http: String, + pub master_key: String, + pub proxy_address_tls: Option, + pub tls_certificate: Option, + pub tls_key_file: Option, +} + +// pub fn parce_main_config(path: &str) -> DashMap { +pub fn parce_main_config(path: &str) -> AppConfig { info!("Parsing configuration"); let data = fs::read_to_string(path).unwrap(); let reply = DashMap::new(); let cfg: HashMap = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); + let mut cfo: AppConfig = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); + cfo.hc_method = cfo.hc_method.to_uppercase(); for (k, v) in cfg { reply.insert(k.to_string(), v.to_string()); } - reply + cfo } diff --git a/src/web/bgservice.rs b/src/web/bgservice.rs index 0e27129..5743daa 100644 --- a/src/web/bgservice.rs +++ b/src/web/bgservice.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use dashmap::DashMap; use futures::channel::mpsc; use futures::StreamExt; -use log::{error, info}; +use log::info; use pingora_core::server::ShutdownWatch; use pingora_core::services::background::BackgroundService; @@ -17,42 +17,30 @@ impl BackgroundService for LB { info!("Starting background service"); let (tx, mut rx) = mpsc::channel::(0); - let from_file = self.config.get("upstreams_conf"); - match from_file { - Some(from_file) => { - let tx_file = tx.clone(); - let tx_consul = tx.clone(); + let tx_file = tx.clone(); + let tx_consul = tx.clone(); - let file_load = FromFileProvider { path: from_file.to_string() }; - let consul_load = ConsulProvider { path: from_file.to_string() }; + let file_load = FromFileProvider { + path: self.config.upstreams_conf.clone(), + }; + let consul_load = ConsulProvider { + path: self.config.upstreams_conf.clone(), + }; - let _ = tokio::spawn(async move { file_load.start(tx_file).await }); - let _ = tokio::spawn(async move { consul_load.start(tx_consul).await }); - } - None => { - error!("Can't read config file"); - } - } - let config_address = self.config.get("config_address"); - let masterkey = self.config.get("master_key").unwrap(); - match config_address { - Some(config_address) => { - let api_load = APIUpstreamProvider { - address: config_address.to_string(), - masterkey: masterkey.value().to_string(), - }; - let tx_api = tx.clone(); - let _ = tokio::spawn(async move { api_load.start(tx_api).await }); - } - None => { - error!("Can't read config file"); - } - } + let _ = tokio::spawn(async move { file_load.start(tx_file).await }); + let _ = tokio::spawn(async move { consul_load.start(tx_consul).await }); + + let api_load = APIUpstreamProvider { + address: self.config.config_address.clone(), + masterkey: self.config.master_key.clone(), + }; + let tx_api = tx.clone(); + let _ = tokio::spawn(async move { api_load.start(tx_api).await }); let uu = self.ump_upst.clone(); let ff = self.ump_full.clone(); let im = self.ump_byid.clone(); - let (hc_method, hc_interval) = (self.config.get("hc_method").unwrap().clone(), self.config.get("hc_interval").unwrap().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 }); loop { diff --git a/src/web/gethosts.rs b/src/web/gethosts.rs index 1c4d051..c5bdc85 100644 --- a/src/web/gethosts.rs +++ b/src/web/gethosts.rs @@ -12,7 +12,7 @@ impl GetHost for LB { fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<(String, u16, bool)> { if let Some(b) = backend_id { if let Some(bb) = self.ump_byid.get(b) { - println!("BIB :===> {:?}", Some(bb.value())); + // println!("BIB :===> {:?}", Some(bb.value())); return Some(bb.value().clone()); } } @@ -44,7 +44,7 @@ impl GetHost for LB { } } } - println!("BMT :===> {:?}", best_match); + // println!("BMT :===> {:?}", best_match); best_match } fn get_header(&self, peer: &str, path: &str) -> Option> { diff --git a/src/web/proxyhttp.rs b/src/web/proxyhttp.rs index 040f8d4..3316887 100644 --- a/src/web/proxyhttp.rs +++ b/src/web/proxyhttp.rs @@ -1,4 +1,5 @@ use crate::utils::auth::authenticate; +use crate::utils::parceyaml::AppConfig; use crate::utils::tools::*; use crate::web::gethosts::GetHost; use async_trait::async_trait; @@ -18,7 +19,8 @@ pub struct LB { pub ump_full: Arc, pub ump_byid: Arc, pub headers: Arc, - pub config: Arc>, + // pub config: Arc>, + pub config: Arc, pub local: Arc<(String, u16)>, pub proxyconf: Arc>>, } @@ -52,7 +54,7 @@ impl ProxyHttp for LB { let mut backend_id = None; - if let Some(_) = self.config.get("sticky_sessions") { + if self.config.sticky_sessions { if let Some(cookies) = session.req_header().headers.get("cookie") { if let Ok(cookie_str) = cookies.to_str() { for cookie in cookie_str.split(';') { @@ -132,7 +134,7 @@ impl ProxyHttp for LB { async fn response_filter(&self, _session: &mut Session, _upstream_response: &mut ResponseHeader, _ctx: &mut Self::CTX) -> Result<()> { // _upstream_response.insert_header("X-Proxied-From", "Fooooooooooooooo").unwrap(); - if let Some(_) = self.config.get("sticky_sessions") { + if self.config.sticky_sessions { let backend_id = _ctx.backend_id.clone(); if let Some(bid) = self.ump_byid.get(&backend_id) { // let _ = _upstream_response.insert_header("set-cookie", format!("backend {}", bid.0)); diff --git a/src/web/start.rs b/src/web/start.rs index a022517..1c9b193 100644 --- a/src/web/start.rs +++ b/src/web/start.rs @@ -12,8 +12,10 @@ pub fn run() { let file = parameters.conf.clone().unwrap(); let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str()); + // println!("{:?}", maincfg); + let mut local_conf: (String, u16) = ("0.0.0.0".to_string(), 0); - if let Some((ip, port_str)) = maincfg.get("config_address").unwrap().split_once(':') { + if let Some((ip, port_str)) = maincfg.config_address.split_once(':') { if let Ok(port) = port_str.parse::() { local_conf = (ip.to_string(), port); } @@ -60,7 +62,7 @@ pub fn run() { // env_logger::Env::new(); // env_logger::init(); - let log_level = cfg.get("log_level").unwrap(); + let log_level = cfg.log_level.clone(); match log_level.as_str() { "info" => env::set_var("RUST_LOG", "info"), "error" => env::set_var("RUST_LOG", "error"), @@ -82,17 +84,17 @@ pub fn run() { let bg_srvc = background_service("bgsrvc", bg); let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb); - let bind_address_http = cfg.get("proxy_address_http").unwrap(); + let bind_address_http = cfg.proxy_address_http.clone(); - let bind_address_tls = cfg.get("proxy_address_tls"); + let bind_address_tls = cfg.proxy_address_tls.clone(); match bind_address_tls { Some(bind_address_tls) => { - info!("Running TLS listener on :{}", bind_address_tls.value()); - let cert_path = cfg.get("tls_certificate").unwrap(); - let key_path = cfg.get("tls_key_file").unwrap(); + info!("Running TLS listener on :{}", bind_address_tls); + let cert_path = cfg.tls_certificate.clone().unwrap(); + let key_path = cfg.tls_key_file.clone().unwrap(); let mut tls_settings = pingora_core::listeners::tls::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); tls_settings.enable_h2(); - proxy.add_tls_with_settings(bind_address_tls.value(), None, tls_settings); + proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings); } None => {} }