diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..8cff25b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# CHANGELOG + +## 0.x.x (unreleased) + +### Improvement + +- Implement path replacing option for each reverse proxy backend group. diff --git a/TODO.md b/TODO.md index 3f1d8ff..b8d4087 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,7 @@ # TODO List - Improvement of path matcher -- Option for rewriting path like - ``` - https://example.com:8080/path/to -> http://backend:3030/any_path - ``` - Currently, incoming path (`/path/to/`) is always preserved in the mapping process, i.e., mapped to `backend:3030/path/to`. +- More flexible option for rewriting path - Smaller footprint of docker image using musl - Refactoring - Options to serve custom http_error page. diff --git a/config-example.toml b/config-example.toml index 8032962..98b83c0 100644 --- a/config-example.toml +++ b/config-example.toml @@ -56,6 +56,12 @@ upstream_options = ["override_host"] # Non-default destination in "localhost" app, which is routed by "path" [[apps.localhost.reverse_proxy]] path = '/maps' +# For request path starting with "/maps", +# this configuration results that any path like "/maps/org/any.ext" is mapped to "/replacing/path1/org/any.ext" +# by replacing "/maps" with "/replacing/path1" for routing to the locations given in upstream array +# Note that unless "path_replaced_with" is specified, the "path" is always preserved. +# "path_replaced_with" must be start from "/" (root path) +replace_path = "/replacing/path1" upstream = [ { location = 'www.bing.com', tls = true }, { location = 'www.bing.co.jp', tls = true }, diff --git a/src/backend.rs b/src/backend.rs index d7bd7df..ae86be2 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -39,11 +39,11 @@ pub struct Backend { #[derive(Debug, Clone)] pub struct ReverseProxy { - pub upstream: HashMap, // TODO: HashMapでいいのかは疑問。max_by_keyでlongest prefix matchしてるのも無駄っぽいが。。。 + pub upstream: HashMap, // TODO: HashMapでいいのかは疑問。max_by_keyでlongest prefix matchしてるのも無駄っぽいが。。。 } impl ReverseProxy { - pub fn get<'a>(&self, path_str: impl Into>) -> Option<&Upstream> { + pub fn get<'a>(&self, path_str: impl Into>) -> Option<&UpstreamGroup> { // trie使ってlongest prefix match させてもいいけどルート記述は少ないと思われるので、 // コスト的にこの程度で十分 let path_lc = path_str.into().to_ascii_lowercase(); @@ -91,7 +91,14 @@ impl Default for LoadBalance { #[derive(Debug, Clone)] pub struct Upstream { - pub uri: Vec, + pub uri: hyper::Uri, // base uri without specific path +} + +#[derive(Debug, Clone)] +pub struct UpstreamGroup { + pub upstream: Vec, + pub path: PathNameLC, + pub replace_path: Option, pub lb: LoadBalance, pub cnt: UpstreamCount, // counter for load balancing pub opts: HashSet, @@ -100,17 +107,17 @@ pub struct Upstream { #[derive(Debug, Clone, Default)] pub struct UpstreamCount(Arc); -impl Upstream { - pub fn get(&self) -> Option<&hyper::Uri> { +impl UpstreamGroup { + pub fn get(&self) -> Option<&Upstream> { match self.lb { LoadBalance::RoundRobin => { let idx = self.increment_cnt(); - self.uri.get(idx) + self.upstream.get(idx) } LoadBalance::Random => { let mut rng = rand::thread_rng(); - let max = self.uri.len() - 1; - self.uri.get(rng.gen_range(0..max)) + let max = self.upstream.len() - 1; + self.upstream.get(rng.gen_range(0..max)) } } } @@ -120,7 +127,7 @@ impl Upstream { } fn increment_cnt(&self) -> usize { - if self.current_cnt() < self.uri.len() - 1 { + if self.current_cnt() < self.upstream.len() - 1 { self.cnt.0.fetch_add(1, Ordering::Relaxed) } else { self.cnt.0.fetch_and(0, Ordering::Relaxed) diff --git a/src/config/parse.rs b/src/config/parse.rs index 88b6899..0bb3f4d 100644 --- a/src/config/parse.rs +++ b/src/config/parse.rs @@ -1,6 +1,6 @@ use super::toml::{ConfigToml, ReverseProxyOption}; use crate::{ - backend::{Backend, PathNameLC, ReverseProxy, Upstream}, + backend::{Backend, PathNameLC, ReverseProxy, UpstreamGroup}, backend_opt::UpstreamOption, constants::*, error::*, @@ -192,10 +192,20 @@ pub fn parse_opts(globals: &mut Globals) -> Result<()> { } fn get_reverse_proxy(rp_settings: &[ReverseProxyOption]) -> Result { - let mut upstream: HashMap = HashMap::default(); + let mut upstream: HashMap = HashMap::default(); rp_settings.iter().for_each(|rpo| { - let elem = Upstream { - uri: rpo.upstream.iter().map(|x| x.to_uri().unwrap()).collect(), + let path = match &rpo.path { + Some(p) => p.as_bytes().to_ascii_lowercase(), + None => "/".as_bytes().to_ascii_lowercase(), + }; + + let elem = UpstreamGroup { + upstream: rpo.upstream.iter().map(|x| x.to_upstream().unwrap()).collect(), + path: path.clone(), + replace_path: rpo + .replace_path + .as_ref() + .map_or_else(|| None, |v| Some(v.as_bytes().to_ascii_lowercase())), cnt: Default::default(), lb: Default::default(), opts: { @@ -210,11 +220,7 @@ fn get_reverse_proxy(rp_settings: &[ReverseProxyOption]) -> Result }, }; - if rpo.path.is_some() { - upstream.insert(rpo.path.as_ref().unwrap().as_bytes().to_ascii_lowercase(), elem); - } else { - upstream.insert("/".as_bytes().to_ascii_lowercase(), elem); - } + upstream.insert(path, elem); }); ensure!( rp_settings.iter().filter(|rpo| rpo.path.is_none()).count() < 2, diff --git a/src/config/toml.rs b/src/config/toml.rs index 7125e59..90a1961 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -1,4 +1,4 @@ -use crate::error::*; +use crate::{backend::Upstream, error::*}; use rustc_hash::FxHashMap as HashMap; use serde::Deserialize; use std::fs; @@ -52,6 +52,7 @@ pub struct TlsOption { #[derive(Deserialize, Debug, Default)] pub struct ReverseProxyOption { pub path: Option, + pub replace_path: Option, pub upstream: Vec, pub upstream_options: Option>, } @@ -62,7 +63,7 @@ pub struct UpstreamParams { pub tls: Option, } impl UpstreamParams { - pub fn to_uri(&self) -> Result { + pub fn to_upstream(&self) -> Result { let mut scheme = "http"; if let Some(t) = self.tls { if t { @@ -70,7 +71,9 @@ impl UpstreamParams { } } let location = format!("{}://{}", scheme, self.location); - location.parse::().map_err(|e| anyhow!("{}", e)) + Ok(Upstream { + uri: location.parse::().map_err(|e| anyhow!("{}", e))?, + }) } } diff --git a/src/handler/handler_main.rs b/src/handler/handler_main.rs index b69a489..615e479 100644 --- a/src/handler/handler_main.rs +++ b/src/handler/handler_main.rs @@ -1,7 +1,7 @@ // Highly motivated by https://github.com/felipenoris/hyper-reverse-proxy use super::{utils_headers::*, utils_request::*, utils_response::ResLog, utils_synth_response::*}; use crate::{ - backend::{ServerNameLC, Upstream}, + backend::{ServerNameLC, UpstreamGroup}, error::*, globals::Globals, log::*, @@ -80,15 +80,9 @@ where // Find reverse proxy for given path and choose one of upstream host // Longest prefix match let path = req.uri().path(); - let upstream = if let Some(upstream) = backend.reverse_proxy.get(path) { - upstream - } else { - return self.return_with_error_log(StatusCode::NOT_FOUND, &mut log_data); - }; - let upstream_scheme_host = if let Some(u) = upstream.get() { - u - } else { - return self.return_with_error_log(StatusCode::SERVICE_UNAVAILABLE, &mut log_data); + let upstream_group = match backend.reverse_proxy.get(path) { + Some(ug) => ug, + None => return self.return_with_error_log(StatusCode::NOT_FOUND, &mut log_data), }; // Upgrade in request header @@ -100,9 +94,8 @@ where &client_addr, &listen_addr, &mut req, - upstream_scheme_host, &upgrade_in_request, - upstream, + upstream_group, tls_enabled, ) { error!("Failed to generate destination uri for reverse proxy: {}", e); @@ -111,7 +104,7 @@ where // debug!("Request to be forwarded: {:?}", req_forwarded); req.log_debug(&client_addr, Some("(to Backend)")); log_data.xff(&req.headers().get("x-forwarded-for")); - log_data.upstream(&upstream_scheme_host.to_string()); + log_data.upstream(req.uri()); ////// // Forward request to @@ -223,9 +216,8 @@ where client_addr: &SocketAddr, listen_addr: &SocketAddr, req: &mut Request, - upstream_scheme_host: &Uri, upgrade: &Option, - upstream: &Upstream, + upstream_group: &UpstreamGroup, tls_enabled: bool, ) -> Result<()> { debug!("Generate request to be forwarded"); @@ -263,21 +255,37 @@ where .insert(header::HOST, HeaderValue::from_str(&org_host)?); }; + // Fix unique upstream destination since there could be multiple ones. + let upstream_chosen = upstream_group.get().ok_or_else(|| anyhow!("Failed to get upstream"))?; + // apply upstream-specific headers given in upstream_option let headers = req.headers_mut(); - apply_upstream_options_to_header(headers, client_addr, upstream_scheme_host, upstream)?; + apply_upstream_options_to_header(headers, client_addr, upstream_group, &upstream_chosen.uri)?; // update uri in request - ensure!(upstream_scheme_host.authority().is_some() && upstream_scheme_host.scheme().is_some()); + ensure!(upstream_chosen.uri.authority().is_some() && upstream_chosen.uri.scheme().is_some()); let new_uri = Uri::builder() - .scheme(upstream_scheme_host.scheme().unwrap().as_str()) - .authority(upstream_scheme_host.authority().unwrap().as_str()); - let pq = req.uri().path_and_query(); - *req.uri_mut() = match pq { - None => new_uri, - Some(x) => new_uri.path_and_query(x.to_owned()), + .scheme(upstream_chosen.uri.scheme().unwrap().as_str()) + .authority(upstream_chosen.uri.authority().unwrap().as_str()); + let org_pq = match req.uri().path_and_query() { + Some(pq) => pq.to_string(), + None => "/".to_string(), } - .build()?; + .into_bytes(); + + // replace some parts of path if opt_replace_path is enabled for chosen upstream + let new_pq = match &upstream_group.replace_path { + Some(new_path) => { + let matched_path: &[u8] = upstream_group.path.as_ref(); + ensure!(!matched_path.is_empty() && org_pq.len() >= matched_path.len()); + let mut new_pq = Vec::::with_capacity(org_pq.len() - matched_path.len() + new_path.len()); + new_pq.extend_from_slice(new_path); + new_pq.extend_from_slice(&org_pq[matched_path.len()..]); + new_pq + } + None => org_pq, + }; + *req.uri_mut() = new_uri.path_and_query(new_pq).build()?; // upgrade if let Some(v) = upgrade { @@ -288,7 +296,7 @@ where } // Change version to http/1.1 when destination scheme is http - if req.version() != Version::HTTP_11 && upstream_scheme_host.scheme() == Some(&Scheme::HTTP) { + if req.version() != Version::HTTP_11 && upstream_chosen.uri.scheme() == Some(&Scheme::HTTP) { *req.version_mut() = Version::HTTP_11; } else if req.version() == Version::HTTP_3 { debug!("HTTP/3 is currently unsupported for request to upstream. Use HTTP/2."); diff --git a/src/handler/utils_headers.rs b/src/handler/utils_headers.rs index 547efe1..dc956aa 100644 --- a/src/handler/utils_headers.rs +++ b/src/handler/utils_headers.rs @@ -1,4 +1,4 @@ -use crate::{backend::Upstream, backend_opt::UpstreamOption, error::*, log::*, utils::*}; +use crate::{backend::UpstreamGroup, backend_opt::UpstreamOption, error::*, log::*, utils::*}; use bytes::BufMut; use hyper::{ header::{self, HeaderMap, HeaderName, HeaderValue}, @@ -12,14 +12,14 @@ use std::net::SocketAddr; pub(super) fn apply_upstream_options_to_header( headers: &mut HeaderMap, _client_addr: &SocketAddr, - upstream_scheme_host: &Uri, - upstream: &Upstream, + upstream: &UpstreamGroup, + upstream_base_uri: &Uri, ) -> Result<()> { for opt in upstream.opts.iter() { match opt { UpstreamOption::OverrideHost => { // overwrite HOST value with upstream hostname (like 192.168.xx.x seen from rpxy) - let upstream_host = upstream_scheme_host.host().ok_or_else(|| anyhow!("none"))?; + let upstream_host = upstream_base_uri.host().ok_or_else(|| anyhow!("none"))?; headers .insert(header::HOST, HeaderValue::from_str(upstream_host)?) .ok_or_else(|| anyhow!("none"))?; diff --git a/src/log.rs b/src/log.rs index a8f4b8a..9612dc6 100644 --- a/src/log.rs +++ b/src/log.rs @@ -65,7 +65,7 @@ impl MessageLog { self.xff = xff.map_or_else(|| "", |v| v.to_str().unwrap_or("")).to_string(); self } - pub fn upstream(&mut self, upstream: &str) -> &mut Self { + pub fn upstream(&mut self, upstream: &hyper::Uri) -> &mut Self { self.upstream = upstream.to_string(); self }