implement path replacing option
This commit is contained in:
parent
6365fe298f
commit
a0aed6d848
9 changed files with 89 additions and 56 deletions
|
|
@ -0,0 +1,7 @@
|
||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 0.x.x (unreleased)
|
||||||
|
|
||||||
|
### Improvement
|
||||||
|
|
||||||
|
- Implement path replacing option for each reverse proxy backend group.
|
||||||
6
TODO.md
6
TODO.md
|
|
@ -1,11 +1,7 @@
|
||||||
# TODO List
|
# TODO List
|
||||||
|
|
||||||
- Improvement of path matcher
|
- Improvement of path matcher
|
||||||
- Option for rewriting path like
|
- More flexible option for rewriting path
|
||||||
```
|
|
||||||
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`.
|
|
||||||
- Smaller footprint of docker image using musl
|
- Smaller footprint of docker image using musl
|
||||||
- Refactoring
|
- Refactoring
|
||||||
- Options to serve custom http_error page.
|
- Options to serve custom http_error page.
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,12 @@ upstream_options = ["override_host"]
|
||||||
# Non-default destination in "localhost" app, which is routed by "path"
|
# Non-default destination in "localhost" app, which is routed by "path"
|
||||||
[[apps.localhost.reverse_proxy]]
|
[[apps.localhost.reverse_proxy]]
|
||||||
path = '/maps'
|
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 = [
|
upstream = [
|
||||||
{ location = 'www.bing.com', tls = true },
|
{ location = 'www.bing.com', tls = true },
|
||||||
{ location = 'www.bing.co.jp', tls = true },
|
{ location = 'www.bing.co.jp', tls = true },
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,11 @@ pub struct Backend {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ReverseProxy {
|
pub struct ReverseProxy {
|
||||||
pub upstream: HashMap<PathNameLC, Upstream>, // TODO: HashMapでいいのかは疑問。max_by_keyでlongest prefix matchしてるのも無駄っぽいが。。。
|
pub upstream: HashMap<PathNameLC, UpstreamGroup>, // TODO: HashMapでいいのかは疑問。max_by_keyでlongest prefix matchしてるのも無駄っぽいが。。。
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReverseProxy {
|
impl ReverseProxy {
|
||||||
pub fn get<'a>(&self, path_str: impl Into<Cow<'a, str>>) -> Option<&Upstream> {
|
pub fn get<'a>(&self, path_str: impl Into<Cow<'a, str>>) -> Option<&UpstreamGroup> {
|
||||||
// trie使ってlongest prefix match させてもいいけどルート記述は少ないと思われるので、
|
// trie使ってlongest prefix match させてもいいけどルート記述は少ないと思われるので、
|
||||||
// コスト的にこの程度で十分
|
// コスト的にこの程度で十分
|
||||||
let path_lc = path_str.into().to_ascii_lowercase();
|
let path_lc = path_str.into().to_ascii_lowercase();
|
||||||
|
|
@ -91,7 +91,14 @@ impl Default for LoadBalance {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Upstream {
|
pub struct Upstream {
|
||||||
pub uri: Vec<hyper::Uri>,
|
pub uri: hyper::Uri, // base uri without specific path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpstreamGroup {
|
||||||
|
pub upstream: Vec<Upstream>,
|
||||||
|
pub path: PathNameLC,
|
||||||
|
pub replace_path: Option<PathNameLC>,
|
||||||
pub lb: LoadBalance,
|
pub lb: LoadBalance,
|
||||||
pub cnt: UpstreamCount, // counter for load balancing
|
pub cnt: UpstreamCount, // counter for load balancing
|
||||||
pub opts: HashSet<UpstreamOption>,
|
pub opts: HashSet<UpstreamOption>,
|
||||||
|
|
@ -100,17 +107,17 @@ pub struct Upstream {
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct UpstreamCount(Arc<AtomicUsize>);
|
pub struct UpstreamCount(Arc<AtomicUsize>);
|
||||||
|
|
||||||
impl Upstream {
|
impl UpstreamGroup {
|
||||||
pub fn get(&self) -> Option<&hyper::Uri> {
|
pub fn get(&self) -> Option<&Upstream> {
|
||||||
match self.lb {
|
match self.lb {
|
||||||
LoadBalance::RoundRobin => {
|
LoadBalance::RoundRobin => {
|
||||||
let idx = self.increment_cnt();
|
let idx = self.increment_cnt();
|
||||||
self.uri.get(idx)
|
self.upstream.get(idx)
|
||||||
}
|
}
|
||||||
LoadBalance::Random => {
|
LoadBalance::Random => {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let max = self.uri.len() - 1;
|
let max = self.upstream.len() - 1;
|
||||||
self.uri.get(rng.gen_range(0..max))
|
self.upstream.get(rng.gen_range(0..max))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +127,7 @@ impl Upstream {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn increment_cnt(&self) -> usize {
|
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)
|
self.cnt.0.fetch_add(1, Ordering::Relaxed)
|
||||||
} else {
|
} else {
|
||||||
self.cnt.0.fetch_and(0, Ordering::Relaxed)
|
self.cnt.0.fetch_and(0, Ordering::Relaxed)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use super::toml::{ConfigToml, ReverseProxyOption};
|
use super::toml::{ConfigToml, ReverseProxyOption};
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{Backend, PathNameLC, ReverseProxy, Upstream},
|
backend::{Backend, PathNameLC, ReverseProxy, UpstreamGroup},
|
||||||
backend_opt::UpstreamOption,
|
backend_opt::UpstreamOption,
|
||||||
constants::*,
|
constants::*,
|
||||||
error::*,
|
error::*,
|
||||||
|
|
@ -192,10 +192,20 @@ pub fn parse_opts(globals: &mut Globals) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_reverse_proxy(rp_settings: &[ReverseProxyOption]) -> Result<ReverseProxy> {
|
fn get_reverse_proxy(rp_settings: &[ReverseProxyOption]) -> Result<ReverseProxy> {
|
||||||
let mut upstream: HashMap<PathNameLC, Upstream> = HashMap::default();
|
let mut upstream: HashMap<PathNameLC, UpstreamGroup> = HashMap::default();
|
||||||
rp_settings.iter().for_each(|rpo| {
|
rp_settings.iter().for_each(|rpo| {
|
||||||
let elem = Upstream {
|
let path = match &rpo.path {
|
||||||
uri: rpo.upstream.iter().map(|x| x.to_uri().unwrap()).collect(),
|
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(),
|
cnt: Default::default(),
|
||||||
lb: Default::default(),
|
lb: Default::default(),
|
||||||
opts: {
|
opts: {
|
||||||
|
|
@ -210,11 +220,7 @@ fn get_reverse_proxy(rp_settings: &[ReverseProxyOption]) -> Result<ReverseProxy>
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if rpo.path.is_some() {
|
upstream.insert(path, elem);
|
||||||
upstream.insert(rpo.path.as_ref().unwrap().as_bytes().to_ascii_lowercase(), elem);
|
|
||||||
} else {
|
|
||||||
upstream.insert("/".as_bytes().to_ascii_lowercase(), elem);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
ensure!(
|
ensure!(
|
||||||
rp_settings.iter().filter(|rpo| rpo.path.is_none()).count() < 2,
|
rp_settings.iter().filter(|rpo| rpo.path.is_none()).count() < 2,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::error::*;
|
use crate::{backend::Upstream, error::*};
|
||||||
use rustc_hash::FxHashMap as HashMap;
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
@ -52,6 +52,7 @@ pub struct TlsOption {
|
||||||
#[derive(Deserialize, Debug, Default)]
|
#[derive(Deserialize, Debug, Default)]
|
||||||
pub struct ReverseProxyOption {
|
pub struct ReverseProxyOption {
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
|
pub replace_path: Option<String>,
|
||||||
pub upstream: Vec<UpstreamParams>,
|
pub upstream: Vec<UpstreamParams>,
|
||||||
pub upstream_options: Option<Vec<String>>,
|
pub upstream_options: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +63,7 @@ pub struct UpstreamParams {
|
||||||
pub tls: Option<bool>,
|
pub tls: Option<bool>,
|
||||||
}
|
}
|
||||||
impl UpstreamParams {
|
impl UpstreamParams {
|
||||||
pub fn to_uri(&self) -> Result<hyper::Uri> {
|
pub fn to_upstream(&self) -> Result<Upstream> {
|
||||||
let mut scheme = "http";
|
let mut scheme = "http";
|
||||||
if let Some(t) = self.tls {
|
if let Some(t) = self.tls {
|
||||||
if t {
|
if t {
|
||||||
|
|
@ -70,7 +71,9 @@ impl UpstreamParams {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let location = format!("{}://{}", scheme, self.location);
|
let location = format!("{}://{}", scheme, self.location);
|
||||||
location.parse::<hyper::Uri>().map_err(|e| anyhow!("{}", e))
|
Ok(Upstream {
|
||||||
|
uri: location.parse::<hyper::Uri>().map_err(|e| anyhow!("{}", e))?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Highly motivated by https://github.com/felipenoris/hyper-reverse-proxy
|
// Highly motivated by https://github.com/felipenoris/hyper-reverse-proxy
|
||||||
use super::{utils_headers::*, utils_request::*, utils_response::ResLog, utils_synth_response::*};
|
use super::{utils_headers::*, utils_request::*, utils_response::ResLog, utils_synth_response::*};
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{ServerNameLC, Upstream},
|
backend::{ServerNameLC, UpstreamGroup},
|
||||||
error::*,
|
error::*,
|
||||||
globals::Globals,
|
globals::Globals,
|
||||||
log::*,
|
log::*,
|
||||||
|
|
@ -80,15 +80,9 @@ where
|
||||||
// Find reverse proxy for given path and choose one of upstream host
|
// Find reverse proxy for given path and choose one of upstream host
|
||||||
// Longest prefix match
|
// Longest prefix match
|
||||||
let path = req.uri().path();
|
let path = req.uri().path();
|
||||||
let upstream = if let Some(upstream) = backend.reverse_proxy.get(path) {
|
let upstream_group = match backend.reverse_proxy.get(path) {
|
||||||
upstream
|
Some(ug) => ug,
|
||||||
} else {
|
None => return self.return_with_error_log(StatusCode::NOT_FOUND, &mut log_data),
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Upgrade in request header
|
// Upgrade in request header
|
||||||
|
|
@ -100,9 +94,8 @@ where
|
||||||
&client_addr,
|
&client_addr,
|
||||||
&listen_addr,
|
&listen_addr,
|
||||||
&mut req,
|
&mut req,
|
||||||
upstream_scheme_host,
|
|
||||||
&upgrade_in_request,
|
&upgrade_in_request,
|
||||||
upstream,
|
upstream_group,
|
||||||
tls_enabled,
|
tls_enabled,
|
||||||
) {
|
) {
|
||||||
error!("Failed to generate destination uri for reverse proxy: {}", e);
|
error!("Failed to generate destination uri for reverse proxy: {}", e);
|
||||||
|
|
@ -111,7 +104,7 @@ where
|
||||||
// debug!("Request to be forwarded: {:?}", req_forwarded);
|
// debug!("Request to be forwarded: {:?}", req_forwarded);
|
||||||
req.log_debug(&client_addr, Some("(to Backend)"));
|
req.log_debug(&client_addr, Some("(to Backend)"));
|
||||||
log_data.xff(&req.headers().get("x-forwarded-for"));
|
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
|
// Forward request to
|
||||||
|
|
@ -223,9 +216,8 @@ where
|
||||||
client_addr: &SocketAddr,
|
client_addr: &SocketAddr,
|
||||||
listen_addr: &SocketAddr,
|
listen_addr: &SocketAddr,
|
||||||
req: &mut Request<B>,
|
req: &mut Request<B>,
|
||||||
upstream_scheme_host: &Uri,
|
|
||||||
upgrade: &Option<String>,
|
upgrade: &Option<String>,
|
||||||
upstream: &Upstream,
|
upstream_group: &UpstreamGroup,
|
||||||
tls_enabled: bool,
|
tls_enabled: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
debug!("Generate request to be forwarded");
|
debug!("Generate request to be forwarded");
|
||||||
|
|
@ -263,21 +255,37 @@ where
|
||||||
.insert(header::HOST, HeaderValue::from_str(&org_host)?);
|
.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
|
// apply upstream-specific headers given in upstream_option
|
||||||
let headers = req.headers_mut();
|
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
|
// 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()
|
let new_uri = Uri::builder()
|
||||||
.scheme(upstream_scheme_host.scheme().unwrap().as_str())
|
.scheme(upstream_chosen.uri.scheme().unwrap().as_str())
|
||||||
.authority(upstream_scheme_host.authority().unwrap().as_str());
|
.authority(upstream_chosen.uri.authority().unwrap().as_str());
|
||||||
let pq = req.uri().path_and_query();
|
let org_pq = match req.uri().path_and_query() {
|
||||||
*req.uri_mut() = match pq {
|
Some(pq) => pq.to_string(),
|
||||||
None => new_uri,
|
None => "/".to_string(),
|
||||||
Some(x) => new_uri.path_and_query(x.to_owned()),
|
|
||||||
}
|
}
|
||||||
.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::<u8>::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
|
// upgrade
|
||||||
if let Some(v) = upgrade {
|
if let Some(v) = upgrade {
|
||||||
|
|
@ -288,7 +296,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change version to http/1.1 when destination scheme is http
|
// 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;
|
*req.version_mut() = Version::HTTP_11;
|
||||||
} else if req.version() == Version::HTTP_3 {
|
} else if req.version() == Version::HTTP_3 {
|
||||||
debug!("HTTP/3 is currently unsupported for request to upstream. Use HTTP/2.");
|
debug!("HTTP/3 is currently unsupported for request to upstream. Use HTTP/2.");
|
||||||
|
|
|
||||||
|
|
@ -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 bytes::BufMut;
|
||||||
use hyper::{
|
use hyper::{
|
||||||
header::{self, HeaderMap, HeaderName, HeaderValue},
|
header::{self, HeaderMap, HeaderName, HeaderValue},
|
||||||
|
|
@ -12,14 +12,14 @@ use std::net::SocketAddr;
|
||||||
pub(super) fn apply_upstream_options_to_header(
|
pub(super) fn apply_upstream_options_to_header(
|
||||||
headers: &mut HeaderMap,
|
headers: &mut HeaderMap,
|
||||||
_client_addr: &SocketAddr,
|
_client_addr: &SocketAddr,
|
||||||
upstream_scheme_host: &Uri,
|
upstream: &UpstreamGroup,
|
||||||
upstream: &Upstream,
|
upstream_base_uri: &Uri,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for opt in upstream.opts.iter() {
|
for opt in upstream.opts.iter() {
|
||||||
match opt {
|
match opt {
|
||||||
UpstreamOption::OverrideHost => {
|
UpstreamOption::OverrideHost => {
|
||||||
// overwrite HOST value with upstream hostname (like 192.168.xx.x seen from rpxy)
|
// 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
|
headers
|
||||||
.insert(header::HOST, HeaderValue::from_str(upstream_host)?)
|
.insert(header::HOST, HeaderValue::from_str(upstream_host)?)
|
||||||
.ok_or_else(|| anyhow!("none"))?;
|
.ok_or_else(|| anyhow!("none"))?;
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ impl MessageLog {
|
||||||
self.xff = xff.map_or_else(|| "", |v| v.to_str().unwrap_or("")).to_string();
|
self.xff = xff.map_or_else(|| "", |v| v.to_str().unwrap_or("")).to_string();
|
||||||
self
|
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.upstream = upstream.to_string();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue