v0.25.0
This commit is contained in:
1 parent
2e4aa4df4d
commit
87e571c170
106 files changed
+17952
-6185
No files matched your search
+247
-24
@@ -60,6 +60,8 @@ pub struct Config {
|
||||
#[serde(default, skip_serializing_if = "crate::tls::Settings::is_empty")]
|
||||
pub tls: crate::tls::Settings,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub web_services: Vec<crate::web_server::Service>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub vhosts: Vec<crate::vhost::Vhost>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub host_policies: Vec<HostPolicy>,
|
||||
@@ -169,6 +171,12 @@ pub struct CompiledBackend {
|
||||
/// directly. Used by the guarded control-plane reverse-proxy path.
|
||||
pub targets_admin_listener: bool,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompiledSource {
|
||||
ReverseProxy,
|
||||
WebServer(Arc<crate::web_server::CompiledService>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompiledRoute {
|
||||
pub backend_id: Option<String>,
|
||||
@@ -182,7 +190,8 @@ pub struct CompiledRoute {
|
||||
pub path_regex: Option<regex::Regex>,
|
||||
pub rewrite: crate::vhost::PathRewrite,
|
||||
pub response: Option<crate::vhost::LocationResponse>,
|
||||
pub backend: Arc<CompiledBackend>,
|
||||
pub source: CompiledSource,
|
||||
pub backend: Option<Arc<CompiledBackend>>,
|
||||
pub acl: Option<CompiledAcl>,
|
||||
pub headers: CompiledHeaders,
|
||||
}
|
||||
@@ -280,8 +289,7 @@ impl Snapshot {
|
||||
&& frontend.websocket_rfc8441
|
||||
&& frontend.http2_enabled
|
||||
&& frontend.bindings.iter().any(|binding| {
|
||||
binding.matches(local, tls)
|
||||
&& if tls { binding.http2 } else { binding.h2c }
|
||||
binding.matches(local, tls) && if tls { binding.http2 } else { binding.h2c }
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -854,11 +862,47 @@ pub fn compile_with_active(
|
||||
config.compression.validate()?;
|
||||
config.server_header.validate()?;
|
||||
let forwarding = config.forwarding.compile()?;
|
||||
ensure!(
|
||||
config.web_services.len() <= 128,
|
||||
"too many Web Services (max 128)"
|
||||
);
|
||||
let mut web_services = HashMap::new();
|
||||
for service in &mut config.web_services {
|
||||
service
|
||||
.validate()
|
||||
.with_context(|| format!("Web Service {}", service.id))?;
|
||||
let compiled = Arc::new(
|
||||
service
|
||||
.compile()
|
||||
.with_context(|| format!("Web Service {}", service.id))?,
|
||||
);
|
||||
ensure!(
|
||||
web_services.insert(service.id.clone(), compiled).is_none(),
|
||||
"duplicate Web Service id"
|
||||
);
|
||||
}
|
||||
ensure!(config.vhosts.len() <= 64, "too many vhosts (max 64)");
|
||||
let mut vhost_ids = HashSet::new();
|
||||
for host in &mut config.vhosts {
|
||||
host.validate()
|
||||
.with_context(|| format!("vhost {}", host.id))?;
|
||||
let validate_source = |source: &crate::vhost::Source| -> Result<()> {
|
||||
if let crate::vhost::Source::WebServer { web_service_id } = source {
|
||||
ensure!(
|
||||
web_services.contains_key(web_service_id),
|
||||
"vhost {} references unknown Web Service {}",
|
||||
host.id,
|
||||
web_service_id
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
validate_source(&host.source)?;
|
||||
for route in &host.routes {
|
||||
if route.response.is_none() {
|
||||
validate_source(&route.source)?;
|
||||
}
|
||||
}
|
||||
ensure!(vhost_ids.insert(host.id.clone()), "duplicate vhost id");
|
||||
}
|
||||
ensure!(
|
||||
@@ -1183,16 +1227,20 @@ pub fn compile_with_active(
|
||||
backend.id = format!("{}/{}", host.id, definition.id);
|
||||
compiled_members.push(Arc::new(backend));
|
||||
}
|
||||
let mut pool = crate::balance::Pool::new(
|
||||
host.id.clone(),
|
||||
host.load_balancing.clone(),
|
||||
&host.backends,
|
||||
compiled_members,
|
||||
)?;
|
||||
if let Some(old) = previous.and_then(|s| s.pools.iter().find(|p| p.id == host.id)) {
|
||||
pool.reuse_observations(old);
|
||||
}
|
||||
let pool = Arc::new(pool);
|
||||
let pool = if compiled_members.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut pool = crate::balance::Pool::new(
|
||||
host.id.clone(),
|
||||
host.load_balancing.clone(),
|
||||
&host.backends,
|
||||
compiled_members,
|
||||
)?;
|
||||
if let Some(old) = previous.and_then(|s| s.pools.iter().find(|p| p.id == host.id)) {
|
||||
pool.reuse_observations(old);
|
||||
}
|
||||
Some(Arc::new(pool))
|
||||
};
|
||||
let metrics = previous
|
||||
.and_then(|s| s.vhost_metrics.get(&host.id))
|
||||
.cloned()
|
||||
@@ -1270,11 +1318,31 @@ pub fn compile_with_active(
|
||||
proxy_timeouts: host.proxy_timeouts.clone(),
|
||||
});
|
||||
if host.enabled {
|
||||
let (fallback_source, fallback_pool, fallback_backend) = match &host.source {
|
||||
crate::vhost::Source::ReverseProxy => {
|
||||
let pool = pool.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"vhost {} Reverse Proxy source has no backend pool",
|
||||
host.id
|
||||
)
|
||||
})?;
|
||||
(
|
||||
CompiledSource::ReverseProxy,
|
||||
Some(pool.clone()),
|
||||
Some(pool.members[0].backend.clone()),
|
||||
)
|
||||
}
|
||||
crate::vhost::Source::WebServer { web_service_id } => (
|
||||
CompiledSource::WebServer(web_services[web_service_id].clone()),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
};
|
||||
let fallback = CompiledRoute {
|
||||
backend_id: None,
|
||||
bindings: frontend.bindings.clone(),
|
||||
frontend: Some(frontend.clone()),
|
||||
pool: Some(pool.clone()),
|
||||
pool: fallback_pool,
|
||||
cache: None,
|
||||
id: format!("{}/main", host.id),
|
||||
path_prefix: "/".into(),
|
||||
@@ -1282,7 +1350,8 @@ pub fn compile_with_active(
|
||||
path_regex: None,
|
||||
rewrite: crate::vhost::PathRewrite::default(),
|
||||
response: None,
|
||||
backend: pool.members[0].backend.clone(),
|
||||
source: fallback_source,
|
||||
backend: fallback_backend,
|
||||
acl: None,
|
||||
headers: crate::headers::CompiledHeaders::default(),
|
||||
};
|
||||
@@ -1341,19 +1410,56 @@ pub fn compile_with_active(
|
||||
validate_location_response(route.response.as_ref())?;
|
||||
let acl = route.acl.as_ref().map(Acl::compile).transpose()?;
|
||||
let headers = route.headers.compile()?;
|
||||
if let Some(id) = &route.backend_id {
|
||||
ensure!(
|
||||
host.backends.iter().any(|b| &b.id == id),
|
||||
"vhost path references unknown backend {}",
|
||||
id
|
||||
);
|
||||
if route.source.is_reverse_proxy() {
|
||||
if let Some(id) = &route.backend_id {
|
||||
ensure!(
|
||||
host.backends.iter().any(|b| &b.id == id),
|
||||
"vhost path references unknown backend {}",
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
if host.enabled && route.enabled {
|
||||
let (source, route_pool, backend) = if route.response.is_some() {
|
||||
// Local return is handled before source dispatch and needs no origin.
|
||||
(CompiledSource::ReverseProxy, None, None)
|
||||
} else {
|
||||
match &route.source {
|
||||
crate::vhost::Source::ReverseProxy => {
|
||||
let pool = pool.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"vhost {} location {} Reverse Proxy source has no backend pool",
|
||||
host.id,
|
||||
route.id
|
||||
)
|
||||
})?;
|
||||
let backend = if let Some(id) = route.backend_id.as_deref() {
|
||||
pool.members
|
||||
.iter()
|
||||
.find(|member| member.definition.id == id)
|
||||
.map(|member| member.backend.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("unknown backend {id}"))?
|
||||
} else {
|
||||
pool.members[0].backend.clone()
|
||||
};
|
||||
(
|
||||
CompiledSource::ReverseProxy,
|
||||
Some(pool.clone()),
|
||||
Some(backend),
|
||||
)
|
||||
}
|
||||
crate::vhost::Source::WebServer { web_service_id } => (
|
||||
CompiledSource::WebServer(web_services[web_service_id].clone()),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
}
|
||||
};
|
||||
let compiled = CompiledRoute {
|
||||
backend_id: route.backend_id.clone(),
|
||||
bindings: frontend.bindings.clone(),
|
||||
frontend: Some(frontend.clone()),
|
||||
pool: Some(pool.clone()),
|
||||
pool: route_pool,
|
||||
cache: route.cache,
|
||||
id: format!("{}/{}", host.id, route.id),
|
||||
path_prefix: route.path_prefix.clone(),
|
||||
@@ -1361,7 +1467,8 @@ pub fn compile_with_active(
|
||||
path_regex,
|
||||
rewrite: route.rewrite.clone(),
|
||||
response: route.response.clone(),
|
||||
backend: pool.members[0].backend.clone(),
|
||||
source,
|
||||
backend,
|
||||
acl,
|
||||
headers,
|
||||
};
|
||||
@@ -1380,7 +1487,9 @@ pub fn compile_with_active(
|
||||
.or_default()
|
||||
.push(frontend.clone());
|
||||
}
|
||||
pools.push(pool);
|
||||
if let Some(pool) = pool {
|
||||
pools.push(pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
let quic_tls = tls
|
||||
@@ -1537,6 +1646,120 @@ mod tests {
|
||||
let second = compile(serde_json::from_str(&saved).unwrap(), 1, &policy()).unwrap();
|
||||
assert!(second.select("localhost", "/caf\u{e9}/hello").is_some());
|
||||
}
|
||||
#[test]
|
||||
fn web_server_main_source_needs_no_backend_pool() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("index.html"), b"hello").unwrap();
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.web_services.push(crate::web_server::Service {
|
||||
id: "site".into(),
|
||||
name: "Site".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
index: vec!["index.html".into()],
|
||||
directory_listing: false,
|
||||
..Default::default()
|
||||
});
|
||||
config.vhosts[0].source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "site".into(),
|
||||
};
|
||||
config.vhosts[0].backends.clear();
|
||||
config.vhosts[0].routes.clear();
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let route = snapshot.select("localhost", "/").unwrap();
|
||||
assert!(matches!(&route.source, CompiledSource::WebServer(_)));
|
||||
assert!(route.pool.is_none());
|
||||
assert!(route.backend.is_none());
|
||||
assert!(snapshot.pools.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_server_and_reverse_proxy_can_share_one_vhost() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("index.html"), b"hello").unwrap();
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.web_services.push(crate::web_server::Service {
|
||||
id: "site".into(),
|
||||
name: "Site".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
index: vec!["index.html".into()],
|
||||
directory_listing: false,
|
||||
..Default::default()
|
||||
});
|
||||
let host = &mut config.vhosts[0];
|
||||
host.source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "site".into(),
|
||||
};
|
||||
host.routes = vec![crate::vhost::PathRoute {
|
||||
id: "api".into(),
|
||||
path_prefix: "/api".into(),
|
||||
source: crate::vhost::Source::ReverseProxy,
|
||||
..Default::default()
|
||||
}];
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let root = snapshot.select("localhost", "/").unwrap();
|
||||
let api = snapshot.select("localhost", "/api/test").unwrap();
|
||||
assert!(matches!(&root.source, CompiledSource::WebServer(_)));
|
||||
assert!(matches!(&api.source, CompiledSource::ReverseProxy));
|
||||
assert!(api.pool.is_some());
|
||||
assert!(api.backend.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_web_service_can_be_reused_by_multiple_vhosts() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("index.html"), b"shared").unwrap();
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.web_services.push(crate::web_server::Service {
|
||||
id: "shared-site".into(),
|
||||
name: "Shared site".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
index: vec!["index.html".into()],
|
||||
directory_listing: false,
|
||||
..Default::default()
|
||||
});
|
||||
let first = &mut config.vhosts[0];
|
||||
first.source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "shared-site".into(),
|
||||
};
|
||||
first.backends.clear();
|
||||
first.routes.clear();
|
||||
|
||||
let mut second = first.clone();
|
||||
second.id = "shared-second".into();
|
||||
second.frontend.hosts = vec!["second.localhost".into()];
|
||||
config.vhosts.push(second);
|
||||
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let first_route = snapshot.select("localhost", "/").unwrap();
|
||||
let second_route = snapshot.select("second.localhost", "/").unwrap();
|
||||
assert!(
|
||||
matches!(&first_route.source, CompiledSource::WebServer(service) if service.id == "shared-site")
|
||||
);
|
||||
assert!(
|
||||
matches!(&second_route.source, CompiledSource::WebServer(service) if service.id == "shared-site")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_web_service_reference_is_rejected() {
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.vhosts[0].source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "missing".into(),
|
||||
};
|
||||
let error = compile(config, 1, &policy()).unwrap_err().to_string();
|
||||
assert!(error.contains("unknown Web Service"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_source_fields_remain_reverse_proxy_for_old_configs() {
|
||||
let source: crate::vhost::Source =
|
||||
serde_json::from_value(serde_json::json!({"type":"reverse_proxy"})).unwrap();
|
||||
assert_eq!(source, crate::vhost::Source::ReverseProxy);
|
||||
let route: crate::vhost::PathRoute =
|
||||
serde_json::from_value(serde_json::json!({"id":"legacy"})).unwrap();
|
||||
assert_eq!(route.source, crate::vhost::Source::ReverseProxy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_normalization() {
|
||||
assert_eq!(request_host("EXAMPLE.com.:8080").unwrap(), "example.com");
|
||||
|
||||
Reference in new issue
Block a user