diff --git a/TODO.md b/TODO.md index fe8eb92..fa0494f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,12 @@ # TODO -- [ ] authentication for admin page - [ ] support etcd or other storage for config - [ ] better error handler - [ ] log rotate - [ ] tls cert auto update - [ ] support validate config before save(web) +- [ ] auto reload config and restart +- [x] authentication for admin page - [x] custom error for pingora error - [x] support alpn for location - [x] support add header for location diff --git a/conf/pingap.toml b/conf/pingap.toml index 4dc33bd..0be5157 100644 --- a/conf/pingap.toml +++ b/conf/pingap.toml @@ -117,3 +117,5 @@ stats_path = "/stats" # Admin path for admin server. Default `None` admin_path = "/pingap" +# Basic authorization for admin server, value is `base64(account + ":" + password). Default `None` +# authorization = "dGVzdDoxMjMxMjM=" diff --git a/src/config/load.rs b/src/config/load.rs index 077cddd..19a7afe 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -198,6 +198,7 @@ impl LocationConf { pub struct ServerConf { pub addr: String, pub access_log: Option, + pub authorization: Option, pub locations: Option>, pub tls_cert: Option, pub tls_key: Option, diff --git a/src/http_extra/http_header.rs b/src/http_extra/http_header.rs index 5a1e566..0feb4fd 100644 --- a/src/http_extra/http_header.rs +++ b/src/http_extra/http_header.rs @@ -54,6 +54,13 @@ pub static HTTP_HEADER_NO_STORE: Lazy = Lazy::new(|| { ) }); +pub static HTTP_HEADER_WWW_AUTHENTICATE: Lazy = Lazy::new(|| { + ( + header::WWW_AUTHENTICATE, + HeaderValue::from_str(r###"Basic realm="Pingap""###).unwrap(), + ) +}); + pub static HTTP_HEADER_NO_CACHE: Lazy = Lazy::new(|| { ( header::CACHE_CONTROL, diff --git a/src/proxy/server.rs b/src/proxy/server.rs index 473e35a..4ee96cc 100644 --- a/src/proxy/server.rs +++ b/src/proxy/server.rs @@ -15,7 +15,7 @@ use super::logger::Parser; use super::{Location, Upstream}; use crate::config::{LocationConf, PingapConf, UpstreamConf}; -use crate::http_extra::{HttpResponse, HTTP_HEADER_CONTENT_JSON}; +use crate::http_extra::{HttpResponse, HTTP_HEADER_CONTENT_JSON, HTTP_HEADER_WWW_AUTHENTICATE}; use crate::serve::Serve; use crate::serve::ADMIN_SERVE; use crate::state::{get_hostname, State}; @@ -72,6 +72,7 @@ pub struct ServerConf { pub stats_path: Option, pub admin_path: Option, pub access_log: Option, + pub authorization: Option, pub upstreams: Vec<(String, UpstreamConf)>, pub locations: Vec<(String, LocationConf)>, pub tls_cert: Option>, @@ -135,6 +136,7 @@ impl From for Vec { tls_cert, tls_key, admin: false, + authorization: item.authorization, stats_path: item.stats_path, admin_path: item.admin_path, addr: item.addr, @@ -164,6 +166,7 @@ pub struct Server { processing: AtomicI32, locations: Vec, log_parser: Option, + authorization: Option, error_template: String, stats_path: Option, admin_path: Option, @@ -227,6 +230,7 @@ impl Server { processing: AtomicI32::new(0), stats_path: conf.stats_path, admin_path: conf.admin_path, + authorization: conf.authorization, addr: conf.addr, log_parser: p, locations, @@ -314,12 +318,36 @@ impl Server { ctx.status = Some(StatusCode::OK); ctx.response_body_size = size; } + fn auth_validate(&self, req_header: &RequestHeader) -> bool { + if let Some(authorization) = &self.authorization { + let value = + utils::get_req_header_value(req_header, "Authorization").unwrap_or_default(); + if value.is_empty() { + return false; + } + if value != format!("Basic {authorization}") { + return false; + } + } + true + } async fn serve_admin( &self, admin_path: &str, session: &mut Session, ctx: &mut State, ) -> pingora::Result { + if !self.auth_validate(session.req_header()) { + let _ = HttpResponse { + status: StatusCode::UNAUTHORIZED, + headers: Some(vec![HTTP_HEADER_WWW_AUTHENTICATE.clone()]), + ..Default::default() + } + .send(session) + .await?; + return Ok(true); + } + let header = session.req_header_mut(); let path = header.uri.path(); let mut new_path = path.substring(admin_path.len(), path.len()).to_string(); diff --git a/src/serve/admin.rs b/src/serve/admin.rs index a280ea8..59a20d2 100644 --- a/src/serve/admin.rs +++ b/src/serve/admin.rs @@ -249,7 +249,7 @@ impl Serve for AdminServe { memory, }) .unwrap_or(HttpResponse::unknown_error()) - } else if path == "/restart" { + } else if path == "/restart" && method == Method::POST { if let Err(e) = restart() { error!("Restart fail: {e}"); return Err(utils::new_internal_error(400, e.to_string())); diff --git a/src/state/process.rs b/src/state/process.rs index 4bfb2a9..85e46fd 100644 --- a/src/state/process.rs +++ b/src/state/process.rs @@ -77,7 +77,7 @@ pub fn restart() -> io::Result { "Pingap is restarting", )); } - info!("pingap will restart now"); + info!("pingap will restart"); if let Some(cmd) = CMD.get() { nix::sys::signal::kill( nix::unistd::Pid::from_raw(std::process::id() as i32), diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 7637940..985c998 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -67,7 +67,7 @@ pub fn get_remote_addr(session: &Session) -> Option { /// Gets client ip from X-Forwarded-For, /// If none, get from X-Real-Ip, -/// If none, get remote addr +/// If none, get remote addr. pub fn get_client_ip(session: &Session) -> String { if let Some(value) = session.get_header(HTTP_HEADER_X_FORWARDED_FOR.clone()) { let arr: Vec<&str> = value.to_str().unwrap_or_default().split(',').collect(); @@ -84,6 +84,7 @@ pub fn get_client_ip(session: &Session) -> String { "".to_string() } +/// Gets string value from req header. pub fn get_req_header_value<'a>(req_header: &'a RequestHeader, key: &str) -> Option<&'a str> { if let Some(value) = req_header.headers.get(key) { if let Ok(value) = value.to_str() { @@ -93,6 +94,7 @@ pub fn get_req_header_value<'a>(req_header: &'a RequestHeader, key: &str) -> Opt None } +/// Gets cookie value from req header. pub fn get_cookie_value<'a>(req_header: &'a RequestHeader, cookie_name: &str) -> Option<&'a str> { if let Some(cookie_value) = get_req_header_value(req_header, "Cookie") { for item in cookie_value.split(';') { @@ -106,6 +108,7 @@ pub fn get_cookie_value<'a>(req_header: &'a RequestHeader, cookie_name: &str) -> None } +/// Gets query value from req header. pub fn get_query_value<'a>(req_header: &'a RequestHeader, name: &str) -> Option<&'a str> { if let Some(query) = req_header.uri.query() { for item in query.split('&') { @@ -119,6 +122,7 @@ pub fn get_query_value<'a>(req_header: &'a RequestHeader, name: &str) -> Option< None } +/// Creates a new internal error pub fn new_internal_error(status: u16, message: String) -> pingora::BError { pingora::Error::because( pingora::ErrorType::HTTPStatus(status), diff --git a/web/src/pages/server-info.tsx b/web/src/pages/server-info.tsx index b7815d2..5cd1476 100644 --- a/web/src/pages/server-info.tsx +++ b/web/src/pages/server-info.tsx @@ -45,24 +45,24 @@ export default function ServerInfo() { options: locations, }, { - id: "stats_path", - label: "Stats Path", - defaultValue: server.stats_path, + id: "admin_path", + label: "Admin Path", + defaultValue: server.admin_path, span: 6, category: FormItemCategory.TEXT, }, { - id: "admin_path", - label: "Admin Path", - defaultValue: server.admin_path, + id: "authorization", + label: "Authorization", + defaultValue: server.authorization, span: 6, category: FormItemCategory.TEXT, }, { - id: "access_log", - label: "Access Log", - defaultValue: server.access_log, - span: 12, + id: "stats_path", + label: "Stats Path", + defaultValue: server.stats_path, + span: 6, category: FormItemCategory.TEXT, }, { @@ -72,6 +72,14 @@ export default function ServerInfo() { span: 6, category: FormItemCategory.NUMBER, }, + { + id: "access_log", + label: "Access Log", + defaultValue: server.access_log, + span: 12, + category: FormItemCategory.TEXT, + }, + { id: "tls_cert", label: "Tls Cert(base64)", diff --git a/web/src/states/config.ts b/web/src/states/config.ts index 6c50f9d..879dd33 100644 --- a/web/src/states/config.ts +++ b/web/src/states/config.ts @@ -41,6 +41,7 @@ interface Server { tls_key?: string; stats_path?: string; admin_path?: string; + authorization?: string; remark?: string; }