From 50008c388e0c99036f71576bf547852eebb1cc1e Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Thu, 12 Dec 2024 11:41:48 +0100 Subject: [PATCH 01/18] URL rewrite and redirections Signed-off-by: Eloi DEMOLIS --- bin/src/ctl/request_builder.rs | 8 + command/src/command.proto | 18 + command/src/config.rs | 26 +- command/src/request.rs | 14 +- command/src/response.rs | 14 +- lib/src/http.rs | 38 ++- lib/src/https.rs | 28 +- lib/src/lib.rs | 8 +- lib/src/protocol/kawa_h1/editor.rs | 2 +- lib/src/protocol/kawa_h1/mod.rs | 70 ++-- lib/src/router/mod.rs | 524 ++++++++++++++++++++++++----- lib/src/router/pattern_trie.rs | 42 ++- 12 files changed, 632 insertions(+), 160 deletions(-) diff --git a/bin/src/ctl/request_builder.rs b/bin/src/ctl/request_builder.rs index eb07929d7..fc04667fb 100644 --- a/bin/src/ctl/request_builder.rs +++ b/bin/src/ctl/request_builder.rs @@ -250,6 +250,10 @@ impl CommandManager { Some(tags) => tags, None => BTreeMap::new(), }, + redirect: todo!(), + redirect_scheme: todo!(), + host_rewrite: todo!(), + path_rewrite: todo!(), }) .into(), ), @@ -298,6 +302,10 @@ impl CommandManager { Some(tags) => tags, None => BTreeMap::new(), }, + redirect: todo!(), + redirect_scheme: todo!(), + host_rewrite: todo!(), + path_rewrite: todo!(), }) .into(), ), diff --git a/command/src/command.proto b/command/src/command.proto index ad93b9e34..c3836b1b5 100644 --- a/command/src/command.proto +++ b/command/src/command.proto @@ -237,6 +237,20 @@ message ListenersList { map tcp_listeners = 3; } +enum RedirectPolicy { + FORWARD = 0; + FORCE_HTTPS = 1; + TEMPORARY = 2; + PERMANENT = 3; + UNAUTHORIZED = 4; +} + +enum RedirectScheme { + USE_SAME = 0; + USE_HTTP = 1; + USE_HTTPS = 2; +} + // An HTTP or HTTPS frontend, as order to, or received from, Sōzu message RequestHttpFrontend { optional string cluster_id = 1; @@ -247,6 +261,10 @@ message RequestHttpFrontend { required RulePosition position = 6 [default = TREE]; // custom tags to identify the frontend in the access logs map tags = 7; + optional RedirectPolicy redirect = 8; + optional RedirectScheme redirect_scheme = 9; + optional string host_rewrite = 10; + optional string path_rewrite = 11; } message RequestTcpFrontend { diff --git a/command/src/config.rs b/command/src/config.rs index e148cc1d0..dd5709fb5 100644 --- a/command/src/config.rs +++ b/command/src/config.rs @@ -64,9 +64,9 @@ use crate::{ request::RequestType, ActivateListener, AddBackend, AddCertificate, CertificateAndKey, Cluster, CustomHttpAnswers, HttpListenerConfig, HttpsListenerConfig, ListenerType, LoadBalancingAlgorithms, LoadBalancingParams, LoadMetric, MetricsConfiguration, PathRule, - ProtobufAccessLogFormat, ProxyProtocolConfig, Request, RequestHttpFrontend, - RequestTcpFrontend, RulePosition, ServerConfig, ServerMetricsConfig, SocketAddress, - TcpListenerConfig, TlsVersion, WorkerRequest, + ProtobufAccessLogFormat, ProxyProtocolConfig, RedirectPolicy, RedirectScheme, Request, + RequestHttpFrontend, RequestTcpFrontend, RulePosition, ServerConfig, ServerMetricsConfig, + SocketAddress, TcpListenerConfig, TlsVersion, WorkerRequest, }, ObjectKind, }; @@ -667,6 +667,10 @@ pub struct FileClusterFrontendConfig { #[serde(default)] pub position: RulePosition, pub tags: Option>, + pub redirect: Option, + pub redirect_scheme: Option, + pub host_rewrite: Option, + pub path_rewrite: Option, } impl FileClusterFrontendConfig { @@ -752,6 +756,10 @@ impl FileClusterFrontendConfig { path, method: self.method.clone(), tags: self.tags.clone(), + redirect: self.redirect, + redirect_scheme: self.redirect_scheme, + host_rewrite: self.host_rewrite.clone(), + path_rewrite: self.path_rewrite.clone(), }) } } @@ -902,6 +910,10 @@ pub struct HttpFrontendConfig { #[serde(default)] pub position: RulePosition, pub tags: Option>, + pub redirect: Option, + pub redirect_scheme: Option, + pub host_rewrite: Option, + pub path_rewrite: Option, } impl HttpFrontendConfig { @@ -939,6 +951,10 @@ impl HttpFrontendConfig { method: self.method.clone(), position: self.position.into(), tags, + redirect: self.redirect.map(Into::into), + redirect_scheme: self.redirect_scheme.map(Into::into), + host_rewrite: self.host_rewrite.clone(), + path_rewrite: self.path_rewrite.clone(), }) .into(), ); @@ -953,6 +969,10 @@ impl HttpFrontendConfig { method: self.method.clone(), position: self.position.into(), tags, + redirect: self.redirect.map(Into::into), + redirect_scheme: self.redirect_scheme.map(Into::into), + host_rewrite: self.host_rewrite.clone(), + path_rewrite: self.path_rewrite.clone(), }) .into(), ); diff --git a/command/src/request.rs b/command/src/request.rs index f43e7c621..ba5ce6413 100644 --- a/command/src/request.rs +++ b/command/src/request.rs @@ -14,8 +14,7 @@ use crate::{ proto::{ command::{ ip_address, request::RequestType, InitialState, IpAddress, LoadBalancingAlgorithms, - PathRuleKind, Request, RequestHttpFrontend, RulePosition, SocketAddress, Uint128, - WorkerRequest, + PathRuleKind, Request, RequestHttpFrontend, SocketAddress, Uint128, WorkerRequest, }, display::format_request_type, }, @@ -161,18 +160,17 @@ impl RequestHttpFrontend { /// convert a requested frontend to a usable one by parsing its address pub fn to_frontend(self) -> Result { Ok(HttpFrontend { + position: self.position(), + redirect: self.redirect(), + redirect_scheme: self.redirect_scheme(), address: self.address.into(), cluster_id: self.cluster_id, hostname: self.hostname, path: self.path, method: self.method, - position: RulePosition::try_from(self.position).map_err(|_| { - RequestError::InvalidValue { - name: "position".to_string(), - value: self.position, - } - })?, tags: Some(self.tags), + host_rewrite: self.host_rewrite, + path_rewrite: self.path_rewrite, }) } } diff --git a/command/src/response.rs b/command/src/response.rs index c399b657a..8c0a81225 100644 --- a/command/src/response.rs +++ b/command/src/response.rs @@ -2,9 +2,7 @@ use std::{cmp::Ordering, collections::BTreeMap, fmt, net::SocketAddr}; use crate::{ proto::command::{ - AddBackend, FilteredTimeSerie, LoadBalancingParams, PathRule, PathRuleKind, - RequestHttpFrontend, RequestTcpFrontend, Response, ResponseContent, ResponseStatus, - RulePosition, RunState, WorkerResponse, + AddBackend, FilteredTimeSerie, LoadBalancingParams, PathRule, PathRuleKind, RedirectPolicy, RedirectScheme, RequestHttpFrontend, RequestTcpFrontend, Response, ResponseContent, ResponseStatus, RulePosition, RunState, WorkerResponse }, state::ClusterId, }; @@ -38,6 +36,12 @@ pub struct HttpFrontend { pub method: Option, #[serde(default)] pub position: RulePosition, + pub redirect: RedirectPolicy, + pub redirect_scheme: RedirectScheme, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_rewrite: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_rewrite: Option, pub tags: Option>, } @@ -51,6 +55,10 @@ impl From for RequestHttpFrontend { method: val.method, position: val.position.into(), tags: val.tags.unwrap_or_default(), + redirect: Some(val.redirect.into()), + redirect_scheme: Some(val.redirect_scheme.into()), + host_rewrite: val.host_rewrite, + path_rewrite: val.path_rewrite, } } } diff --git a/lib/src/http.rs b/lib/src/http.rs index 2f6e69e39..cc6c48176 100644 --- a/lib/src/http.rs +++ b/lib/src/http.rs @@ -39,7 +39,7 @@ use crate::{ proxy_protocol::expect::ExpectProxyProtocol, Http, Pipe, SessionState, }, - router::{Route, Router}, + router::{Route, RouteResult, Router}, server::{ListenToken, SessionManager}, socket::server_bind, timer::TimeoutContainer, @@ -436,7 +436,7 @@ impl L7ListenerHandler for HttpListener { host: &str, uri: &str, method: &Method, - ) -> Result { + ) -> Result { let start = Instant::now(); let (remaining_input, (hostname, _)) = match hostname_and_port(host.as_bytes()) { Ok(tuple) => tuple, @@ -472,8 +472,12 @@ impl L7ListenerHandler for HttpListener { let now = Instant::now(); - if let Route::ClusterId(cluster) = &route { - time!("frontend_matching_time", cluster, (now - start).as_millis()); + if let RouteResult::Cluster { cluster_id, .. } = &route { + time!( + "frontend_matching_time", + cluster_id, + (now - start).as_millis() + ); } Ok(route) @@ -1052,7 +1056,7 @@ mod tests { use super::testing::start_http_worker; use super::*; - use sozu_command::proto::command::{CustomHttpAnswers, SocketAddress}; + use sozu_command::proto::command::{CustomHttpAnswers, RedirectPolicy, RedirectScheme, SocketAddress}; use crate::sozu_command::{ channel::Channel, @@ -1325,6 +1329,10 @@ mod tests { path: PathRule::prefix(uri1), position: RulePosition::Tree, cluster_id: Some(cluster_id1), + redirect: RedirectPolicy::Forward, + redirect_scheme: RedirectScheme::UseSame, + host_rewrite: None, + path_rewrite: None, tags: None, }) .expect("Could not add http frontend"); @@ -1336,6 +1344,10 @@ mod tests { path: PathRule::prefix(uri2), position: RulePosition::Tree, cluster_id: Some(cluster_id2), + redirect: RedirectPolicy::Forward, + redirect_scheme: RedirectScheme::UseSame, + host_rewrite: None, + path_rewrite: None, tags: None, }) .expect("Could not add http frontend"); @@ -1347,6 +1359,10 @@ mod tests { path: PathRule::prefix(uri3), position: RulePosition::Tree, cluster_id: Some(cluster_id3), + redirect: RedirectPolicy::Forward, + redirect_scheme: RedirectScheme::UseSame, + host_rewrite: None, + path_rewrite: None, tags: None, }) .expect("Could not add http frontend"); @@ -1358,6 +1374,10 @@ mod tests { path: PathRule::prefix("/test".to_owned()), position: RulePosition::Tree, cluster_id: Some("cluster_1".to_owned()), + redirect: RedirectPolicy::Forward, + redirect_scheme: RedirectScheme::UseSame, + host_rewrite: None, + path_rewrite: None, tags: None, }) .expect("Could not add http frontend"); @@ -1388,19 +1408,19 @@ mod tests { let frontend5 = listener.frontend_from_request("domain", "/", &Method::Get); assert_eq!( frontend1.expect("should find frontend"), - Route::ClusterId("cluster_1".to_string()) + RouteResult::simple("cluster_1".to_string()) ); assert_eq!( frontend2.expect("should find frontend"), - Route::ClusterId("cluster_1".to_string()) + RouteResult::simple("cluster_1".to_string()) ); assert_eq!( frontend3.expect("should find frontend"), - Route::ClusterId("cluster_2".to_string()) + RouteResult::simple("cluster_2".to_string()) ); assert_eq!( frontend4.expect("should find frontend"), - Route::ClusterId("cluster_3".to_string()) + RouteResult::simple("cluster_3".to_string()) ); assert!(frontend5.is_err()); } diff --git a/lib/src/https.rs b/lib/src/https.rs index ce03d33bd..92e8d3b62 100644 --- a/lib/src/https.rs +++ b/lib/src/https.rs @@ -62,7 +62,7 @@ use crate::{ rustls::TlsHandshake, Http, Pipe, SessionState, }, - router::{Route, Router}, + router::{Route, RouteResult, Router}, server::{ListenToken, SessionManager}, socket::{server_bind, FrontRustls}, timer::TimeoutContainer, @@ -569,7 +569,7 @@ impl L7ListenerHandler for HttpsListener { host: &str, uri: &str, method: &Method, - ) -> Result { + ) -> Result { let start = Instant::now(); let (remaining_input, (hostname, _)) = match hostname_and_port(host.as_bytes()) { Ok(tuple) => tuple, @@ -600,8 +600,12 @@ impl L7ListenerHandler for HttpsListener { let now = Instant::now(); - if let Route::ClusterId(cluster) = &route { - time!("frontend_matching_time", cluster, (now - start).as_millis()); + if let RouteResult::Cluster { cluster_id, .. } = &route { + time!( + "frontend_matching_time", + cluster_id, + (now - start).as_millis() + ); } Ok(route) @@ -1542,25 +1546,25 @@ mod tests { "lolcatho.st".as_bytes(), &PathRule::Prefix(uri1), &MethodRule::new(None), - &Route::ClusterId(cluster_id1.clone()) + &Route::simple(cluster_id1.clone()) )); assert!(fronts.add_tree_rule( "lolcatho.st".as_bytes(), &PathRule::Prefix(uri2), &MethodRule::new(None), - &Route::ClusterId(cluster_id2) + &Route::simple(cluster_id2) )); assert!(fronts.add_tree_rule( "lolcatho.st".as_bytes(), &PathRule::Prefix(uri3), &MethodRule::new(None), - &Route::ClusterId(cluster_id3) + &Route::simple(cluster_id3) )); assert!(fronts.add_tree_rule( "other.domain".as_bytes(), &PathRule::Prefix("test".to_string()), &MethodRule::new(None), - &Route::ClusterId(cluster_id1) + &Route::simple(cluster_id1) )); let address = SocketAddress::new_v4(127, 0, 0, 1, 1032); @@ -1601,25 +1605,25 @@ mod tests { let frontend1 = listener.frontend_from_request("lolcatho.st", "/", &Method::Get); assert_eq!( frontend1.expect("should find a frontend"), - Route::ClusterId("cluster_1".to_string()) + RouteResult::simple("cluster_1".to_string()) ); println!("TEST {}", line!()); let frontend2 = listener.frontend_from_request("lolcatho.st", "/test", &Method::Get); assert_eq!( frontend2.expect("should find a frontend"), - Route::ClusterId("cluster_1".to_string()) + RouteResult::simple("cluster_1".to_string()) ); println!("TEST {}", line!()); let frontend3 = listener.frontend_from_request("lolcatho.st", "/yolo/test", &Method::Get); assert_eq!( frontend3.expect("should find a frontend"), - Route::ClusterId("cluster_2".to_string()) + RouteResult::simple("cluster_2".to_string()) ); println!("TEST {}", line!()); let frontend4 = listener.frontend_from_request("lolcatho.st", "/yolo/swag", &Method::Get); assert_eq!( frontend4.expect("should find a frontend"), - Route::ClusterId("cluster_3".to_string()) + RouteResult::simple("cluster_3".to_string()) ); println!("TEST {}", line!()); let frontend5 = listener.frontend_from_request("domain", "/", &Method::Get); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 0c2bca139..f00c681c5 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -350,13 +350,13 @@ use backends::BackendError; use hex::FromHexError; use mio::{net::TcpStream, Interest, Token}; use protocol::http::{answers::TemplateError, parser::Method}; -use router::RouterError; +use router::{RouteResult, RouterError}; use socket::ServerBindError; use tls::CertificateResolverError; use sozu_command::{ logging::{CachedTags, LogContext}, - proto::command::{Cluster, ListenerType, RequestHttpFrontend, WorkerRequest, WorkerResponse}, + proto::command::{Cluster, ListenerType, RedirectPolicy, RequestHttpFrontend, WorkerRequest, WorkerResponse}, ready::Ready, state::ClusterId, AsStr, ObjectKind, @@ -554,7 +554,7 @@ pub trait L7ListenerHandler { host: &str, uri: &str, method: &Method, - ) -> Result; + ) -> Result; } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -602,6 +602,8 @@ pub enum RetrieveClusterError { NoPath, #[error("unauthorized route")] UnauthorizedRoute, + #[error("redirected {0:?}")] + Redirected(RedirectPolicy), #[error("{0}")] RetrieveFrontend(FrontendFromRequestError), } diff --git a/lib/src/protocol/kawa_h1/editor.rs b/lib/src/protocol/kawa_h1/editor.rs index 26a8d3658..e7e870c5f 100644 --- a/lib/src/protocol/kawa_h1/editor.rs +++ b/lib/src/protocol/kawa_h1/editor.rs @@ -117,7 +117,7 @@ impl HttpContext { let key = cookie.key.data(buf); if key == self.sticky_name.as_bytes() { let val = cookie.val.data(buf); - self.sticky_session_found = from_utf8(val).ok().map(|val| val.to_string()); + self.sticky_session_found = from_utf8(val).ok().map(ToOwned::to_owned); cookie.elide(); } } diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs index 270ca7e7e..fd1a040bd 100644 --- a/lib/src/protocol/kawa_h1/mod.rs +++ b/lib/src/protocol/kawa_h1/mod.rs @@ -16,7 +16,7 @@ use rusty_ulid::Ulid; use sozu_command::{ config::MAX_LOOP_ITERATIONS, logging::EndpointRecord, - proto::command::{Event, EventKind, ListenerType}, + proto::command::{Event, EventKind, ListenerType, RedirectPolicy, RedirectScheme}, }; // use time::{Duration, Instant}; @@ -34,7 +34,7 @@ use crate::{ SessionState, }, retry::RetryPolicy, - router::Route, + router::{Route, RouteResult}, server::{push_event, CONN_RETRIES}, socket::{stats::socket_rtt, SocketHandler, SocketResult, TransportProtocol}, sozu_command::{logging::LogContext, ready::Ready}, @@ -1279,30 +1279,54 @@ impl Http cluster_id, - Route::Deny => { + match route { + RouteResult::Deny + | RouteResult::Cluster { + redirect: RedirectPolicy::Unauthorized, + .. + } => { self.set_answer(DefaultAnswer::Answer401 {}); - return Err(RetrieveClusterError::UnauthorizedRoute); + Err(RetrieveClusterError::UnauthorizedRoute) + } + RouteResult::Cluster { + cluster_id, + redirect, + redirect_scheme, + rewritten_host, + rewritten_path, + } => { + let host = rewritten_host.as_deref().unwrap_or(host); + let path = rewritten_path.as_deref().unwrap_or(uri); + let is_https = matches!(proxy.borrow().kind(), ListenerType::Https); + match (redirect, is_https) { + (RedirectPolicy::Forward, _) | (RedirectPolicy::ForceHttps, true) => { + Ok(cluster_id) + } + (RedirectPolicy::ForceHttps, false) => { + self.set_answer(DefaultAnswer::Answer301 { + location: format!("https://{host}{path}"), + }); + Err(RetrieveClusterError::Redirected(redirect)) + } + (RedirectPolicy::Permanent, _) => { + let proto = match (redirect_scheme, is_https) { + (RedirectScheme::UseHttp, _) | (RedirectScheme::UseSame, false) => { + "http" + } + (RedirectScheme::UseHttps, _) | (RedirectScheme::UseSame, true) => { + "https" + } + }; + self.set_answer(DefaultAnswer::Answer301 { + location: format!("{proto}://{host}{path}"), + }); + Err(RetrieveClusterError::Redirected(redirect)) + } + (RedirectPolicy::Temporary, _) => todo!(), + (RedirectPolicy::Unauthorized, _) => unreachable!(), + } } - }; - - let frontend_should_redirect_https = matches!(proxy.borrow().kind(), ListenerType::Http) - && proxy - .borrow() - .clusters() - .get(&cluster_id) - .map(|cluster| cluster.https_redirect) - .unwrap_or(false); - - if frontend_should_redirect_https { - self.set_answer(DefaultAnswer::Answer301 { - location: format!("https://{host}{uri}"), - }); - return Err(RetrieveClusterError::UnauthorizedRoute); } - - Ok(cluster_id) } pub fn backend_from_request( diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index 5f91baa7c..f9fab2e04 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -1,11 +1,19 @@ pub mod pattern_trie; -use std::{str::from_utf8, time::Instant}; +use std::{ + fmt::Write, + str::{from_utf8, from_utf8_unchecked}, + time::Instant, +}; +use nom::AsChar; +use pattern_trie::{TrieMatches, TrieSubMatch}; use regex::bytes::Regex; use sozu_command::{ - proto::command::{PathRule as CommandPathRule, PathRuleKind, RulePosition}, + proto::command::{ + PathRule as CommandPathRule, PathRuleKind, RedirectPolicy, RedirectScheme, RulePosition, + }, response::HttpFrontend, state::ClusterId, }; @@ -16,8 +24,12 @@ use crate::{protocol::http::parser::Method, router::pattern_trie::TrieNode}; pub enum RouterError { #[error("Could not parse rule from frontend path {0:?}")] InvalidPathRule(String), - #[error("parsing hostname {hostname} failed")] - InvalidDomain { hostname: String }, + #[error("Could not parse hostname {0:?}")] + InvalidDomain(String), + #[error("Could not parse host rewrite {0:?}")] + InvalidHostRewrite(String), + #[error("Could not parse path rewrite {0:?}")] + InvalidPathRewrite(String), #[error("Could not add route {0}")] AddRoute(String), #[error("Could not remove route {0}")] @@ -51,35 +63,46 @@ impl Router { } } - pub fn lookup( - &self, - hostname: &str, - path: &str, - method: &Method, - ) -> Result { + pub fn lookup<'a, 'b>( + &'a self, + hostname: &'b str, + path: &'b str, + method: &'b Method, + ) -> Result { let hostname_b = hostname.as_bytes(); let path_b = path.as_bytes(); - for (domain_rule, path_rule, method_rule, cluster_id) in &self.pre { + for (domain_rule, path_rule, method_rule, route) in &self.pre { if domain_rule.matches(hostname_b) && path_rule.matches(path_b) != PathRuleResult::None && method_rule.matches(method) != MethodRuleResult::None { - return Ok(cluster_id.clone()); + return Ok(RouteResult::new_no_trie( + hostname_b, + domain_rule, + path_b, + path_rule, + route, + )); } } - if let Some((_, path_rules)) = self.tree.lookup(hostname_b, true) { + let trie_path = Vec::with_capacity(16); + if let Some(((_, rules), trie_path)) = self.tree.lookup(hostname_b, true, trie_path) { let mut prefix_length = 0; - let mut route = None; + let mut frontend = None; - for (rule, method_rule, cluster_id) in path_rules { - match rule.matches(path_b) { + for (path_rule, method_rule, route) in rules { + match path_rule.matches(path_b) { PathRuleResult::Regex | PathRuleResult::Equals => { match method_rule.matches(method) { - MethodRuleResult::Equals => return Ok(cluster_id.clone()), + MethodRuleResult::Equals => { + return Ok(RouteResult::new_with_trie( + trie_path, path_b, path_rule, route, + )) + } MethodRuleResult::All => { prefix_length = path_b.len(); - route = Some(cluster_id); + frontend = Some((path_rule, route)); } MethodRuleResult::None => {} } @@ -90,11 +113,11 @@ impl Router { // FIXME: the rule order will be important here MethodRuleResult::Equals => { prefix_length = size; - route = Some(cluster_id); + frontend = Some((path_rule, route)); } MethodRuleResult::All => { prefix_length = size; - route = Some(cluster_id); + frontend = Some((path_rule, route)); } MethodRuleResult::None => {} } @@ -104,17 +127,25 @@ impl Router { } } - if let Some(cluster_id) = route { - return Ok(cluster_id.clone()); + if let Some((path_rule, route)) = frontend { + return Ok(RouteResult::new_with_trie( + trie_path, path_b, path_rule, route, + )); } } - for (domain_rule, path_rule, method_rule, cluster_id) in self.post.iter() { + for (domain_rule, path_rule, method_rule, route) in self.post.iter() { if domain_rule.matches(hostname_b) && path_rule.matches(path_b) != PathRuleResult::None && method_rule.matches(method) != MethodRuleResult::None { - return Ok(cluster_id.clone()); + return Ok(RouteResult::new_no_trie( + hostname_b, + domain_rule, + path_b, + path_rule, + route, + )); } } @@ -126,34 +157,34 @@ impl Router { } pub fn add_http_front(&mut self, front: &HttpFrontend) -> Result<(), RouterError> { + let domain_rule = front + .hostname + .parse::() + .map_err(|_| RouterError::InvalidDomain(front.hostname.clone()))?; + let path_rule = PathRule::from_config(front.path.clone()) .ok_or(RouterError::InvalidPathRule(front.path.to_string()))?; let method_rule = MethodRule::new(front.method.clone()); let route = match &front.cluster_id { - Some(cluster_id) => Route::ClusterId(cluster_id.clone()), + Some(cluster_id) => Route::new( + cluster_id.clone(), + &domain_rule, + &path_rule, + front.redirect, + front.redirect_scheme, + front.host_rewrite.clone(), + front.path_rewrite.clone(), + )?, None => Route::Deny, }; + println!("ROUTE:{route:#?}"); let success = match front.position { - RulePosition::Pre => { - let domain = front.hostname.parse::().map_err(|_| { - RouterError::InvalidDomain { - hostname: front.hostname.clone(), - } - })?; - - self.add_pre_rule(&domain, &path_rule, &method_rule, &route) - } + RulePosition::Pre => self.add_pre_rule(&domain_rule, &path_rule, &method_rule, &route), RulePosition::Post => { - let domain = front.hostname.parse::().map_err(|_| { - RouterError::InvalidDomain { - hostname: front.hostname.clone(), - } - })?; - - self.add_post_rule(&domain, &path_rule, &method_rule, &route) + self.add_post_rule(&domain_rule, &path_rule, &method_rule, &route) } RulePosition::Tree => { self.add_tree_rule(front.hostname.as_bytes(), &path_rule, &method_rule, &route) @@ -173,22 +204,20 @@ impl Router { let remove_success = match front.position { RulePosition::Pre => { - let domain = front.hostname.parse::().map_err(|_| { - RouterError::InvalidDomain { - hostname: front.hostname.clone(), - } - })?; + let domain_rule = front + .hostname + .parse::() + .map_err(|_| RouterError::InvalidDomain(front.hostname.clone()))?; - self.remove_pre_rule(&domain, &path_rule, &method_rule) + self.remove_pre_rule(&domain_rule, &path_rule, &method_rule) } RulePosition::Post => { - let domain = front.hostname.parse::().map_err(|_| { - RouterError::InvalidDomain { - hostname: front.hostname.clone(), - } - })?; + let domain_rule = front + .hostname + .parse::() + .map_err(|_| RouterError::InvalidDomain(front.hostname.clone()))?; - self.remove_post_rule(&domain, &path_rule, &method_rule) + self.remove_post_rule(&domain_rule, &path_rule, &method_rule) } RulePosition::Tree => { self.remove_tree_rule(front.hostname.as_bytes(), &path_rule, &method_rule) @@ -367,7 +396,7 @@ impl Router { #[derive(Clone, Debug)] pub enum DomainRule { Any, - Exact(String), + Equals(String), Wildcard(String), Regex(Regex), } @@ -388,6 +417,7 @@ fn convert_regex_domain_rule(hostname: &str) -> Option { } index = i + 1; found = true; + break; } } @@ -434,7 +464,7 @@ impl DomainRule { hostname.ends_with(s[1..].as_bytes()) && !&hostname[..len_without_suffix].contains(&b'.') } - DomainRule::Exact(s) => s.as_bytes() == hostname, + DomainRule::Equals(s) => s.as_bytes() == hostname, DomainRule::Regex(r) => { let start = Instant::now(); let is_a_match = r.is_match(hostname); @@ -451,7 +481,7 @@ impl std::cmp::PartialEq for DomainRule { match (self, other) { (DomainRule::Any, DomainRule::Any) => true, (DomainRule::Wildcard(s1), DomainRule::Wildcard(s2)) => s1 == s2, - (DomainRule::Exact(s1), DomainRule::Exact(s2)) => s1 == s2, + (DomainRule::Equals(s1), DomainRule::Equals(s2)) => s1 == s2, (DomainRule::Regex(r1), DomainRule::Regex(r2)) => r1.as_str() == r2.as_str(), _ => false, } @@ -483,7 +513,7 @@ impl std::str::FromStr for DomainRule { } } else { match ::idna::domain_to_ascii(s) { - Ok(r) => DomainRule::Exact(r), + Ok(r) => DomainRule::Equals(r), Err(_) => return Err(()), } }) @@ -590,13 +620,339 @@ impl MethodRule { } } +#[derive(Debug, Clone)] +enum RewritePart { + String(String), + Host(usize), + Path(usize), +} +impl RewritePart { + pub fn string(s: &str) -> Self { + Self::String(String::from(s)) + } + pub fn bytes(b: &[u8]) -> Self { + Self::String(unsafe { String::from_utf8_unchecked(b.to_vec()) }) + } +} + +#[derive(Debug, Clone)] +pub struct RewriteParts(Vec); +impl RewriteParts { + pub fn new( + pattern: &str, + index_max_host: usize, + index_max_path: usize, + used_index_host: &mut usize, + used_index_path: &mut usize, + ) -> Option { + let mut result = Vec::new(); + let mut i = 0; + let pattern = pattern.as_bytes(); + while i < pattern.len() { + if pattern[i] == b'$' { + let is_host = if pattern[i..].starts_with(b"$HOST[") { + i += 6; + true + } else if pattern[i..].starts_with(b"$PATH[") { + i += 6; + false + } else { + return None; + }; + let mut index = 0; + while i < pattern.len() && pattern[i].is_dec_digit() { + index = index * 10 + (pattern[i] - b'0') as usize; + i += 1; + } + if i >= pattern.len() || pattern[i] != b']' { + return None; + } + if is_host && index > *used_index_host { + if index >= index_max_host { + return None; + } + *used_index_host = index; + result.push(RewritePart::Host(index)); + } else if index > *used_index_path { + if index >= index_max_path { + return None; + } + *used_index_path = index; + result.push(RewritePart::Path(index)); + } + i += 1; + } else { + let start = i; + while i < pattern.len() && pattern[i] != b'$' { + i += 1; + } + result.push(RewritePart::bytes(&pattern[start..i])); + } + } + Some(Self(result)) + } + pub fn run(&self, host_captures: &Vec<&str>, path_captures: &Vec<&str>) -> String { + let mut cap = 0; + for part in &self.0 { + cap += match part { + RewritePart::String(s) => s.len(), + RewritePart::Host(i) => unsafe { host_captures.get_unchecked(*i).len() }, + RewritePart::Path(i) => unsafe { path_captures.get_unchecked(*i).len() }, + }; + } + let mut result = String::with_capacity(cap); + for part in &self.0 { + match part { + RewritePart::String(s) => result.write_str(s), + RewritePart::Host(i) => { + result.write_str(unsafe { host_captures.get_unchecked(*i) }) + } + RewritePart::Path(i) => { + result.write_str(unsafe { path_captures.get_unchecked(*i) }) + } + }; + } + result + } +} + /// The cluster to which the traffic will be redirected -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone)] pub enum Route { /// send a 401 default answer Deny, /// the cluster to which the frontend belongs - ClusterId(ClusterId), + Cluster { + cluster_id: ClusterId, + redirect: RedirectPolicy, + redirect_scheme: RedirectScheme, + capture_cap_host: usize, + capture_cap_path: usize, + rewrite_host: Option, + rewrite_path: Option, + }, +} + +impl Route { + pub fn new( + cluster_id: ClusterId, + domain_rule: &DomainRule, + path_rule: &PathRule, + redirect: RedirectPolicy, + redirect_scheme: RedirectScheme, + rewrite_host: Option, + rewrite_path: Option, + ) -> Result { + let mut capture_cap_host = match domain_rule { + DomainRule::Any => 2, + DomainRule::Equals(_) => 1, + DomainRule::Wildcard(_) => 2, + DomainRule::Regex(regex) => regex.captures_len(), + }; + let mut capture_cap_path = match path_rule { + PathRule::Prefix(_) => 2, + PathRule::Equals(_) => 1, + PathRule::Regex(regex) => regex.captures_len(), + }; + let mut used_capture_host = 0; + let mut used_capture_path = 0; + let rewrite_host = if let Some(p) = rewrite_host { + Some( + RewriteParts::new( + &p, + capture_cap_host, + capture_cap_path, + &mut used_capture_host, + &mut used_capture_path, + ) + .ok_or(RouterError::InvalidHostRewrite(p))?, + ) + } else { + None + }; + let rewrite_path = if let Some(p) = rewrite_path { + Some( + RewriteParts::new( + &p, + capture_cap_host, + capture_cap_path, + &mut used_capture_host, + &mut used_capture_path, + ) + .ok_or(RouterError::InvalidPathRewrite(p))?, + ) + } else { + None + }; + if used_capture_host == 0 { + capture_cap_host = 0; + } + if used_capture_path == 0 { + capture_cap_path = 0; + } + Ok(Self::Cluster { + cluster_id, + redirect, + redirect_scheme, + capture_cap_host, + capture_cap_path, + rewrite_host, + rewrite_path, + }) + } + + #[cfg(test)] + pub fn simple(cluster_id: ClusterId) -> Self { + Self::Cluster { + cluster_id, + redirect: RedirectPolicy::Forward, + redirect_scheme: RedirectScheme::UseSame, + capture_cap_host: 0, + capture_cap_path: 0, + rewrite_host: None, + rewrite_path: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RouteResult { + Deny, + Cluster { + cluster_id: ClusterId, + redirect: RedirectPolicy, + redirect_scheme: RedirectScheme, + rewritten_host: Option, + rewritten_path: Option, + }, +} + +impl RouteResult { + fn new<'a>( + captures_host: Vec<&'a str>, + path: &'a [u8], + path_rule: &PathRule, + route: &Route, + ) -> Self { + match route { + Route::Deny => Self::Deny, + Route::Cluster { + cluster_id, + redirect, + redirect_scheme, + capture_cap_path, + rewrite_host, + rewrite_path, + .. + } => { + let mut captures_path = Vec::with_capacity(*capture_cap_path); + if *capture_cap_path > 0 { + match path_rule { + PathRule::Prefix(prefix) => captures_path + .push(unsafe { from_utf8_unchecked(&path[prefix.len()..]) }), + PathRule::Regex(regex) => captures_path.extend( + regex + .captures(&path) + .unwrap() + .iter() + .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }), + ), + _ => {} + } + } + println!("========HOST_CAPTURES: {captures_host:?}"); + println!("========PATH_CAPTURES: {captures_path:?}"); + Self::Cluster { + cluster_id: cluster_id.clone(), + redirect: *redirect, + redirect_scheme: *redirect_scheme, + rewritten_host: rewrite_host + .as_ref() + .map(|rewrite| rewrite.run(&captures_host, &captures_path)), + rewritten_path: rewrite_path + .as_ref() + .map(|rewrite| rewrite.run(&captures_host, &captures_path)), + } + } + } + } + fn new_no_trie<'a>( + domain: &'a [u8], + domain_rule: &DomainRule, + path: &'a [u8], + path_rule: &PathRule, + route: &Route, + ) -> Self { + match route { + Route::Deny => Self::Deny, + Route::Cluster { + capture_cap_host, .. + } => { + let mut captures_host = Vec::with_capacity(*capture_cap_host); + if *capture_cap_host > 0 { + match domain_rule { + DomainRule::Wildcard(suffix) => captures_host.push(unsafe { + from_utf8_unchecked(&domain[..domain.len() - suffix.len()]) + }), + DomainRule::Regex(regex) => captures_host.extend( + regex + .captures(&domain) + .unwrap() + .iter() + .skip(1) + .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }), + ), + _ => {} + } + } + Self::new(captures_host, path, path_rule, route) + } + } + } + fn new_with_trie<'a>( + domain_submatches: TrieMatches<'_, 'a>, + path: &'a [u8], + path_rule: &PathRule, + route: &Route, + ) -> Self { + match route { + Route::Deny => Self::Deny, + Route::Cluster { + capture_cap_host, .. + } => { + let mut captures_host = Vec::with_capacity(*capture_cap_host); + if *capture_cap_host > 0 { + for submatch in domain_submatches { + match submatch { + TrieSubMatch::Wildcard(part) => { + captures_host.push(unsafe { from_utf8_unchecked(part) }) + } + TrieSubMatch::Regexp(part, regex) => captures_host.extend( + regex + .captures(&part) + .unwrap() + .iter() + .skip(1) + .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }), + ), + } + } + } + Self::new(captures_host, path, path_rule, route) + } + } + } + + #[cfg(test)] + pub fn simple(cluster_id: ClusterId) -> Self { + Self::Cluster { + cluster_id, + redirect: RedirectPolicy::Forward, + redirect_scheme: RedirectScheme::UseSame, + rewritten_host: None, + rewritten_path: None, + } + } } #[cfg(test)] @@ -643,7 +999,7 @@ mod tests { assert_eq!("*".parse::().unwrap(), DomainRule::Any); assert_eq!( "www.example.com".parse::().unwrap(), - DomainRule::Exact("www.example.com".to_string()) + DomainRule::Equals("www.example.com".to_string()) ); assert_eq!( "*.example.com".parse::().unwrap(), @@ -660,7 +1016,7 @@ mod tests { fn match_domain_rule() { assert!(DomainRule::Any.matches("www.example.com".as_bytes())); assert!( - DomainRule::Exact("www.example.com".to_string()).matches("www.example.com".as_bytes()) + DomainRule::Equals("www.example.com".to_string()).matches("www.example.com".as_bytes()) ); assert!( DomainRule::Wildcard("*.example.com".to_string()).matches("www.example.com".as_bytes()) @@ -715,27 +1071,27 @@ mod tests { b"*.sozu.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("base".to_string()) + &Route::simple("base".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(RouteResult::simple("base".to_string())) ); assert!(router.add_tree_rule( b"*.sozu.io", &PathRule::Prefix("/api".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("api".to_string()) + &Route::simple("api".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/ap", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(RouteResult::simple("base".to_string())) ); assert_eq!( router.lookup("www.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("api".to_string())) + Ok(RouteResult::simple("api".to_string())) ); } @@ -754,27 +1110,27 @@ mod tests { b"*.sozu.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("base".to_string()) + &Route::simple("base".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(RouteResult::simple("base".to_string())) ); assert!(router.add_tree_rule( b"api.sozu.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("api".to_string()) + &Route::simple("api".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(RouteResult::simple("base".to_string())) ); assert_eq!( router.lookup("api.sozu.io", "/api", &Method::Get), - Ok(Route::ClusterId("api".to_string())) + Ok(RouteResult::simple("api".to_string())) ); } @@ -786,23 +1142,23 @@ mod tests { b"www./.*/.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("base".to_string()) + &Route::simple("base".to_string()) )); println!("{:#?}", router.tree); assert!(router.add_tree_rule( b"www.doc./.*/.io", &PathRule::Prefix("".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("doc".to_string()) + &Route::simple("doc".to_string()) )); println!("{:#?}", router.tree); assert_eq!( router.lookup("www.sozu.io", "/", &Method::Get), - Ok(Route::ClusterId("base".to_string())) + Ok(RouteResult::simple("base".to_string())) ); assert_eq!( router.lookup("www.doc.sozu.io", "/", &Method::Get), - Ok(Route::ClusterId("doc".to_string())) + Ok(RouteResult::simple("doc".to_string())) ); assert!(router.remove_tree_rule( b"www./.*/.io", @@ -813,7 +1169,7 @@ mod tests { assert!(router.lookup("www.sozu.io", "/", &Method::Get).is_err()); assert_eq!( router.lookup("www.doc.sozu.io", "/", &Method::Get), - Ok(Route::ClusterId("doc".to_string())) + Ok(RouteResult::simple("doc".to_string())) ); } @@ -825,30 +1181,30 @@ mod tests { &"*".parse::().unwrap(), &PathRule::Prefix("/.well-known/acme-challenge".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("acme".to_string()) + &Route::simple("acme".to_string()) )); assert!(router.add_tree_rule( "www.example.com".as_bytes(), &PathRule::Prefix("/".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("example".to_string()) + &Route::simple("example".to_string()) )); assert!(router.add_tree_rule( "*.test.example.com".as_bytes(), &PathRule::Regex(Regex::new("/hello[A-Z]+/").unwrap()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("examplewildcard".to_string()) + &Route::simple("examplewildcard".to_string()) )); assert!(router.add_tree_rule( "/test[0-9]/.example.com".as_bytes(), &PathRule::Prefix("/".to_string()), &MethodRule::new(Some("GET".to_string())), - &Route::ClusterId("exampleregex".to_string()) + &Route::simple("exampleregex".to_string()) )); assert_eq!( router.lookup("www.example.com", "/helloA", &Method::new(&b"GET"[..])), - Ok(Route::ClusterId("example".to_string())) + Ok(RouteResult::simple("example".to_string())) ); assert_eq!( router.lookup( @@ -856,7 +1212,7 @@ mod tests { "/.well-known/acme-challenge", &Method::new(&b"GET"[..]) ), - Ok(Route::ClusterId("acme".to_string())) + Ok(RouteResult::simple("acme".to_string())) ); assert!(router .lookup("www.test.example.com", "/", &Method::new(&b"GET"[..])) @@ -867,11 +1223,11 @@ mod tests { "/helloAB/", &Method::new(&b"GET"[..]) ), - Ok(Route::ClusterId("examplewildcard".to_string())) + Ok(RouteResult::simple("examplewildcard".to_string())) ); assert_eq!( router.lookup("test1.example.com", "/helloAB/", &Method::new(&b"GET"[..])), - Ok(Route::ClusterId("exampleregex".to_string())) + Ok(RouteResult::simple("exampleregex".to_string())) ); } } diff --git a/lib/src/router/pattern_trie.rs b/lib/src/router/pattern_trie.rs index f25a46962..af8dff230 100644 --- a/lib/src/router/pattern_trie.rs +++ b/lib/src/router/pattern_trie.rs @@ -1,9 +1,15 @@ -use std::{collections::HashMap, fmt::Debug, iter, str}; +use std::{collections::HashMap, fmt::Debug, hash::Hash, iter, str}; use regex::bytes::Regex; pub type Key = Vec; pub type KeyValue = (K, V); +pub type TrieMatches<'a, 'b> = Vec>; + +pub enum TrieSubMatch<'a, 'b> { + Wildcard(&'a [u8]), + Regexp(&'a [u8], &'b Regex), +} #[derive(Debug, PartialEq, Eq)] pub enum InsertResult { @@ -263,11 +269,16 @@ impl TrieNode { } } - pub fn lookup(&self, partial_key: &[u8], accept_wildcard: bool) -> Option<&KeyValue> { + pub fn lookup<'a, 'b>( + &'a self, + partial_key: &'b [u8], + accept_wildcard: bool, + mut path: TrieMatches<'a, 'b>, + ) -> Option<(&'a KeyValue, TrieMatches<'a, 'b>)> { //println!("lookup: key == {}", std::str::from_utf8(partial_key).unwrap()); if partial_key.is_empty() { - return self.key_value.as_ref(); + return self.key_value.as_ref().map(|kv| (kv, path)); } let pos = find_last_dot(partial_key); @@ -278,27 +289,29 @@ impl TrieNode { //println!("lookup: prefix|suffix: {} | {}", std::str::from_utf8(prefix).unwrap(), std::str::from_utf8(suffix).unwrap()); match self.children.get(suffix) { - Some(child) => child.lookup(prefix, accept_wildcard), + Some(child) => child.lookup(prefix, accept_wildcard, path), None => { //println!("no child found, testing wildcard and regexps"); if prefix.is_empty() && self.wildcard.is_some() && accept_wildcard { //println!("no dot, wildcard applies"); - self.wildcard.as_ref() + path.insert(0, TrieSubMatch::Wildcard(suffix)); + self.wildcard.as_ref().map(|kv| (kv, path)) } else { //println!("there's still a subdomain, wildcard does not apply"); - for (ref regexp, ref child) in self.regexps.iter() { - let suffix = if suffix[0] == b'.' { - &suffix[1..] - } else { - suffix - }; + // let suffix = if suffix[0] == b'.' { + // &suffix[1..] + // } else { + // suffix + // }; + for (regexp, child) in &self.regexps { //println!("testing regexp: {} on suffix {}", r.as_str(), str::from_utf8(s).unwrap()); if regexp.is_match(suffix) { //println!("matched"); - return child.lookup(prefix, accept_wildcard); + path.insert(0, TrieSubMatch::Regexp(suffix, regexp)); + return child.lookup(prefix, accept_wildcard, path); } } @@ -421,7 +434,8 @@ impl TrieNode { } pub fn domain_lookup(&self, key: &[u8], accept_wildcard: bool) -> Option<&KeyValue> { - self.lookup(key, accept_wildcard) + let path = Vec::new(); + self.lookup(key, accept_wildcard, path).map(|(kv, _)| kv) } pub fn domain_lookup_mut( @@ -809,7 +823,7 @@ mod tests { } //match root.domain_lookup(k.as_bytes()) { - match root.lookup(k.as_bytes(), false) { + match root.domain_lookup(k.as_bytes(), false) { None => { println!("did not find key '{k}'"); return false; From 5d7e0f95b57904f3bbb123dd475705b53c4c5370 Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Fri, 13 Dec 2024 12:08:11 +0100 Subject: [PATCH 02/18] Allow clusterless redirections Signed-off-by: Eloi DEMOLIS --- bin/src/ctl/request_builder.rs | 8 +-- command/src/command.proto | 4 +- command/src/config.rs | 20 +++--- command/src/request.rs | 4 +- command/src/response.rs | 12 ++-- lib/src/http.rs | 28 +++++--- lib/src/https.rs | 8 ++- lib/src/lib.rs | 8 +-- lib/src/protocol/kawa_h1/mod.rs | 37 +++-------- lib/src/router/mod.rs | 114 +++++++++++++++++--------------- lib/src/router/pattern_trie.rs | 2 +- 11 files changed, 123 insertions(+), 122 deletions(-) diff --git a/bin/src/ctl/request_builder.rs b/bin/src/ctl/request_builder.rs index fc04667fb..bbb9f2074 100644 --- a/bin/src/ctl/request_builder.rs +++ b/bin/src/ctl/request_builder.rs @@ -252,8 +252,8 @@ impl CommandManager { }, redirect: todo!(), redirect_scheme: todo!(), - host_rewrite: todo!(), - path_rewrite: todo!(), + rewrite_host: todo!(), + rewrite_path: todo!(), }) .into(), ), @@ -304,8 +304,8 @@ impl CommandManager { }, redirect: todo!(), redirect_scheme: todo!(), - host_rewrite: todo!(), - path_rewrite: todo!(), + rewrite_host: todo!(), + rewrite_path: todo!(), }) .into(), ), diff --git a/command/src/command.proto b/command/src/command.proto index c3836b1b5..497c93169 100644 --- a/command/src/command.proto +++ b/command/src/command.proto @@ -263,8 +263,8 @@ message RequestHttpFrontend { map tags = 7; optional RedirectPolicy redirect = 8; optional RedirectScheme redirect_scheme = 9; - optional string host_rewrite = 10; - optional string path_rewrite = 11; + optional string rewrite_host = 10; + optional string rewrite_path = 11; } message RequestTcpFrontend { diff --git a/command/src/config.rs b/command/src/config.rs index dd5709fb5..4a4d49237 100644 --- a/command/src/config.rs +++ b/command/src/config.rs @@ -669,8 +669,8 @@ pub struct FileClusterFrontendConfig { pub tags: Option>, pub redirect: Option, pub redirect_scheme: Option, - pub host_rewrite: Option, - pub path_rewrite: Option, + pub rewrite_host: Option, + pub rewrite_path: Option, } impl FileClusterFrontendConfig { @@ -758,8 +758,8 @@ impl FileClusterFrontendConfig { tags: self.tags.clone(), redirect: self.redirect, redirect_scheme: self.redirect_scheme, - host_rewrite: self.host_rewrite.clone(), - path_rewrite: self.path_rewrite.clone(), + rewrite_host: self.rewrite_host.clone(), + rewrite_path: self.rewrite_path.clone(), }) } } @@ -912,8 +912,8 @@ pub struct HttpFrontendConfig { pub tags: Option>, pub redirect: Option, pub redirect_scheme: Option, - pub host_rewrite: Option, - pub path_rewrite: Option, + pub rewrite_host: Option, + pub rewrite_path: Option, } impl HttpFrontendConfig { @@ -953,8 +953,8 @@ impl HttpFrontendConfig { tags, redirect: self.redirect.map(Into::into), redirect_scheme: self.redirect_scheme.map(Into::into), - host_rewrite: self.host_rewrite.clone(), - path_rewrite: self.path_rewrite.clone(), + rewrite_host: self.rewrite_host.clone(), + rewrite_path: self.rewrite_path.clone(), }) .into(), ); @@ -971,8 +971,8 @@ impl HttpFrontendConfig { tags, redirect: self.redirect.map(Into::into), redirect_scheme: self.redirect_scheme.map(Into::into), - host_rewrite: self.host_rewrite.clone(), - path_rewrite: self.path_rewrite.clone(), + rewrite_host: self.rewrite_host.clone(), + rewrite_path: self.rewrite_path.clone(), }) .into(), ); diff --git a/command/src/request.rs b/command/src/request.rs index ba5ce6413..286f8e349 100644 --- a/command/src/request.rs +++ b/command/src/request.rs @@ -169,8 +169,8 @@ impl RequestHttpFrontend { path: self.path, method: self.method, tags: Some(self.tags), - host_rewrite: self.host_rewrite, - path_rewrite: self.path_rewrite, + rewrite_host: self.rewrite_host, + rewrite_path: self.rewrite_path, }) } } diff --git a/command/src/response.rs b/command/src/response.rs index 8c0a81225..292e0ae1f 100644 --- a/command/src/response.rs +++ b/command/src/response.rs @@ -2,7 +2,9 @@ use std::{cmp::Ordering, collections::BTreeMap, fmt, net::SocketAddr}; use crate::{ proto::command::{ - AddBackend, FilteredTimeSerie, LoadBalancingParams, PathRule, PathRuleKind, RedirectPolicy, RedirectScheme, RequestHttpFrontend, RequestTcpFrontend, Response, ResponseContent, ResponseStatus, RulePosition, RunState, WorkerResponse + AddBackend, FilteredTimeSerie, LoadBalancingParams, PathRule, PathRuleKind, RedirectPolicy, + RedirectScheme, RequestHttpFrontend, RequestTcpFrontend, Response, ResponseContent, + ResponseStatus, RulePosition, RunState, WorkerResponse, }, state::ClusterId, }; @@ -39,9 +41,9 @@ pub struct HttpFrontend { pub redirect: RedirectPolicy, pub redirect_scheme: RedirectScheme, #[serde(skip_serializing_if = "Option::is_none")] - pub host_rewrite: Option, + pub rewrite_host: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub path_rewrite: Option, + pub rewrite_path: Option, pub tags: Option>, } @@ -57,8 +59,8 @@ impl From for RequestHttpFrontend { tags: val.tags.unwrap_or_default(), redirect: Some(val.redirect.into()), redirect_scheme: Some(val.redirect_scheme.into()), - host_rewrite: val.host_rewrite, - path_rewrite: val.path_rewrite, + rewrite_host: val.rewrite_host, + rewrite_path: val.rewrite_path, } } } diff --git a/lib/src/http.rs b/lib/src/http.rs index cc6c48176..8f9c3b7b2 100644 --- a/lib/src/http.rs +++ b/lib/src/http.rs @@ -39,7 +39,7 @@ use crate::{ proxy_protocol::expect::ExpectProxyProtocol, Http, Pipe, SessionState, }, - router::{Route, RouteResult, Router}, + router::{RouteDirection, RouteResult, Router}, server::{ListenToken, SessionManager}, socket::server_bind, timer::TimeoutContainer, @@ -472,7 +472,11 @@ impl L7ListenerHandler for HttpListener { let now = Instant::now(); - if let RouteResult::Cluster { cluster_id, .. } = &route { + if let RouteResult::Flow { + direction: RouteDirection::Forward(cluster_id), + .. + } = &route + { time!( "frontend_matching_time", cluster_id, @@ -1056,7 +1060,9 @@ mod tests { use super::testing::start_http_worker; use super::*; - use sozu_command::proto::command::{CustomHttpAnswers, RedirectPolicy, RedirectScheme, SocketAddress}; + use sozu_command::proto::command::{ + CustomHttpAnswers, RedirectPolicy, RedirectScheme, SocketAddress, + }; use crate::sozu_command::{ channel::Channel, @@ -1331,8 +1337,8 @@ mod tests { cluster_id: Some(cluster_id1), redirect: RedirectPolicy::Forward, redirect_scheme: RedirectScheme::UseSame, - host_rewrite: None, - path_rewrite: None, + rewrite_host: None, + rewrite_path: None, tags: None, }) .expect("Could not add http frontend"); @@ -1346,8 +1352,8 @@ mod tests { cluster_id: Some(cluster_id2), redirect: RedirectPolicy::Forward, redirect_scheme: RedirectScheme::UseSame, - host_rewrite: None, - path_rewrite: None, + rewrite_host: None, + rewrite_path: None, tags: None, }) .expect("Could not add http frontend"); @@ -1361,8 +1367,8 @@ mod tests { cluster_id: Some(cluster_id3), redirect: RedirectPolicy::Forward, redirect_scheme: RedirectScheme::UseSame, - host_rewrite: None, - path_rewrite: None, + rewrite_host: None, + rewrite_path: None, tags: None, }) .expect("Could not add http frontend"); @@ -1376,8 +1382,8 @@ mod tests { cluster_id: Some("cluster_1".to_owned()), redirect: RedirectPolicy::Forward, redirect_scheme: RedirectScheme::UseSame, - host_rewrite: None, - path_rewrite: None, + rewrite_host: None, + rewrite_path: None, tags: None, }) .expect("Could not add http frontend"); diff --git a/lib/src/https.rs b/lib/src/https.rs index 92e8d3b62..42a50937d 100644 --- a/lib/src/https.rs +++ b/lib/src/https.rs @@ -62,7 +62,7 @@ use crate::{ rustls::TlsHandshake, Http, Pipe, SessionState, }, - router::{Route, RouteResult, Router}, + router::{RouteDirection, RouteResult, Router}, server::{ListenToken, SessionManager}, socket::{server_bind, FrontRustls}, timer::TimeoutContainer, @@ -600,7 +600,11 @@ impl L7ListenerHandler for HttpsListener { let now = Instant::now(); - if let RouteResult::Cluster { cluster_id, .. } = &route { + if let RouteResult::Flow { + direction: RouteDirection::Forward(cluster_id), + .. + } = &route + { time!( "frontend_matching_time", cluster_id, diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f00c681c5..4564237f4 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -356,13 +356,13 @@ use tls::CertificateResolverError; use sozu_command::{ logging::{CachedTags, LogContext}, - proto::command::{Cluster, ListenerType, RedirectPolicy, RequestHttpFrontend, WorkerRequest, WorkerResponse}, + proto::command::{Cluster, ListenerType, RequestHttpFrontend, WorkerRequest, WorkerResponse}, ready::Ready, state::ClusterId, AsStr, ObjectKind, }; -use crate::{backends::BackendMap, router::Route}; +use crate::backends::BackendMap; /// Anything that can be registered in mio (subscribe to kernel events) #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -602,8 +602,8 @@ pub enum RetrieveClusterError { NoPath, #[error("unauthorized route")] UnauthorizedRoute, - #[error("redirected {0:?}")] - Redirected(RedirectPolicy), + #[error("redirected")] + Redirected, #[error("{0}")] RetrieveFrontend(FrontendFromRequestError), } diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs index fd1a040bd..baa3b6001 100644 --- a/lib/src/protocol/kawa_h1/mod.rs +++ b/lib/src/protocol/kawa_h1/mod.rs @@ -16,7 +16,7 @@ use rusty_ulid::Ulid; use sozu_command::{ config::MAX_LOOP_ITERATIONS, logging::EndpointRecord, - proto::command::{Event, EventKind, ListenerType, RedirectPolicy, RedirectScheme}, + proto::command::{Event, EventKind, ListenerType, RedirectScheme}, }; // use time::{Duration, Instant}; @@ -34,7 +34,7 @@ use crate::{ SessionState, }, retry::RetryPolicy, - router::{Route, RouteResult}, + router::{RouteDirection, RouteResult}, server::{push_event, CONN_RETRIES}, socket::{stats::socket_rtt, SocketHandler, SocketResult, TransportProtocol}, sozu_command::{logging::LogContext, ready::Ready}, @@ -1280,35 +1280,21 @@ impl Http { + RouteResult::Deny => { self.set_answer(DefaultAnswer::Answer401 {}); Err(RetrieveClusterError::UnauthorizedRoute) } - RouteResult::Cluster { - cluster_id, - redirect, - redirect_scheme, + RouteResult::Flow { + direction: flow, rewritten_host, rewritten_path, } => { let host = rewritten_host.as_deref().unwrap_or(host); let path = rewritten_path.as_deref().unwrap_or(uri); let is_https = matches!(proxy.borrow().kind(), ListenerType::Https); - match (redirect, is_https) { - (RedirectPolicy::Forward, _) | (RedirectPolicy::ForceHttps, true) => { - Ok(cluster_id) - } - (RedirectPolicy::ForceHttps, false) => { - self.set_answer(DefaultAnswer::Answer301 { - location: format!("https://{host}{path}"), - }); - Err(RetrieveClusterError::Redirected(redirect)) - } - (RedirectPolicy::Permanent, _) => { + match flow { + RouteDirection::Forward(cluster_id) => Ok(cluster_id), + RouteDirection::Permanent(redirect_scheme) => { let proto = match (redirect_scheme, is_https) { (RedirectScheme::UseHttp, _) | (RedirectScheme::UseSame, false) => { "http" @@ -1320,10 +1306,9 @@ impl Http todo!(), - (RedirectPolicy::Unauthorized, _) => unreachable!(), + RouteDirection::Temporary(_) => todo!(), } } } @@ -1615,7 +1600,7 @@ impl Http StateResult { + pub fn backend_hup(&mut self, _metrics: &mut SessionMetrics) -> StateResult { let response_stream = match &mut self.response_stream { ResponseStream::BackendAnswer(response_stream) => response_stream, _ => return StateResult::CloseBackend, diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index f9fab2e04..e6ee8499f 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -97,7 +97,7 @@ impl Router { match method_rule.matches(method) { MethodRuleResult::Equals => { return Ok(RouteResult::new_with_trie( - trie_path, path_b, path_rule, route, + hostname_b, trie_path, path_b, path_rule, route, )) } MethodRuleResult::All => { @@ -129,7 +129,7 @@ impl Router { if let Some((path_rule, route)) = frontend { return Ok(RouteResult::new_with_trie( - trie_path, path_b, path_rule, route, + hostname_b, trie_path, path_b, path_rule, route, )); } } @@ -167,18 +167,15 @@ impl Router { let method_rule = MethodRule::new(front.method.clone()); - let route = match &front.cluster_id { - Some(cluster_id) => Route::new( - cluster_id.clone(), - &domain_rule, - &path_rule, - front.redirect, - front.redirect_scheme, - front.host_rewrite.clone(), - front.path_rewrite.clone(), - )?, - None => Route::Deny, - }; + let route = Route::new( + front.cluster_id.clone(), + &domain_rule, + &path_rule, + front.redirect, + front.redirect_scheme, + front.rewrite_host.clone(), + front.rewrite_path.clone(), + )?; println!("ROUTE:{route:#?}"); let success = match front.position { @@ -667,17 +664,21 @@ impl RewriteParts { if i >= pattern.len() || pattern[i] != b']' { return None; } - if is_host && index > *used_index_host { + if is_host { if index >= index_max_host { return None; } - *used_index_host = index; + if index >= *used_index_host { + *used_index_host = index + 1; + } result.push(RewritePart::Host(index)); - } else if index > *used_index_path { + } else { if index >= index_max_path { return None; } - *used_index_path = index; + if index >= *used_index_path { + *used_index_path = index + 1; + } result.push(RewritePart::Path(index)); } i += 1; @@ -702,7 +703,7 @@ impl RewriteParts { } let mut result = String::with_capacity(cap); for part in &self.0 { - match part { + let _ = match part { RewritePart::String(s) => result.write_str(s), RewritePart::Host(i) => { result.write_str(unsafe { host_captures.get_unchecked(*i) }) @@ -716,26 +717,30 @@ impl RewriteParts { } } -/// The cluster to which the traffic will be redirected +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RouteDirection { + Forward(ClusterId), + Temporary(RedirectScheme), + Permanent(RedirectScheme), +} + +/// What to do with the traffic #[derive(Debug, Clone)] pub enum Route { - /// send a 401 default answer Deny, - /// the cluster to which the frontend belongs - Cluster { - cluster_id: ClusterId, - redirect: RedirectPolicy, - redirect_scheme: RedirectScheme, + Flow { + direction: RouteDirection, capture_cap_host: usize, capture_cap_path: usize, rewrite_host: Option, rewrite_path: Option, + // rewrite_port? }, } impl Route { pub fn new( - cluster_id: ClusterId, + cluster_id: Option, domain_rule: &DomainRule, path_rule: &PathRule, redirect: RedirectPolicy, @@ -743,15 +748,21 @@ impl Route { rewrite_host: Option, rewrite_path: Option, ) -> Result { + let flow = match (cluster_id, redirect) { + (Some(cluster_id), RedirectPolicy::Forward) => RouteDirection::Forward(cluster_id), + (_, RedirectPolicy::Temporary) => RouteDirection::Temporary(redirect_scheme), + (_, RedirectPolicy::Permanent) => RouteDirection::Permanent(redirect_scheme), + _ => return Ok(Route::Deny), + }; let mut capture_cap_host = match domain_rule { - DomainRule::Any => 2, + DomainRule::Any => 1, DomainRule::Equals(_) => 1, DomainRule::Wildcard(_) => 2, DomainRule::Regex(regex) => regex.captures_len(), }; let mut capture_cap_path = match path_rule { - PathRule::Prefix(_) => 2, PathRule::Equals(_) => 1, + PathRule::Prefix(_) => 2, PathRule::Regex(regex) => regex.captures_len(), }; let mut used_capture_host = 0; @@ -790,10 +801,8 @@ impl Route { if used_capture_path == 0 { capture_cap_path = 0; } - Ok(Self::Cluster { - cluster_id, - redirect, - redirect_scheme, + Ok(Route::Flow { + direction: flow, capture_cap_host, capture_cap_path, rewrite_host, @@ -803,10 +812,8 @@ impl Route { #[cfg(test)] pub fn simple(cluster_id: ClusterId) -> Self { - Self::Cluster { - cluster_id, - redirect: RedirectPolicy::Forward, - redirect_scheme: RedirectScheme::UseSame, + Self::Flow { + direction: RouteDirection::Forward(cluster_id), capture_cap_host: 0, capture_cap_path: 0, rewrite_host: None, @@ -818,12 +825,11 @@ impl Route { #[derive(Debug, Clone, PartialEq, Eq)] pub enum RouteResult { Deny, - Cluster { - cluster_id: ClusterId, - redirect: RedirectPolicy, - redirect_scheme: RedirectScheme, + Flow { + direction: RouteDirection, rewritten_host: Option, rewritten_path: Option, + // rewritten_port? }, } @@ -836,10 +842,8 @@ impl RouteResult { ) -> Self { match route { Route::Deny => Self::Deny, - Route::Cluster { - cluster_id, - redirect, - redirect_scheme, + Route::Flow { + direction: flow, capture_cap_path, rewrite_host, rewrite_path, @@ -847,6 +851,7 @@ impl RouteResult { } => { let mut captures_path = Vec::with_capacity(*capture_cap_path); if *capture_cap_path > 0 { + captures_path.push(unsafe { from_utf8_unchecked(path) }); match path_rule { PathRule::Prefix(prefix) => captures_path .push(unsafe { from_utf8_unchecked(&path[prefix.len()..]) }), @@ -862,10 +867,8 @@ impl RouteResult { } println!("========HOST_CAPTURES: {captures_host:?}"); println!("========PATH_CAPTURES: {captures_path:?}"); - Self::Cluster { - cluster_id: cluster_id.clone(), - redirect: *redirect, - redirect_scheme: *redirect_scheme, + Self::Flow { + direction: flow.clone(), rewritten_host: rewrite_host .as_ref() .map(|rewrite| rewrite.run(&captures_host, &captures_path)), @@ -885,11 +888,12 @@ impl RouteResult { ) -> Self { match route { Route::Deny => Self::Deny, - Route::Cluster { + Route::Flow { capture_cap_host, .. } => { let mut captures_host = Vec::with_capacity(*capture_cap_host); if *capture_cap_host > 0 { + captures_host.push(unsafe { from_utf8_unchecked(domain) }); match domain_rule { DomainRule::Wildcard(suffix) => captures_host.push(unsafe { from_utf8_unchecked(&domain[..domain.len() - suffix.len()]) @@ -910,6 +914,7 @@ impl RouteResult { } } fn new_with_trie<'a>( + domain: &'a [u8], domain_submatches: TrieMatches<'_, 'a>, path: &'a [u8], path_rule: &PathRule, @@ -917,11 +922,12 @@ impl RouteResult { ) -> Self { match route { Route::Deny => Self::Deny, - Route::Cluster { + Route::Flow { capture_cap_host, .. } => { let mut captures_host = Vec::with_capacity(*capture_cap_host); if *capture_cap_host > 0 { + captures_host.push(unsafe { from_utf8_unchecked(domain) }); for submatch in domain_submatches { match submatch { TrieSubMatch::Wildcard(part) => { @@ -945,10 +951,8 @@ impl RouteResult { #[cfg(test)] pub fn simple(cluster_id: ClusterId) -> Self { - Self::Cluster { - cluster_id, - redirect: RedirectPolicy::Forward, - redirect_scheme: RedirectScheme::UseSame, + Self::Flow { + direction: RouteDirection::Forward(cluster_id), rewritten_host: None, rewritten_path: None, } diff --git a/lib/src/router/pattern_trie.rs b/lib/src/router/pattern_trie.rs index af8dff230..04f4dc23a 100644 --- a/lib/src/router/pattern_trie.rs +++ b/lib/src/router/pattern_trie.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt::Debug, hash::Hash, iter, str}; +use std::{collections::HashMap, fmt::Debug, iter, str}; use regex::bytes::Regex; From 2360820178fe6e0a78ea373f8821ec0af2b0096a Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Fri, 13 Dec 2024 16:42:35 +0100 Subject: [PATCH 03/18] Re-enable https_redirect on cluster level Signed-off-by: Eloi DEMOLIS --- command/src/state.rs | 5 ++++- lib/src/protocol/kawa_h1/mod.rs | 23 +++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/command/src/state.rs b/command/src/state.rs index e8371a8bd..62e2156cc 100644 --- a/command/src/state.rs +++ b/command/src/state.rs @@ -1482,7 +1482,8 @@ mod tests { use super::*; use crate::proto::command::{ - CustomHttpAnswers, LoadBalancingParams, RequestHttpFrontend, RulePosition, + CustomHttpAnswers, LoadBalancingParams, RedirectPolicy, RedirectScheme, + RequestHttpFrontend, RulePosition, }; #[test] @@ -1724,6 +1725,8 @@ mod tests { hostname: String::from("test.local"), path: PathRule::prefix(String::from("/abc")), address: SocketAddress::new_v4(0, 0, 0, 0, 8080), + redirect: Some(RedirectPolicy::Forward.into()), + redirect_scheme: Some(RedirectScheme::UseSame.into()), ..Default::default() }) .into(), diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs index baa3b6001..67fb96fdb 100644 --- a/lib/src/protocol/kawa_h1/mod.rs +++ b/lib/src/protocol/kawa_h1/mod.rs @@ -1252,7 +1252,7 @@ impl Http>, ) -> Result { - let (host, uri, method) = match self.extract_route() { + let (host, path, method) = match self.extract_route() { Ok(tuple) => tuple, Err(cluster_error) => { self.set_answer(DefaultAnswer::Answer400 { @@ -1269,7 +1269,7 @@ impl Http route, @@ -1289,9 +1289,24 @@ impl Http { - let host = rewritten_host.as_deref().unwrap_or(host); - let path = rewritten_path.as_deref().unwrap_or(uri); let is_https = matches!(proxy.borrow().kind(), ListenerType::Https); + if let RouteDirection::Forward(cluster_id) = &flow { + if !is_https + && proxy + .borrow() + .clusters() + .get(cluster_id) + .map(|cluster| cluster.https_redirect) + .unwrap_or(false) + { + self.set_answer(DefaultAnswer::Answer301 { + location: format!("https://{host}{path}"), + }); + return Err(RetrieveClusterError::Redirected); + } + } + let host = rewritten_host.as_deref().unwrap_or(host); + let path = rewritten_path.as_deref().unwrap_or(path); match flow { RouteDirection::Forward(cluster_id) => Ok(cluster_id), RouteDirection::Permanent(redirect_scheme) => { From c9ffaae68621f543b848aa447d76411e3656751f Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Tue, 17 Dec 2024 12:04:33 +0100 Subject: [PATCH 04/18] Add rewrite_port on frontends Signed-off-by: Eloi DEMOLIS --- bin/src/ctl/request_builder.rs | 2 + command/src/command.proto | 1 + command/src/config.rs | 5 +++ command/src/request.rs | 1 + command/src/response.rs | 3 ++ lib/src/http.rs | 55 +++-------------------- lib/src/https.rs | 45 ++----------------- lib/src/protocol/kawa_h1/mod.rs | 79 +++++++++++++++++++++++++++------ lib/src/router/mod.rs | 11 ++++- 9 files changed, 97 insertions(+), 105 deletions(-) diff --git a/bin/src/ctl/request_builder.rs b/bin/src/ctl/request_builder.rs index bbb9f2074..a4094aff6 100644 --- a/bin/src/ctl/request_builder.rs +++ b/bin/src/ctl/request_builder.rs @@ -254,6 +254,7 @@ impl CommandManager { redirect_scheme: todo!(), rewrite_host: todo!(), rewrite_path: todo!(), + rewrite_port: todo!(), }) .into(), ), @@ -306,6 +307,7 @@ impl CommandManager { redirect_scheme: todo!(), rewrite_host: todo!(), rewrite_path: todo!(), + rewrite_port: todo!(), }) .into(), ), diff --git a/command/src/command.proto b/command/src/command.proto index 497c93169..7ce05681a 100644 --- a/command/src/command.proto +++ b/command/src/command.proto @@ -265,6 +265,7 @@ message RequestHttpFrontend { optional RedirectScheme redirect_scheme = 9; optional string rewrite_host = 10; optional string rewrite_path = 11; + optional uint32 rewrite_port = 12; } message RequestTcpFrontend { diff --git a/command/src/config.rs b/command/src/config.rs index 4a4d49237..bfd3236bf 100644 --- a/command/src/config.rs +++ b/command/src/config.rs @@ -671,6 +671,7 @@ pub struct FileClusterFrontendConfig { pub redirect_scheme: Option, pub rewrite_host: Option, pub rewrite_path: Option, + pub rewrite_port: Option, } impl FileClusterFrontendConfig { @@ -760,6 +761,7 @@ impl FileClusterFrontendConfig { redirect_scheme: self.redirect_scheme, rewrite_host: self.rewrite_host.clone(), rewrite_path: self.rewrite_path.clone(), + rewrite_port: self.rewrite_port.clone(), }) } } @@ -914,6 +916,7 @@ pub struct HttpFrontendConfig { pub redirect_scheme: Option, pub rewrite_host: Option, pub rewrite_path: Option, + pub rewrite_port: Option, } impl HttpFrontendConfig { @@ -955,6 +958,7 @@ impl HttpFrontendConfig { redirect_scheme: self.redirect_scheme.map(Into::into), rewrite_host: self.rewrite_host.clone(), rewrite_path: self.rewrite_path.clone(), + rewrite_port: self.rewrite_port.map(|x| x as u32), }) .into(), ); @@ -973,6 +977,7 @@ impl HttpFrontendConfig { redirect_scheme: self.redirect_scheme.map(Into::into), rewrite_host: self.rewrite_host.clone(), rewrite_path: self.rewrite_path.clone(), + rewrite_port: self.rewrite_port.map(|x| x as u32), }) .into(), ); diff --git a/command/src/request.rs b/command/src/request.rs index 286f8e349..2dd91a5d0 100644 --- a/command/src/request.rs +++ b/command/src/request.rs @@ -171,6 +171,7 @@ impl RequestHttpFrontend { tags: Some(self.tags), rewrite_host: self.rewrite_host, rewrite_path: self.rewrite_path, + rewrite_port: self.rewrite_port.map(|x| x as u16), }) } } diff --git a/command/src/response.rs b/command/src/response.rs index 292e0ae1f..af91a01cf 100644 --- a/command/src/response.rs +++ b/command/src/response.rs @@ -44,6 +44,8 @@ pub struct HttpFrontend { pub rewrite_host: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rewrite_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rewrite_port: Option, pub tags: Option>, } @@ -61,6 +63,7 @@ impl From for RequestHttpFrontend { redirect_scheme: Some(val.redirect_scheme.into()), rewrite_host: val.rewrite_host, rewrite_path: val.rewrite_path, + rewrite_port: val.rewrite_port.map(|x| x as u32), } } } diff --git a/lib/src/http.rs b/lib/src/http.rs index 8f9c3b7b2..f8197568f 100644 --- a/lib/src/http.rs +++ b/lib/src/http.rs @@ -430,61 +430,16 @@ impl L7ListenerHandler for HttpListener { self.config.connect_timeout } - // redundant, already called once in extract_route fn frontend_from_request( &self, host: &str, - uri: &str, + path: &str, method: &Method, ) -> Result { - let start = Instant::now(); - let (remaining_input, (hostname, _)) = match hostname_and_port(host.as_bytes()) { - Ok(tuple) => tuple, - Err(parse_error) => { - // parse_error contains a slice of given_host, which should NOT escape this scope - return Err(FrontendFromRequestError::HostParse { - host: host.to_owned(), - error: parse_error.to_string(), - }); - } - }; - if remaining_input != &b""[..] { - return Err(FrontendFromRequestError::InvalidCharsAfterHost( - host.to_owned(), - )); - } - - /*if port == Some(&b"80"[..]) { - // it is alright to call from_utf8_unchecked, - // we already verified that there are only ascii - // chars in there - unsafe { from_utf8_unchecked(hostname) } - } else { - host - } - */ - let host = unsafe { from_utf8_unchecked(hostname) }; - - let route = self.fronts.lookup(host, uri, method).map_err(|e| { + self.fronts.lookup(host, path, method).map_err(|e| { incr!("http.failed_backend_matching"); FrontendFromRequestError::NoClusterFound(e) - })?; - - let now = Instant::now(); - - if let RouteResult::Flow { - direction: RouteDirection::Forward(cluster_id), - .. - } = &route - { - time!( - "frontend_matching_time", - cluster_id, - (now - start).as_millis() - ); - } - - Ok(route) + }) } } @@ -1339,6 +1294,7 @@ mod tests { redirect_scheme: RedirectScheme::UseSame, rewrite_host: None, rewrite_path: None, + rewrite_port: None, tags: None, }) .expect("Could not add http frontend"); @@ -1354,6 +1310,7 @@ mod tests { redirect_scheme: RedirectScheme::UseSame, rewrite_host: None, rewrite_path: None, + rewrite_port: None, tags: None, }) .expect("Could not add http frontend"); @@ -1369,6 +1326,7 @@ mod tests { redirect_scheme: RedirectScheme::UseSame, rewrite_host: None, rewrite_path: None, + rewrite_port: None, tags: None, }) .expect("Could not add http frontend"); @@ -1384,6 +1342,7 @@ mod tests { redirect_scheme: RedirectScheme::UseSame, rewrite_host: None, rewrite_path: None, + rewrite_port: None, tags: None, }) .expect("Could not add http frontend"); diff --git a/lib/src/https.rs b/lib/src/https.rs index 42a50937d..8047175ae 100644 --- a/lib/src/https.rs +++ b/lib/src/https.rs @@ -567,52 +567,13 @@ impl L7ListenerHandler for HttpsListener { fn frontend_from_request( &self, host: &str, - uri: &str, + path: &str, method: &Method, ) -> Result { - let start = Instant::now(); - let (remaining_input, (hostname, _)) = match hostname_and_port(host.as_bytes()) { - Ok(tuple) => tuple, - Err(parse_error) => { - // parse_error contains a slice of given_host, which should NOT escape this scope - return Err(FrontendFromRequestError::HostParse { - host: host.to_owned(), - error: parse_error.to_string(), - }); - } - }; - - if remaining_input != &b""[..] { - return Err(FrontendFromRequestError::InvalidCharsAfterHost( - host.to_owned(), - )); - } - - // it is alright to call from_utf8_unchecked, - // we already verified that there are only ascii - // chars in there - let host = unsafe { from_utf8_unchecked(hostname) }; - - let route = self.fronts.lookup(host, uri, method).map_err(|e| { + self.fronts.lookup(host, path, method).map_err(|e| { incr!("http.failed_backend_matching"); FrontendFromRequestError::NoClusterFound(e) - })?; - - let now = Instant::now(); - - if let RouteResult::Flow { - direction: RouteDirection::Forward(cluster_id), - .. - } = &route - { - time!( - "frontend_matching_time", - cluster_id, - (now - start).as_millis() - ); - } - - Ok(route) + }) } } diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs index 67fb96fdb..dcac5fc0c 100644 --- a/lib/src/protocol/kawa_h1/mod.rs +++ b/lib/src/protocol/kawa_h1/mod.rs @@ -8,10 +8,12 @@ use std::{ io::ErrorKind, net::{Shutdown, SocketAddr}, rc::{Rc, Weak}, + str::from_utf8_unchecked, time::{Duration, Instant}, }; use mio::{net::TcpStream, Interest, Token}; +use parser::hostname_and_port; use rusty_ulid::Ulid; use sozu_command::{ config::MAX_LOOP_ITERATIONS, @@ -40,8 +42,9 @@ use crate::{ sozu_command::{logging::LogContext, ready::Ready}, timer::TimeoutContainer, AcceptError, BackendConnectAction, BackendConnectionError, BackendConnectionStatus, - L7ListenerHandler, L7Proxy, ListenerHandler, Protocol, ProxySession, Readiness, - RetrieveClusterError, SessionIsToBeClosed, SessionMetrics, SessionResult, StateResult, + FrontendFromRequestError, L7ListenerHandler, L7Proxy, ListenerHandler, Protocol, ProxySession, + Readiness, RetrieveClusterError, SessionIsToBeClosed, SessionMetrics, SessionResult, + StateResult, }; /// This macro is defined uniquely in this module to help the tracking of kawa h1 @@ -1257,7 +1260,7 @@ impl Http { self.set_answer(DefaultAnswer::Answer400 { message: "Could not extract the route after connection started, this should not happen.".into(), - phase: self.request_stream.parsing_phase.marker(), + phase: kawa::ParsingPhaseMarker::StatusLine, successfully_parsed: "null".into(), partially_parsed: "null".into(), invalid: "null".into(), @@ -1266,6 +1269,38 @@ impl Http (unsafe { from_utf8_unchecked(hostname) }, port), + Ok(_) => { + let host = host.to_owned(); + self.set_answer(DefaultAnswer::Answer400 { + message: "Invalid characters after hostname, this should not happen.".into(), + phase: kawa::ParsingPhaseMarker::StatusLine, + successfully_parsed: "null".into(), + partially_parsed: "null".into(), + invalid: "null".into(), + }); + return Err(RetrieveClusterError::RetrieveFrontend( + FrontendFromRequestError::InvalidCharsAfterHost(host), + )); + } + Err(parse_error) => { + let host = host.to_owned(); + let error = parse_error.to_string(); + self.set_answer(DefaultAnswer::Answer400 { + message: "Could not parse port from hostname, this should not happen.".into(), + phase: kawa::ParsingPhaseMarker::StatusLine, + successfully_parsed: "null".into(), + partially_parsed: "null".into(), + invalid: "null".into(), + }); + return Err(RetrieveClusterError::RetrieveFrontend( + FrontendFromRequestError::HostParse { host, error }, + )); + } + }; + + let start = Instant::now(); let route_result = self .listener .borrow() @@ -1288,25 +1323,43 @@ impl Http { let is_https = matches!(proxy.borrow().kind(), ListenerType::Https); if let RouteDirection::Forward(cluster_id) = &flow { - if !is_https - && proxy - .borrow() - .clusters() - .get(cluster_id) - .map(|cluster| cluster.https_redirect) - .unwrap_or(false) - { + time!( + "frontend_matching_time", + cluster_id, + start.elapsed().as_millis() + ); + let (https_redirect, https_redirect_port, authentication) = proxy + .borrow() + .clusters() + .get(cluster_id) + .map(|cluster| (cluster.https_redirect, Some(8443), None::<()>)) + .unwrap_or((false, None, None)); + if !is_https && https_redirect { + let port = + https_redirect_port.map_or(String::new(), |port| format!(":{port}")); self.set_answer(DefaultAnswer::Answer301 { - location: format!("https://{host}{path}"), + location: format!("https://{host}{port}{path}"), }); return Err(RetrieveClusterError::Redirected); } + if let Some(authentication) = authentication { + return Err(RetrieveClusterError::UnauthorizedRoute); + } } let host = rewritten_host.as_deref().unwrap_or(host); let path = rewritten_path.as_deref().unwrap_or(path); + let port = rewritten_port.map_or_else( + || { + port.map_or(String::new(), |port| { + format!(":{}", unsafe { from_utf8_unchecked(port) }) + }) + }, + |port| format!(":{port}"), + ); match flow { RouteDirection::Forward(cluster_id) => Ok(cluster_id), RouteDirection::Permanent(redirect_scheme) => { @@ -1319,7 +1372,7 @@ impl Http, rewrite_path: Option, - // rewrite_port? + rewrite_port: Option, }, } @@ -747,6 +748,7 @@ impl Route { redirect_scheme: RedirectScheme, rewrite_host: Option, rewrite_path: Option, + rewrite_port: Option, ) -> Result { let flow = match (cluster_id, redirect) { (Some(cluster_id), RedirectPolicy::Forward) => RouteDirection::Forward(cluster_id), @@ -807,6 +809,7 @@ impl Route { capture_cap_path, rewrite_host, rewrite_path, + rewrite_port, }) } @@ -818,6 +821,7 @@ impl Route { capture_cap_path: 0, rewrite_host: None, rewrite_path: None, + rewrite_port: None, } } } @@ -829,7 +833,7 @@ pub enum RouteResult { direction: RouteDirection, rewritten_host: Option, rewritten_path: Option, - // rewritten_port? + rewritten_port: Option, }, } @@ -847,6 +851,7 @@ impl RouteResult { capture_cap_path, rewrite_host, rewrite_path, + rewrite_port, .. } => { let mut captures_path = Vec::with_capacity(*capture_cap_path); @@ -875,6 +880,7 @@ impl RouteResult { rewritten_path: rewrite_path .as_ref() .map(|rewrite| rewrite.run(&captures_host, &captures_path)), + rewritten_port: *rewrite_port, } } } @@ -955,6 +961,7 @@ impl RouteResult { direction: RouteDirection::Forward(cluster_id), rewritten_host: None, rewritten_path: None, + rewritten_port: None, } } } From 41028717dc3c7ae58c2c18d93d892a2d12f70b89 Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Tue, 17 Dec 2024 12:42:07 +0100 Subject: [PATCH 05/18] Add https_redirect_port on clusters Signed-off-by: Eloi DEMOLIS --- command/src/command.proto | 1 + command/src/config.rs | 5 +++++ lib/src/protocol/kawa_h1/mod.rs | 12 +++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/command/src/command.proto b/command/src/command.proto index 7ce05681a..8622f199b 100644 --- a/command/src/command.proto +++ b/command/src/command.proto @@ -395,6 +395,7 @@ message Cluster { required LoadBalancingAlgorithms load_balancing = 5 [default = ROUND_ROBIN]; optional string answer_503 = 6; optional LoadMetric load_metric = 7; + optional uint32 https_redirect_port = 8; } enum LoadBalancingAlgorithms { diff --git a/command/src/config.rs b/command/src/config.rs index bfd3236bf..3707bd9c5 100644 --- a/command/src/config.rs +++ b/command/src/config.rs @@ -789,6 +789,7 @@ pub struct FileClusterConfig { pub protocol: FileClusterProtocolConfig, pub sticky_session: Option, pub https_redirect: Option, + pub https_redirect_port: Option, #[serde(default)] pub send_proxy: Option, #[serde(default)] @@ -888,6 +889,7 @@ impl FileClusterConfig { backends: self.backends, sticky_session: self.sticky_session.unwrap_or(false), https_redirect: self.https_redirect.unwrap_or(false), + https_redirect_port: self.https_redirect_port, load_balancing: self.load_balancing, load_metric: self.load_metric, answer_503, @@ -995,6 +997,7 @@ pub struct HttpClusterConfig { pub backends: Vec, pub sticky_session: bool, pub https_redirect: bool, + pub https_redirect_port: Option, pub load_balancing: LoadBalancingAlgorithms, pub load_metric: Option, pub answer_503: Option, @@ -1006,6 +1009,7 @@ impl HttpClusterConfig { cluster_id: self.cluster_id.clone(), sticky_session: self.sticky_session, https_redirect: self.https_redirect, + https_redirect_port: self.https_redirect_port.map(|s| s as u32), proxy_protocol: None, load_balancing: self.load_balancing as i32, answer_503: self.answer_503.clone(), @@ -1065,6 +1069,7 @@ impl TcpClusterConfig { cluster_id: self.cluster_id.clone(), sticky_session: false, https_redirect: false, + https_redirect_port: None, proxy_protocol: self.proxy_protocol.map(|s| s as i32), load_balancing: self.load_balancing as i32, load_metric: self.load_metric.map(|s| s as i32), diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs index dcac5fc0c..189d4c688 100644 --- a/lib/src/protocol/kawa_h1/mod.rs +++ b/lib/src/protocol/kawa_h1/mod.rs @@ -1336,11 +1336,17 @@ impl Http)) + .map(|cluster| { + ( + cluster.https_redirect, + cluster.https_redirect_port, + None::<()>, + ) + }) .unwrap_or((false, None, None)); if !is_https && https_redirect { - let port = - https_redirect_port.map_or(String::new(), |port| format!(":{port}")); + let port = https_redirect_port + .map_or(String::new(), |port| format!(":{}", port as u16)); self.set_answer(DefaultAnswer::Answer301 { location: format!("https://{host}{port}{path}"), }); From 663b67f347cd891e441b610e1ecda72d6b4df515 Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Tue, 17 Dec 2024 21:12:02 +0100 Subject: [PATCH 06/18] Rewrite template providing to allow custom ones Signed-off-by: Eloi DEMOLIS --- bin/src/ctl/request_builder.rs | 10 +- command/assets/config.toml | 4 +- command/src/command.proto | 45 +--- command/src/config.rs | 155 +++++--------- command/src/proto/display.rs | 55 +---- command/src/request.rs | 7 +- command/src/response.rs | 5 +- command/src/state.rs | 16 +- e2e/src/tests/tests.rs | 27 ++- lib/examples/http.rs | 1 - lib/src/http.rs | 28 +-- lib/src/https.rs | 27 +-- lib/src/lib.rs | 6 +- lib/src/protocol/kawa_h1/answers.rs | 311 ++++++++++++++-------------- lib/src/protocol/kawa_h1/mod.rs | 139 ++++++------- lib/src/router/mod.rs | 14 +- 16 files changed, 371 insertions(+), 479 deletions(-) diff --git a/bin/src/ctl/request_builder.rs b/bin/src/ctl/request_builder.rs index a4094aff6..dd7764b5e 100644 --- a/bin/src/ctl/request_builder.rs +++ b/bin/src/ctl/request_builder.rs @@ -252,6 +252,7 @@ impl CommandManager { }, redirect: todo!(), redirect_scheme: todo!(), + redirect_template: todo!(), rewrite_host: todo!(), rewrite_path: todo!(), rewrite_port: todo!(), @@ -305,6 +306,7 @@ impl CommandManager { }, redirect: todo!(), redirect_scheme: todo!(), + redirect_template: todo!(), rewrite_host: todo!(), rewrite_path: todo!(), rewrite_port: todo!(), @@ -351,8 +353,8 @@ impl CommandManager { } => { let https_listener = ListenerBuilder::new_https(address.into()) .with_public_address(public_address) - .with_answer_404_path(answer_404) - .with_answer_503_path(answer_503) + .with_answer("404", answer_404) + .with_answer("503", answer_503) .with_tls_versions(tls_versions) .with_cipher_list(cipher_list) .with_expect_proxy(expect_proxy) @@ -394,8 +396,8 @@ impl CommandManager { } => { let http_listener = ListenerBuilder::new_http(address.into()) .with_public_address(public_address) - .with_answer_404_path(answer_404) - .with_answer_503_path(answer_503) + .with_answer("404", answer_404) + .with_answer("503", answer_503) .with_expect_proxy(expect_proxy) .with_sticky_name(sticky_name) .with_front_timeout(front_timeout) diff --git a/command/assets/config.toml b/command/assets/config.toml index 3d48b60cd..8675c58a1 100644 --- a/command/assets/config.toml +++ b/command/assets/config.toml @@ -17,7 +17,7 @@ protocol = "http" [[listeners]] address = "0.0.0.0:443" protocol = "https" -answer_404 = "./assets/custom_404.html" +answers = { "404" = "./assets/custom_404.html" } tls_versions = ["TLS_V12"] [[listeners]] @@ -28,7 +28,7 @@ expect_proxy = true [clusters] [clusters.MyCluster] protocol = "http" -answer_503 = "./assets/custom_503.html" +answers = { "503" = "./assets/custom_503.html" } #sticky_session = false #https_redirect = false frontends = [ diff --git a/command/src/command.proto b/command/src/command.proto index 8622f199b..d89a60275 100644 --- a/command/src/command.proto +++ b/command/src/command.proto @@ -129,7 +129,7 @@ message HttpListenerConfig { required uint32 request_timeout = 10 [default = 10]; // wether the listener is actively listening on its socket required bool active = 11 [default = false]; - optional CustomHttpAnswers http_answers = 12; + map answers = 13; } // details of an HTTPS listener @@ -161,7 +161,7 @@ message HttpsListenerConfig { // The tickets allow the client to resume a session. This protects the client // agains session tracking. Defaults to 4. required uint64 send_tls13_tickets = 20; - optional CustomHttpAnswers http_answers = 21; + map answers = 22; } // details of an TCP listener @@ -179,31 +179,6 @@ message TcpListenerConfig { required bool active = 7 [default = false]; } -// custom HTTP answers, useful for 404, 503 pages -message CustomHttpAnswers { - // MovedPermanently - optional string answer_301 = 1; - // BadRequest - optional string answer_400 = 2; - // Unauthorized - optional string answer_401 = 3; - // NotFound - optional string answer_404 = 4; - // RequestTimeout - optional string answer_408 = 5; - // PayloadTooLarge - optional string answer_413 = 6; - // BadGateway - optional string answer_502 = 7; - // ServiceUnavailable - optional string answer_503 = 8; - // GatewayTimeout - optional string answer_504 = 9; - // InsufficientStorage - optional string answer_507 = 10; - -} - message ActivateListener { required SocketAddress address = 1; required ListenerType proxy = 2; @@ -239,10 +214,9 @@ message ListenersList { enum RedirectPolicy { FORWARD = 0; - FORCE_HTTPS = 1; - TEMPORARY = 2; - PERMANENT = 3; - UNAUTHORIZED = 4; + TEMPORARY = 1; + PERMANENT = 2; + UNAUTHORIZED = 3; } enum RedirectScheme { @@ -263,9 +237,10 @@ message RequestHttpFrontend { map tags = 7; optional RedirectPolicy redirect = 8; optional RedirectScheme redirect_scheme = 9; - optional string rewrite_host = 10; - optional string rewrite_path = 11; - optional uint32 rewrite_port = 12; + optional string redirect_template = 10; + optional string rewrite_host = 11; + optional string rewrite_path = 12; + optional uint32 rewrite_port = 13; } message RequestTcpFrontend { @@ -393,9 +368,9 @@ message Cluster { required bool https_redirect = 3; optional ProxyProtocolConfig proxy_protocol = 4; required LoadBalancingAlgorithms load_balancing = 5 [default = ROUND_ROBIN]; - optional string answer_503 = 6; optional LoadMetric load_metric = 7; optional uint32 https_redirect_port = 8; + map answers = 9; } enum LoadBalancingAlgorithms { diff --git a/command/src/config.rs b/command/src/config.rs index 3707bd9c5..b4027fe7d 100644 --- a/command/src/config.rs +++ b/command/src/config.rs @@ -51,7 +51,7 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, env, fmt, fs::{create_dir_all, metadata, File}, - io::{ErrorKind, Read}, + io::ErrorKind, net::SocketAddr, ops::Range, path::PathBuf, @@ -62,11 +62,11 @@ use crate::{ logging::AccessLogFormat, proto::command::{ request::RequestType, ActivateListener, AddBackend, AddCertificate, CertificateAndKey, - Cluster, CustomHttpAnswers, HttpListenerConfig, HttpsListenerConfig, ListenerType, - LoadBalancingAlgorithms, LoadBalancingParams, LoadMetric, MetricsConfiguration, PathRule, - ProtobufAccessLogFormat, ProxyProtocolConfig, RedirectPolicy, RedirectScheme, Request, - RequestHttpFrontend, RequestTcpFrontend, RulePosition, ServerConfig, ServerMetricsConfig, - SocketAddress, TcpListenerConfig, TlsVersion, WorkerRequest, + Cluster, HttpListenerConfig, HttpsListenerConfig, ListenerType, LoadBalancingAlgorithms, + LoadBalancingParams, LoadMetric, MetricsConfiguration, PathRule, ProtobufAccessLogFormat, + ProxyProtocolConfig, RedirectPolicy, RedirectScheme, Request, RequestHttpFrontend, + RequestTcpFrontend, RulePosition, ServerConfig, ServerMetricsConfig, SocketAddress, + TcpListenerConfig, TlsVersion, WorkerRequest, }, ObjectKind, }; @@ -240,16 +240,7 @@ pub struct ListenerBuilder { pub address: SocketAddr, pub protocol: Option, pub public_address: Option, - pub answer_301: Option, - pub answer_400: Option, - pub answer_401: Option, - pub answer_404: Option, - pub answer_408: Option, - pub answer_413: Option, - pub answer_502: Option, - pub answer_503: Option, - pub answer_504: Option, - pub answer_507: Option, + pub answers: Option>, pub tls_versions: Option>, pub cipher_list: Option>, pub cipher_suites: Option>, @@ -279,6 +270,26 @@ pub fn default_sticky_name() -> String { DEFAULT_STICKY_NAME.to_string() } +pub fn load_answers( + answers: Option<&BTreeMap>, +) -> Result, ConfigError> { + if let Some(answers) = answers { + answers + .iter() + .map(|(name, path)| match Config::load_file(path) { + Ok(content) => Ok((name.to_owned(), content)), + Err(e) => Err((name.to_owned(), path, e)), + }) + .collect::, _>>() + .map_err(|(name, path, e)| { + error!("cannot load answer {:?} at path {:?}: {:?}", name, path, e); + e + }) + } else { + Ok(BTreeMap::new()) + } +} + impl ListenerBuilder { /// starts building an HTTP Listener with config values for timeouts, /// or defaults if no config is provided @@ -302,16 +313,7 @@ impl ListenerBuilder { fn new(address: SocketAddress, protocol: ListenerProtocol) -> ListenerBuilder { ListenerBuilder { address: address.into(), - answer_301: None, - answer_401: None, - answer_400: None, - answer_404: None, - answer_408: None, - answer_413: None, - answer_502: None, - answer_503: None, - answer_504: None, - answer_507: None, + answers: None, back_timeout: None, certificate_chain: None, certificate: None, @@ -338,23 +340,22 @@ impl ListenerBuilder { self } - pub fn with_answer_404_path(&mut self, answer_404_path: Option) -> &mut Self + pub fn with_answer(&mut self, name: S, path: Option) -> &mut Self where S: ToString, { - if let Some(path) = answer_404_path { - self.answer_404 = Some(path.to_string()); + if let Some(path) = path { + self.answers + .get_or_insert_with(BTreeMap::new) + .insert(name.to_string(), path); } self } - pub fn with_answer_503_path(&mut self, answer_503_path: Option) -> &mut Self - where - S: ToString, - { - if let Some(path) = answer_503_path { - self.answer_503 = Some(path.to_string()); - } + pub fn with_answers(&mut self, mut answers: BTreeMap) -> &mut Self { + self.answers + .get_or_insert_with(BTreeMap::new) + .append(&mut answers); self } @@ -429,23 +430,6 @@ impl ListenerBuilder { self } - /// Get the custom HTTP answers from the file system using the provided paths - fn get_http_answers(&self) -> Result, ConfigError> { - let http_answers = CustomHttpAnswers { - answer_301: read_http_answer_file(&self.answer_301)?, - answer_400: read_http_answer_file(&self.answer_400)?, - answer_401: read_http_answer_file(&self.answer_401)?, - answer_404: read_http_answer_file(&self.answer_404)?, - answer_408: read_http_answer_file(&self.answer_408)?, - answer_413: read_http_answer_file(&self.answer_413)?, - answer_502: read_http_answer_file(&self.answer_502)?, - answer_503: read_http_answer_file(&self.answer_503)?, - answer_504: read_http_answer_file(&self.answer_504)?, - answer_507: read_http_answer_file(&self.answer_507)?, - }; - Ok(Some(http_answers)) - } - /// Assign the timeouts of the config to this listener, only if timeouts did not exist fn assign_config_timeouts(&mut self, config: &Config) { self.front_timeout = Some(self.front_timeout.unwrap_or(config.front_timeout)); @@ -467,8 +451,6 @@ impl ListenerBuilder { self.assign_config_timeouts(config); } - let http_answers = self.get_http_answers()?; - let configuration = HttpListenerConfig { address: self.address.into(), public_address: self.public_address.map(|a| a.into()), @@ -478,7 +460,7 @@ impl ListenerBuilder { back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT), connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT), request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT), - http_answers, + answers: load_answers(self.answers.as_ref())?, ..Default::default() }; @@ -550,8 +532,6 @@ impl ListenerBuilder { .map(split_certificate_chain) .unwrap_or_default(); - let http_answers = self.get_http_answers()?; - if let Some(config) = config { self.assign_config_timeouts(config); } @@ -577,7 +557,7 @@ impl ListenerBuilder { send_tls13_tickets: self .send_tls13_tickets .unwrap_or(DEFAULT_SEND_TLS_13_TICKETS), - http_answers, + answers: load_answers(self.answers.as_ref())?, }; Ok(https_listener_config) @@ -608,28 +588,6 @@ impl ListenerBuilder { } } -/// read a custom HTTP answer from a file -fn read_http_answer_file(path: &Option) -> Result, ConfigError> { - match path { - Some(path) => { - let mut content = String::new(); - let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen { - path_to_open: path.to_owned(), - io_error, - })?; - - file.read_to_string(&mut content) - .map_err(|io_error| ConfigError::FileRead { - path_to_read: path.to_owned(), - io_error, - })?; - - Ok(Some(content)) - } - None => Ok(None), - } -} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MetricsConfig { @@ -669,6 +627,7 @@ pub struct FileClusterFrontendConfig { pub tags: Option>, pub redirect: Option, pub redirect_scheme: Option, + pub redirect_template: Option, pub rewrite_host: Option, pub rewrite_path: Option, pub rewrite_port: Option, @@ -759,6 +718,7 @@ impl FileClusterFrontendConfig { tags: self.tags.clone(), redirect: self.redirect, redirect_scheme: self.redirect_scheme, + redirect_template: self.redirect_template.clone(), rewrite_host: self.rewrite_host.clone(), rewrite_path: self.rewrite_path.clone(), rewrite_port: self.rewrite_port.clone(), @@ -781,7 +741,7 @@ pub enum FileClusterProtocolConfig { Tcp, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct FileClusterConfig { pub frontends: Vec, @@ -794,9 +754,10 @@ pub struct FileClusterConfig { pub send_proxy: Option, #[serde(default)] pub load_balancing: LoadBalancingAlgorithms, - pub answer_503: Option, #[serde(default)] pub load_metric: Option, + #[serde(default)] + pub answers: Option>, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -874,15 +835,6 @@ impl FileClusterConfig { frontends.push(http_frontend); } - let answer_503 = self.answer_503.as_ref().and_then(|path| { - Config::load_file(path) - .map_err(|e| { - error!("cannot load 503 error page at path '{}': {:?}", path, e); - e - }) - .ok() - }); - Ok(ClusterConfig::Http(HttpClusterConfig { cluster_id: cluster_id.to_string(), frontends, @@ -892,7 +844,7 @@ impl FileClusterConfig { https_redirect_port: self.https_redirect_port, load_balancing: self.load_balancing, load_metric: self.load_metric, - answer_503, + answers: load_answers(self.answers.as_ref())?, })) } } @@ -916,6 +868,7 @@ pub struct HttpFrontendConfig { pub tags: Option>, pub redirect: Option, pub redirect_scheme: Option, + pub redirect_template: Option, pub rewrite_host: Option, pub rewrite_path: Option, pub rewrite_port: Option, @@ -958,6 +911,7 @@ impl HttpFrontendConfig { tags, redirect: self.redirect.map(Into::into), redirect_scheme: self.redirect_scheme.map(Into::into), + redirect_template: self.redirect_template.clone(), rewrite_host: self.rewrite_host.clone(), rewrite_path: self.rewrite_path.clone(), rewrite_port: self.rewrite_port.map(|x| x as u32), @@ -977,6 +931,7 @@ impl HttpFrontendConfig { tags, redirect: self.redirect.map(Into::into), redirect_scheme: self.redirect_scheme.map(Into::into), + redirect_template: self.redirect_template.clone(), rewrite_host: self.rewrite_host.clone(), rewrite_path: self.rewrite_path.clone(), rewrite_port: self.rewrite_port.map(|x| x as u32), @@ -989,7 +944,7 @@ impl HttpFrontendConfig { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct HttpClusterConfig { pub cluster_id: String, @@ -1000,7 +955,7 @@ pub struct HttpClusterConfig { pub https_redirect_port: Option, pub load_balancing: LoadBalancingAlgorithms, pub load_metric: Option, - pub answer_503: Option, + pub answers: BTreeMap, } impl HttpClusterConfig { @@ -1012,8 +967,8 @@ impl HttpClusterConfig { https_redirect_port: self.https_redirect_port.map(|s| s as u32), proxy_protocol: None, load_balancing: self.load_balancing as i32, - answer_503: self.answer_503.clone(), load_metric: self.load_metric.map(|s| s as i32), + answers: self.answers.clone(), }) .into()]; @@ -1073,7 +1028,7 @@ impl TcpClusterConfig { proxy_protocol: self.proxy_protocol.map(|s| s as i32), load_balancing: self.load_balancing as i32, load_metric: self.load_metric.map(|s| s as i32), - answer_503: None, + answers: Default::default(), }) .into()]; @@ -1112,7 +1067,7 @@ impl TcpClusterConfig { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ClusterConfig { Http(HttpClusterConfig), Tcp(TcpClusterConfig), @@ -1898,7 +1853,7 @@ mod tests { SocketAddress::new_v4(127, 0, 0, 1, 8080), ListenerProtocol::Http, ) - .with_answer_404_path(Some("404.html")) + .with_answer(404, Some("404.html".to_string())) .to_owned(); println!("http: {:?}", to_string(&http)); @@ -1906,7 +1861,7 @@ mod tests { SocketAddress::new_v4(127, 0, 0, 1, 8443), ListenerProtocol::Https, ) - .with_answer_404_path(Some("404.html")) + .with_answer(404, Some("404.html".to_string())) .to_owned(); println!("https: {:?}", to_string(&https)); diff --git a/command/src/proto/display.rs b/command/src/proto/display.rs index 9331de3a7..b2701c2fb 100644 --- a/command/src/proto/display.rs +++ b/command/src/proto/display.rs @@ -13,12 +13,11 @@ use crate::{ command::{ filtered_metrics, protobuf_endpoint, request::RequestType, response_content::ContentType, AggregatedMetrics, AvailableMetrics, CertificateAndKey, - CertificateSummary, CertificatesWithFingerprints, ClusterMetrics, CustomHttpAnswers, - Event, EventKind, FilteredMetrics, HttpEndpoint, HttpListenerConfig, - HttpsListenerConfig, ListOfCertificatesByAddress, ListedFrontends, ListenersList, - ProtobufEndpoint, QueryCertificatesFilters, RequestCounts, Response, ResponseContent, - ResponseStatus, RunState, SocketAddress, TlsVersion, WorkerInfos, WorkerMetrics, - WorkerResponses, + CertificateSummary, CertificatesWithFingerprints, ClusterMetrics, Event, EventKind, + FilteredMetrics, HttpEndpoint, HttpListenerConfig, HttpsListenerConfig, + ListOfCertificatesByAddress, ListedFrontends, ListenersList, ProtobufEndpoint, + QueryCertificatesFilters, RequestCounts, Response, ResponseContent, ResponseStatus, + RunState, SocketAddress, TlsVersion, WorkerInfos, WorkerMetrics, WorkerResponses, }, DisplayError, }, @@ -1011,8 +1010,8 @@ impl Display for HttpListenerConfig { table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); table.add_row(row!["socket address", format!("{:?}", self.address)]); table.add_row(row!["public address", format!("{:?}", self.public_address),]); - for http_answer_row in CustomHttpAnswers::to_rows(&self.http_answers) { - table.add_row(http_answer_row); + for (name, content) in &self.answers { + table.add_row(row![format!("answer({name})"), content]); } table.add_row(row!["expect proxy", self.expect_proxy]); table.add_row(row!["sticky name", self.sticky_name]); @@ -1036,8 +1035,8 @@ impl Display for HttpsListenerConfig { table.add_row(row!["socket address", format!("{:?}", self.address)]); table.add_row(row!["public address", format!("{:?}", self.public_address)]); - for http_answer_row in CustomHttpAnswers::to_rows(&self.http_answers) { - table.add_row(http_answer_row); + for (name, content) in &self.answers { + table.add_row(row![format!("answer({name})"), content]); } table.add_row(row!["versions", tls_versions]); table.add_row(row!["cipher list", list_string_vec(&self.cipher_list),]); @@ -1059,42 +1058,6 @@ impl Display for HttpsListenerConfig { } } -impl CustomHttpAnswers { - fn to_rows(option: &Option) -> Vec { - let mut rows = Vec::new(); - if let Some(answers) = option { - if let Some(a) = &answers.answer_301 { - rows.push(row!("301", a)); - } - if let Some(a) = &answers.answer_400 { - rows.push(row!("400", a)); - } - if let Some(a) = &answers.answer_404 { - rows.push(row!("404", a)); - } - if let Some(a) = &answers.answer_408 { - rows.push(row!("408", a)); - } - if let Some(a) = &answers.answer_413 { - rows.push(row!("413", a)); - } - if let Some(a) = &answers.answer_502 { - rows.push(row!("502", a)); - } - if let Some(a) = &answers.answer_503 { - rows.push(row!("503", a)); - } - if let Some(a) = &answers.answer_504 { - rows.push(row!("504", a)); - } - if let Some(a) = &answers.answer_507 { - rows.push(row!("507", a)); - } - } - rows - } -} - impl Display for Event { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let kind = match self.kind() { diff --git a/command/src/request.rs b/command/src/request.rs index 2dd91a5d0..f40938ae8 100644 --- a/command/src/request.rs +++ b/command/src/request.rs @@ -163,15 +163,16 @@ impl RequestHttpFrontend { position: self.position(), redirect: self.redirect(), redirect_scheme: self.redirect_scheme(), + redirect_template: self.redirect_template, + rewrite_host: self.rewrite_host, + rewrite_path: self.rewrite_path, + rewrite_port: self.rewrite_port.map(|x| x as u16), address: self.address.into(), cluster_id: self.cluster_id, hostname: self.hostname, path: self.path, method: self.method, tags: Some(self.tags), - rewrite_host: self.rewrite_host, - rewrite_path: self.rewrite_path, - rewrite_port: self.rewrite_port.map(|x| x as u16), }) } } diff --git a/command/src/response.rs b/command/src/response.rs index af91a01cf..2883b3794 100644 --- a/command/src/response.rs +++ b/command/src/response.rs @@ -38,15 +38,17 @@ pub struct HttpFrontend { pub method: Option, #[serde(default)] pub position: RulePosition, + pub tags: Option>, pub redirect: RedirectPolicy, pub redirect_scheme: RedirectScheme, #[serde(skip_serializing_if = "Option::is_none")] + pub redirect_template: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub rewrite_host: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rewrite_path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rewrite_port: Option, - pub tags: Option>, } impl From for RequestHttpFrontend { @@ -61,6 +63,7 @@ impl From for RequestHttpFrontend { tags: val.tags.unwrap_or_default(), redirect: Some(val.redirect.into()), redirect_scheme: Some(val.redirect_scheme.into()), + redirect_template: val.redirect_template, rewrite_host: val.rewrite_host, rewrite_path: val.rewrite_path, rewrite_port: val.rewrite_port.map(|x| x as u32), diff --git a/command/src/state.rs b/command/src/state.rs index 62e2156cc..9413fa203 100644 --- a/command/src/state.rs +++ b/command/src/state.rs @@ -1482,8 +1482,7 @@ mod tests { use super::*; use crate::proto::command::{ - CustomHttpAnswers, LoadBalancingParams, RedirectPolicy, RedirectScheme, - RequestHttpFrontend, RulePosition, + LoadBalancingParams, RedirectPolicy, RedirectScheme, RequestHttpFrontend, RulePosition, }; #[test] @@ -1988,10 +1987,7 @@ mod tests { #[test] fn listener_diff() { let mut state: ConfigState = Default::default(); - let custom_http_answers = Some(CustomHttpAnswers { - answer_404: Some("test".to_string()), - ..Default::default() - }); + let answers = BTreeMap::from([("404".to_string(), "test".to_string())]); state .dispatch( &RequestType::AddTcpListener(TcpListenerConfig { @@ -2055,7 +2051,7 @@ mod tests { .dispatch( &RequestType::AddHttpListener(HttpListenerConfig { address: SocketAddress::new_v4(0, 0, 0, 0, 8080), - http_answers: custom_http_answers.clone(), + answers: answers.clone(), ..Default::default() }) .into(), @@ -2075,7 +2071,7 @@ mod tests { .dispatch( &RequestType::AddHttpsListener(HttpsListenerConfig { address: SocketAddress::new_v4(0, 0, 0, 0, 8443), - http_answers: custom_http_answers.clone(), + answers: answers.clone(), ..Default::default() }) .into(), @@ -2117,7 +2113,7 @@ mod tests { .into(), RequestType::AddHttpListener(HttpListenerConfig { address: SocketAddress::new_v4(0, 0, 0, 0, 8080), - http_answers: custom_http_answers.clone(), + answers: answers.clone(), ..Default::default() }) .into(), @@ -2134,7 +2130,7 @@ mod tests { .into(), RequestType::AddHttpsListener(HttpsListenerConfig { address: SocketAddress::new_v4(0, 0, 0, 0, 8443), - http_answers: custom_http_answers.clone(), + answers: answers.clone(), ..Default::default() }) .into(), diff --git a/e2e/src/tests/tests.rs b/e2e/src/tests/tests.rs index 6bcedfc06..89727f53e 100644 --- a/e2e/src/tests/tests.rs +++ b/e2e/src/tests/tests.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeMap, net::SocketAddr, thread, time::{Duration, Instant}, @@ -10,7 +11,7 @@ use sozu_command_lib::{ logging::setup_default_logging, proto::command::{ request::RequestType, ActivateListener, AddCertificate, CertificateAndKey, Cluster, - CustomHttpAnswers, ListenerType, RemoveBackend, RequestHttpFrontend, SocketAddress, + ListenerType, RemoveBackend, RequestHttpFrontend, SocketAddress, }, scm_socket::Listeners, state::ConfigState, @@ -643,14 +644,12 @@ fn try_http_behaviors() -> State { let mut http_config = ListenerBuilder::new_http(front_address.into()) .to_http(None) .unwrap(); - let http_answers = CustomHttpAnswers { - answer_400: Some(immutable_answer(400)), - answer_404: Some(immutable_answer(404)), - answer_502: Some(immutable_answer(502)), - answer_503: Some(immutable_answer(503)), - ..Default::default() - }; - http_config.http_answers = Some(http_answers); + http_config.answers = BTreeMap::from([ + ("400".to_string(), immutable_answer(400)), + ("404".to_string(), immutable_answer(404)), + ("502".to_string(), immutable_answer(502)), + ("503".to_string(), immutable_answer(503)), + ]); worker.send_proxy_request_type(RequestType::AddHttpListener(http_config)); worker.send_proxy_request_type(RequestType::ActivateListener(ActivateListener { @@ -951,12 +950,10 @@ fn try_https_redirect() -> State { .to_http(None) .unwrap(); let answer_301_prefix = "HTTP/1.1 301 Moved Permanently\r\nLocation: "; - - let http_answers = CustomHttpAnswers { - answer_301: Some(format!("{answer_301_prefix}%REDIRECT_LOCATION\r\n\r\n")), - ..Default::default() - }; - http_config.http_answers = Some(http_answers); + http_config.answers = BTreeMap::from([( + "301".to_string(), + format!("{answer_301_prefix}%REDIRECT_LOCATION\r\n\r\n"), + )]); worker.send_proxy_request_type(RequestType::AddHttpListener(http_config)); worker.send_proxy_request_type(RequestType::ActivateListener(ActivateListener { diff --git a/lib/examples/http.rs b/lib/examples/http.rs index 68ec35a03..7ecbc295e 100644 --- a/lib/examples/http.rs +++ b/lib/examples/http.rs @@ -45,7 +45,6 @@ fn main() -> anyhow::Result<()> { sticky_session: false, https_redirect: false, load_balancing: LoadBalancingAlgorithms::RoundRobin as i32, - answer_503: Some("A custom forbidden message".to_string()), ..Default::default() }; diff --git a/lib/src/http.rs b/lib/src/http.rs index f8197568f..37c388605 100644 --- a/lib/src/http.rs +++ b/lib/src/http.rs @@ -2,6 +2,7 @@ use std::{ cell::RefCell, collections::{hash_map::Entry, BTreeMap, HashMap}, io::ErrorKind, + mem, net::{Shutdown, SocketAddr}, os::unix::io::AsRawFd, rc::{Rc, Weak}, @@ -556,18 +557,19 @@ impl HttpProxy { } pub fn add_cluster(&mut self, mut cluster: Cluster) -> Result<(), ProxyError> { - if let Some(answer_503) = cluster.answer_503.take() { + if !cluster.answers.is_empty() { for listener in self.listeners.values() { listener .borrow() .answers .borrow_mut() - .add_custom_answer(&cluster.cluster_id, answer_503.clone()) - .map_err(|(status, error)| { - ProxyError::AddCluster(ListenerError::TemplateParse(status, error)) + .add_cluster_answers(&cluster.cluster_id, &cluster.answers) + .map_err(|(name, error)| { + ProxyError::AddCluster(ListenerError::TemplateParse(name, error)) })?; } } + cluster.answers.clear(); self.clusters.insert(cluster.cluster_id.clone(), cluster); Ok(()) } @@ -580,7 +582,7 @@ impl HttpProxy { .borrow() .answers .borrow_mut() - .remove_custom_answer(cluster_id); + .remove_cluster_answers(cluster_id); } Ok(()) } @@ -688,8 +690,8 @@ impl HttpListener { active: false, address: config.address.into(), answers: Rc::new(RefCell::new( - HttpAnswers::new(&config.http_answers) - .map_err(|(status, error)| ListenerError::TemplateParse(status, error))?, + HttpAnswers::new(&config.answers) + .map_err(|(name, error)| ListenerError::TemplateParse(name, error))?, )), config, fronts: Router::new(), @@ -1015,9 +1017,7 @@ mod tests { use super::testing::start_http_worker; use super::*; - use sozu_command::proto::command::{ - CustomHttpAnswers, RedirectPolicy, RedirectScheme, SocketAddress, - }; + use sozu_command::proto::command::{RedirectPolicy, RedirectScheme, SocketAddress}; use crate::sozu_command::{ channel::Channel, @@ -1292,6 +1292,7 @@ mod tests { cluster_id: Some(cluster_id1), redirect: RedirectPolicy::Forward, redirect_scheme: RedirectScheme::UseSame, + redirect_template: None, rewrite_host: None, rewrite_path: None, rewrite_port: None, @@ -1308,6 +1309,7 @@ mod tests { cluster_id: Some(cluster_id2), redirect: RedirectPolicy::Forward, redirect_scheme: RedirectScheme::UseSame, + redirect_template: None, rewrite_host: None, rewrite_path: None, rewrite_port: None, @@ -1324,6 +1326,7 @@ mod tests { cluster_id: Some(cluster_id3), redirect: RedirectPolicy::Forward, redirect_scheme: RedirectScheme::UseSame, + redirect_template: None, rewrite_host: None, rewrite_path: None, rewrite_port: None, @@ -1340,6 +1343,7 @@ mod tests { cluster_id: Some("cluster_1".to_owned()), redirect: RedirectPolicy::Forward, redirect_scheme: RedirectScheme::UseSame, + redirect_template: None, rewrite_host: None, rewrite_path: None, rewrite_port: None, @@ -1357,9 +1361,7 @@ mod tests { listener: None, address: address.into(), fronts, - answers: Rc::new(RefCell::new( - HttpAnswers::new(&Some(CustomHttpAnswers::default())).unwrap(), - )), + answers: Rc::new(RefCell::new(HttpAnswers::new(&BTreeMap::new()).unwrap())), config: default_config, token: Token(0), active: true, diff --git a/lib/src/https.rs b/lib/src/https.rs index 8047175ae..b1501c15b 100644 --- a/lib/src/https.rs +++ b/lib/src/https.rs @@ -593,10 +593,9 @@ impl HttpsListener { rustls_details: server_config, active: false, fronts: Router::new(), - answers: Rc::new(RefCell::new( - HttpAnswers::new(&config.http_answers) - .map_err(|(status, error)| ListenerError::TemplateParse(status, error))?, - )), + answers: Rc::new(RefCell::new(HttpAnswers::new(&config.answers).map_err( + |(status, error)| ListenerError::TemplateParse(status, error), + )?)), config, token, tags: BTreeMap::new(), @@ -966,18 +965,19 @@ impl HttpsProxy { &mut self, mut cluster: Cluster, ) -> Result, ProxyError> { - if let Some(answer_503) = cluster.answer_503.take() { + if !cluster.answers.is_empty() { for listener in self.listeners.values() { listener .borrow() .answers .borrow_mut() - .add_custom_answer(&cluster.cluster_id, answer_503.clone()) - .map_err(|(status, error)| { - ProxyError::AddCluster(ListenerError::TemplateParse(status, error)) + .add_cluster_answers(&cluster.cluster_id, &cluster.answers) + .map_err(|(name, error)| { + ProxyError::AddCluster(ListenerError::TemplateParse(name, error)) })?; } } + cluster.answers.clear(); self.clusters.insert(cluster.cluster_id.clone(), cluster); Ok(None) } @@ -992,7 +992,7 @@ impl HttpsProxy { .borrow() .answers .borrow_mut() - .remove_custom_answer(cluster_id); + .remove_cluster_answers(cluster_id); } Ok(None) @@ -1473,10 +1473,7 @@ mod tests { use std::sync::Arc; - use sozu_command::{ - config::ListenerBuilder, - proto::command::{CustomHttpAnswers, SocketAddress}, - }; + use sozu_command::{config::ListenerBuilder, proto::command::SocketAddress}; use crate::router::{pattern_trie::TrieNode, MethodRule, PathRule, Route, Router}; @@ -1557,9 +1554,7 @@ mod tests { fronts, rustls_details, resolver, - answers: Rc::new(RefCell::new( - HttpAnswers::new(&Some(CustomHttpAnswers::default())).unwrap(), - )), + answers: Rc::new(RefCell::new(HttpAnswers::new(&BTreeMap::new()).unwrap())), config: default_config, token: Token(0), active: true, diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 4564237f4..311e83f16 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -106,7 +106,6 @@ //! sticky_session: false, //! https_redirect: false, //! load_balancing: LoadBalancingAlgorithms::RoundRobin as i32, -//! answer_503: Some("A custom forbidden message".to_string()), //! ..Default::default() //! }; //! ``` @@ -249,7 +248,6 @@ //! sticky_session: false, //! https_redirect: false, //! load_balancing: LoadBalancingAlgorithms::RoundRobin as i32, -//! answer_503: Some("A custom forbidden message".to_string()), //! ..Default::default() //! }; //! @@ -626,8 +624,8 @@ pub enum ListenerError { Resolver(CertificateResolverError), #[error("failed to parse pem, {0}")] PemParse(String), - #[error("failed to parse template {0}: {1}")] - TemplateParse(u16, TemplateError), + #[error("failed to parse template {0:?}: {1}")] + TemplateParse(String, TemplateError), #[error("failed to build rustls context, {0}")] BuildRustls(String), #[error("could not activate listener with address {address:?}: {error}")] diff --git a/lib/src/protocol/kawa_h1/answers.rs b/lib/src/protocol/kawa_h1/answers.rs index 06be73b56..6a8ae2b76 100644 --- a/lib/src/protocol/kawa_h1/answers.rs +++ b/lib/src/protocol/kawa_h1/answers.rs @@ -3,11 +3,12 @@ use kawa::{ h1::NoCallbacks, AsBuffer, Block, BodySize, Buffer, Chunk, Kawa, Kind, Pair, ParsingPhase, ParsingPhaseMarker, StatusLine, Store, }; -use sozu_command::proto::command::CustomHttpAnswers; +use nom::AsBytes; use std::{ - collections::{HashMap, VecDeque}, + collections::{BTreeMap, HashMap, VecDeque}, fmt, rc::Rc, + str::from_utf8_unchecked, }; #[derive(Clone)] @@ -66,6 +67,7 @@ pub struct Replacement { // TODO: rename for clarity, for instance HttpAnswerTemplate pub struct Template { + status: u16, kawa: DefaultAnswerStream, body_replacements: Vec, header_replacements: Vec, @@ -86,8 +88,8 @@ impl fmt::Debug for Template { impl Template { /// sanitize the template: transform newlines \r (CR) to \r\n (CRLF) fn new( - status: u16, - answer: String, + status: Option, + answer: &str, variables: &[TemplateVariable], ) -> Result { let mut i = 0; @@ -124,13 +126,16 @@ impl Template { if !kawa.is_main_phase() { return Err(TemplateError::InvalidTemplate(kawa.parsing_phase)); } - if let StatusLine::Response { code, .. } = kawa.detached.status_line { - if code != status { - return Err(TemplateError::InvalidStatusCode(code)); + let status = if let StatusLine::Response { code, .. } = &kawa.detached.status_line { + if let Some(expected_code) = status { + if expected_code != *code { + return Err(TemplateError::InvalidStatusCode(*code)); + } } + *code } else { return Err(TemplateError::InvalidType); - } + }; let buf = kawa.storage.buffer(); let mut blocks = VecDeque::new(); let mut header_replacements = Vec::new(); @@ -234,6 +239,7 @@ impl Template { } kawa.blocks = blocks; Ok(Self { + status, kawa, body_replacements, header_replacements, @@ -293,40 +299,10 @@ impl Template { } } -/// a set of templates for HTTP answers, meant for one listener to use -pub struct ListenerAnswers { - /// MovedPermanently - pub answer_301: Template, - /// BadRequest - pub answer_400: Template, - /// Unauthorized - pub answer_401: Template, - /// NotFound - pub answer_404: Template, - /// RequestTimeout - pub answer_408: Template, - /// PayloadTooLarge - pub answer_413: Template, - /// BadGateway - pub answer_502: Template, - /// ServiceUnavailable - pub answer_503: Template, - /// GatewayTimeout - pub answer_504: Template, - /// InsufficientStorage - pub answer_507: Template, -} - -/// templates for HTTP answers, set for one cluster -#[allow(non_snake_case)] -pub struct ClusterAnswers { - /// ServiceUnavailable - pub answer_503: Template, -} - pub struct HttpAnswers { - pub listener_answers: ListenerAnswers, // configurated answers - pub cluster_custom_answers: HashMap, + pub cluster_answers: HashMap>, + pub listener_answers: BTreeMap, + pub fallback: Template, } // const HEADERS: &str = "Connection: close\r @@ -342,6 +318,32 @@ pub struct HttpAnswers { // } // "; // const FOOTER: &str = "
This is an automatic answer by Sōzu.
"; +fn fallback() -> String { + String::from( + "\ +HTTP/1.1 404 Not Found\r +Cache-Control: no-cache\r +Connection: close\r +%Content-Length: %CONTENT_LENGTH\r +Sozu-Id: %REQUEST_ID\r +\r + + +

404 Not Found

+
+{
+    \"status_code\": 404,
+    \"route\": \"%ROUTE\",
+    \"rewritten_url\": \"%REDIRECT_LOCATION\",
+    \"request_id\": \"%REQUEST_ID\"
+    \"cluster_id\": \"%CLUSTER_ID\",
+}
+
+

A frontend requested template \"%TEMPLATE_NAME\" that couldn't be found

+
This is an automatic answer by Sōzu.
", + ) +} + fn default_301() -> String { String::from( "\ @@ -409,6 +411,7 @@ fn default_401() -> String { HTTP/1.1 401 Unauthorized\r Cache-Control: no-cache\r Connection: close\r +%Content-Length: %CONTENT_LENGTH\r Sozu-Id: %REQUEST_ID\r \r @@ -431,6 +434,7 @@ fn default_404() -> String { HTTP/1.1 404 Not Found\r Cache-Control: no-cache\r Connection: close\r +%Content-Length: %CONTENT_LENGTH\r Sozu-Id: %REQUEST_ID\r \r @@ -453,6 +457,7 @@ fn default_408() -> String { HTTP/1.1 408 Request Timeout\r Cache-Control: no-cache\r Connection: close\r +%Content-Length: %CONTENT_LENGTH\r Sozu-Id: %REQUEST_ID\r \r @@ -577,6 +582,7 @@ fn default_504() -> String { HTTP/1.1 504 Gateway Timeout\r Cache-Control: no-cache\r Connection: close\r +%Content-Length: %CONTENT_LENGTH\r Sozu-Id: %REQUEST_ID\r \r @@ -638,7 +644,7 @@ fn phase_to_vec(phase: ParsingPhaseMarker) -> Vec { impl HttpAnswers { #[rustfmt::skip] - pub fn template(status: u16, answer: String) -> Result { + pub fn template(name: &str, answer: &str) -> Result { let length = TemplateVariable { name: "CONTENT_LENGTH", valid_in_body: false, @@ -691,7 +697,7 @@ impl HttpAnswers { let location = TemplateVariable { name: "REDIRECT_LOCATION", - valid_in_body: false, + valid_in_body: true, valid_in_header: true, typ: ReplacementType::VariableOnce(0), }; @@ -719,144 +725,124 @@ impl HttpAnswers { valid_in_header: false, typ: ReplacementType::Variable(0), }; + let template_name = TemplateVariable { + name: "TEMPLATE_NAME", + valid_in_body: true, + valid_in_header: true, + typ: ReplacementType::Variable(0), + }; - match status { - 301 => Template::new( - 301, + match name { + "301" => Template::new( + Some(301), answer, &[length, route, request_id, location] ), - 400 => Template::new( - 400, + "400" => Template::new( + Some(400), answer, &[length, route, request_id, message, phase, successfully_parsed, partially_parsed, invalid], ), - 401 => Template::new( - 401, + "401" => Template::new( + Some(401), answer, &[length, route, request_id] ), - 404 => Template::new( - 404, + "404" => Template::new( + Some(404), answer, &[length, route, request_id] ), - 408 => Template::new( - 408, + "408" => Template::new( + Some(408), answer, &[length, route, request_id, duration] ), - 413 => Template::new( - 413, + "413" => Template::new( + Some(413), answer, &[length, route, request_id, capacity, message, phase], ), - 502 => Template::new( - 502, + "502" => Template::new( + Some(502), answer, &[length, route, request_id, cluster_id, backend_id, message, phase, successfully_parsed, partially_parsed, invalid], ), - 503 => Template::new( - 503, + "503" => Template::new( + Some(503), answer, &[length, route, request_id, cluster_id, backend_id, message], ), - 504 => Template::new( - 504, + "504" => Template::new( + Some(504), answer, &[length, route, request_id, cluster_id, backend_id, duration], ), - 507 => Template::new( - 507, + "507" => Template::new( + Some(507), answer, &[length, route, request_id, cluster_id, backend_id, capacity, message, phase], ), - _ => Err(TemplateError::InvalidStatusCode(status)), + _ => Template::new( + None, + answer, + &[length, route, request_id, cluster_id, location, template_name] + ) } - .map_err(|e| (status, e)) + .map_err(|e| (name.to_owned(), e)) } - pub fn new(conf: &Option) -> Result { + pub fn templates( + answers: &BTreeMap, + ) -> Result, (String, TemplateError)> { + answers + .iter() + .map(|(name, answer)| { + Self::template(name, answer).map(|template| (name.clone(), template)) + }) + .collect::>() + } + + pub fn new(answers: &BTreeMap) -> Result { + let mut listener_answers = Self::templates(answers)?; + let expected_defaults: &[(&str, fn() -> String)] = &[ + ("301", default_301), + ("400", default_400), + ("401", default_401), + ("404", default_404), + ("408", default_408), + ("413", default_413), + ("502", default_502), + ("503", default_503), + ("504", default_504), + ("507", default_507), + ]; + for (name, default) in expected_defaults { + listener_answers + .entry(name.to_string()) + .or_insert_with(|| Self::template(name, &default()).unwrap()); + } Ok(HttpAnswers { - listener_answers: ListenerAnswers { - answer_301: Self::template( - 301, - conf.as_ref() - .and_then(|c| c.answer_301.clone()) - .unwrap_or(default_301()), - )?, - answer_400: Self::template( - 400, - conf.as_ref() - .and_then(|c| c.answer_400.clone()) - .unwrap_or(default_400()), - )?, - answer_401: Self::template( - 401, - conf.as_ref() - .and_then(|c| c.answer_401.clone()) - .unwrap_or(default_401()), - )?, - answer_404: Self::template( - 404, - conf.as_ref() - .and_then(|c| c.answer_404.clone()) - .unwrap_or(default_404()), - )?, - answer_408: Self::template( - 408, - conf.as_ref() - .and_then(|c| c.answer_408.clone()) - .unwrap_or(default_408()), - )?, - answer_413: Self::template( - 413, - conf.as_ref() - .and_then(|c| c.answer_413.clone()) - .unwrap_or(default_413()), - )?, - answer_502: Self::template( - 502, - conf.as_ref() - .and_then(|c| c.answer_502.clone()) - .unwrap_or(default_502()), - )?, - answer_503: Self::template( - 503, - conf.as_ref() - .and_then(|c| c.answer_503.clone()) - .unwrap_or(default_503()), - )?, - answer_504: Self::template( - 504, - conf.as_ref() - .and_then(|c| c.answer_504.clone()) - .unwrap_or(default_504()), - )?, - answer_507: Self::template( - 507, - conf.as_ref() - .and_then(|c| c.answer_507.clone()) - .unwrap_or(default_507()), - )?, - }, - cluster_custom_answers: HashMap::new(), + fallback: Self::template("", &fallback()).unwrap(), + listener_answers, + cluster_answers: HashMap::new(), }) } - pub fn add_custom_answer( + pub fn add_cluster_answers( &mut self, cluster_id: &str, - answer_503: String, - ) -> Result<(), (u16, TemplateError)> { - let answer_503 = Self::template(503, answer_503)?; - self.cluster_custom_answers - .insert(cluster_id.to_string(), ClusterAnswers { answer_503 }); + answers: &BTreeMap, + ) -> Result<(), (String, TemplateError)> { + self.cluster_answers + .entry(cluster_id.to_owned()) + .or_default() + .append(&mut Self::templates(answers)?); Ok(()) } - pub fn remove_custom_answer(&mut self, cluster_id: &str) { - self.cluster_custom_answers.remove(cluster_id); + pub fn remove_cluster_answers(&mut self, cluster_id: &str) { + self.cluster_answers.remove(cluster_id); } pub fn get( @@ -866,14 +852,14 @@ impl HttpAnswers { cluster_id: Option<&str>, backend_id: Option<&str>, route: String, - ) -> DefaultAnswerStream { + ) -> (u16, DefaultAnswerStream) { let variables: Vec>; let mut variables_once: Vec>; - let template = match answer { + let name = match answer { DefaultAnswer::Answer301 { location } => { variables = vec![route.into(), request_id.into()]; variables_once = vec![location.into()]; - &self.listener_answers.answer_301 + "301" } DefaultAnswer::Answer400 { message, @@ -891,22 +877,22 @@ impl HttpAnswers { invalid.into(), ]; variables_once = vec![message.into()]; - &self.listener_answers.answer_400 + "400" } DefaultAnswer::Answer401 {} => { variables = vec![route.into(), request_id.into()]; variables_once = vec![]; - &self.listener_answers.answer_401 + "401" } DefaultAnswer::Answer404 {} => { variables = vec![route.into(), request_id.into()]; variables_once = vec![]; - &self.listener_answers.answer_404 + "404" } DefaultAnswer::Answer408 { duration } => { variables = vec![route.into(), request_id.into(), duration.to_string().into()]; variables_once = vec![]; - &self.listener_answers.answer_408 + "408" } DefaultAnswer::Answer413 { message, @@ -920,7 +906,7 @@ impl HttpAnswers { phase_to_vec(phase), ]; variables_once = vec![message.into()]; - &self.listener_answers.answer_413 + "413" } DefaultAnswer::Answer502 { message, @@ -940,7 +926,7 @@ impl HttpAnswers { invalid.into(), ]; variables_once = vec![message.into()]; - &self.listener_answers.answer_502 + "502" } DefaultAnswer::Answer503 { message } => { variables = vec![ @@ -950,10 +936,7 @@ impl HttpAnswers { backend_id.unwrap_or_default().into(), ]; variables_once = vec![message.into()]; - cluster_id - .and_then(|id: &str| self.cluster_custom_answers.get(id)) - .map(|c| &c.answer_503) - .unwrap_or_else(|| &self.listener_answers.answer_503) + "503" } DefaultAnswer::Answer504 { duration } => { variables = vec![ @@ -964,7 +947,7 @@ impl HttpAnswers { duration.to_string().into(), ]; variables_once = vec![]; - &self.listener_answers.answer_504 + "504" } DefaultAnswer::Answer507 { phase, @@ -980,11 +963,31 @@ impl HttpAnswers { phase_to_vec(phase), ]; variables_once = vec![message.into()]; - &self.listener_answers.answer_507 + "507" + } + DefaultAnswer::AnswerCustom { name, location, .. } => { + variables = vec![ + route.into(), + request_id.into(), + cluster_id.unwrap_or_default().into(), + name.into(), + ]; + variables_once = vec![location.into()]; + // custom_name_owner = name; + // &custom_name_owner + unsafe { &from_utf8_unchecked(variables[3].as_bytes()) } } }; // kawa::debug_kawa(&template.kawa); // println!("{template:#?}"); - template.fill(&variables, &mut variables_once) + let template = cluster_id + .and_then(|id| self.cluster_answers.get(id)) + .and_then(|answers| answers.get(name)) + .or_else(|| self.listener_answers.get(name)) + .unwrap_or(&self.fallback); + ( + template.status, + template.fill(&variables, &mut variables_once), + ) } } diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs index 189d4c688..ba0a0fcf6 100644 --- a/lib/src/protocol/kawa_h1/mod.rs +++ b/lib/src/protocol/kawa_h1/mod.rs @@ -78,6 +78,10 @@ impl kawa::AsBuffer for Checkout { #[derive(Debug, Clone, PartialEq, Eq)] pub enum DefaultAnswer { + AnswerCustom { + name: String, + location: String, + }, Answer301 { location: String, }, @@ -118,23 +122,6 @@ pub enum DefaultAnswer { }, } -impl From<&DefaultAnswer> for u16 { - fn from(answer: &DefaultAnswer) -> u16 { - match answer { - DefaultAnswer::Answer301 { .. } => 301, - DefaultAnswer::Answer400 { .. } => 400, - DefaultAnswer::Answer401 { .. } => 401, - DefaultAnswer::Answer404 { .. } => 404, - DefaultAnswer::Answer408 { .. } => 408, - DefaultAnswer::Answer413 { .. } => 413, - DefaultAnswer::Answer502 { .. } => 502, - DefaultAnswer::Answer503 { .. } => 503, - DefaultAnswer::Answer504 { .. } => 504, - DefaultAnswer::Answer507 { .. } => 507, - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TimeoutStatus { Request, @@ -985,66 +972,68 @@ impl Http incr!( - "http.301.redirection", - self.context.cluster_id.as_deref(), - self.context.backend_id.as_deref() - ), - DefaultAnswer::Answer400 { .. } => incr!("http.400.errors"), - DefaultAnswer::Answer401 { .. } => incr!( - "http.401.errors", - self.context.cluster_id.as_deref(), - self.context.backend_id.as_deref() - ), - DefaultAnswer::Answer404 { .. } => incr!("http.404.errors"), - DefaultAnswer::Answer408 { .. } => incr!( - "http.408.errors", - self.context.cluster_id.as_deref(), - self.context.backend_id.as_deref() - ), - DefaultAnswer::Answer413 { .. } => incr!( - "http.413.errors", - self.context.cluster_id.as_deref(), - self.context.backend_id.as_deref() - ), - DefaultAnswer::Answer502 { .. } => incr!( - "http.502.errors", - self.context.cluster_id.as_deref(), - self.context.backend_id.as_deref() - ), - DefaultAnswer::Answer503 { .. } => incr!( - "http.503.errors", - self.context.cluster_id.as_deref(), - self.context.backend_id.as_deref() - ), - DefaultAnswer::Answer504 { .. } => incr!( - "http.504.errors", - self.context.cluster_id.as_deref(), - self.context.backend_id.as_deref() - ), - DefaultAnswer::Answer507 { .. } => incr!( - "http.507.errors", - self.context.cluster_id.as_deref(), - self.context.backend_id.as_deref() - ), - }; + match answer { + DefaultAnswer::AnswerCustom { .. } => incr!( + "http.custom_asnwers", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), + DefaultAnswer::Answer301 { .. } => incr!( + "http.301.redirection", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), + DefaultAnswer::Answer400 { .. } => incr!("http.400.errors"), + DefaultAnswer::Answer401 { .. } => incr!( + "http.401.errors", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), + DefaultAnswer::Answer404 { .. } => incr!("http.404.errors"), + DefaultAnswer::Answer408 { .. } => incr!( + "http.408.errors", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), + DefaultAnswer::Answer413 { .. } => incr!( + "http.413.errors", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), + DefaultAnswer::Answer502 { .. } => incr!( + "http.502.errors", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), + DefaultAnswer::Answer503 { .. } => incr!( + "http.503.errors", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), + DefaultAnswer::Answer504 { .. } => incr!( + "http.504.errors", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), + DefaultAnswer::Answer507 { .. } => incr!( + "http.507.errors", + self.context.cluster_id.as_deref(), + self.context.backend_id.as_deref() + ), } - - let mut kawa = self.answers.borrow().get( + let (status, mut kawa) = self.answers.borrow().get( answer, - self.context.id.to_string(), + self.context.id.to_string(), // TODO: this feels wrong self.context.cluster_id.as_deref(), self.context.backend_id.as_deref(), self.get_route(), ); + if let ResponseStream::DefaultAnswer(old_status, ..) = self.response_stream { + error!( + "already set the default answer to {}, trying to set to {}", + old_status, status + ); + }; kawa.prepare(&mut kawa::h1::BlockConverter); self.context.status = Some(status); self.context.reason = None; @@ -1383,6 +1372,13 @@ impl Http todo!(), + RouteDirection::Template(cluster_id, name) => { + let location = format!("{host}{port}{path}"); + // TODO: this feels wrong + self.context.cluster_id = cluster_id; + self.set_answer(DefaultAnswer::AnswerCustom { name, location }); + Err(RetrieveClusterError::Redirected) + } } } } @@ -1464,6 +1460,7 @@ impl Http, String), } /// What to do with the traffic @@ -746,14 +748,18 @@ impl Route { path_rule: &PathRule, redirect: RedirectPolicy, redirect_scheme: RedirectScheme, + redirect_template: Option, rewrite_host: Option, rewrite_path: Option, rewrite_port: Option, ) -> Result { - let flow = match (cluster_id, redirect) { - (Some(cluster_id), RedirectPolicy::Forward) => RouteDirection::Forward(cluster_id), - (_, RedirectPolicy::Temporary) => RouteDirection::Temporary(redirect_scheme), - (_, RedirectPolicy::Permanent) => RouteDirection::Permanent(redirect_scheme), + let flow = match (cluster_id, redirect, redirect_template) { + (cluster_id, RedirectPolicy::Forward, Some(template)) => { + RouteDirection::Template(cluster_id, template) + } + (Some(cluster_id), RedirectPolicy::Forward, _) => RouteDirection::Forward(cluster_id), + (_, RedirectPolicy::Temporary, _) => RouteDirection::Temporary(redirect_scheme), + (_, RedirectPolicy::Permanent, _) => RouteDirection::Permanent(redirect_scheme), _ => return Ok(Route::Deny), }; let mut capture_cap_host = match domain_rule { From 0722e7820849582d16c5f55006d6f3b2fe159bfb Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Thu, 19 Dec 2024 14:41:03 +0100 Subject: [PATCH 07/18] Refactor Route, RouteResult and redirection flow Signed-off-by: Eloi DEMOLIS --- command/assets/custom_200.html | 7 + command/assets/custom_404.html | 4 +- command/assets/custom_503.html | 6 +- lib/src/http.rs | 10 +- lib/src/https.rs | 10 +- lib/src/protocol/kawa_h1/editor.rs | 6 +- lib/src/protocol/kawa_h1/mod.rs | 154 ++++++++-------- lib/src/router/mod.rs | 272 ++++++++++++++++------------- 8 files changed, 249 insertions(+), 220 deletions(-) create mode 100644 command/assets/custom_200.html diff --git a/command/assets/custom_200.html b/command/assets/custom_200.html new file mode 100644 index 000000000..73e5926d9 --- /dev/null +++ b/command/assets/custom_200.html @@ -0,0 +1,7 @@ +HTTP/1.1 200 OK +%Content-Length: %CONTENT_LENGTH +Sozu-Id: %REQUEST_ID + +

%CLUSTER_ID Custom 200

+

original url: %ROUTE

+

rewritten url: %REDIRECT_LOCATION

\ No newline at end of file diff --git a/command/assets/custom_404.html b/command/assets/custom_404.html index 34f6c80d0..8302d4319 100644 --- a/command/assets/custom_404.html +++ b/command/assets/custom_404.html @@ -1,7 +1,7 @@ HTTP/1.1 404 Not Found Cache-Control: no-cache Connection: close -Sozu-Id: %SOZU_ID +Sozu-Id: %REQUEST_ID

My own 404 error page

-

Your request %SOZU_ID found no frontend and cannot be redirected.

\ No newline at end of file +

Your request %REQUEST_ID found no frontend and cannot be redirected.

\ No newline at end of file diff --git a/command/assets/custom_503.html b/command/assets/custom_503.html index 8f174262b..ac12b8825 100644 --- a/command/assets/custom_503.html +++ b/command/assets/custom_503.html @@ -2,10 +2,10 @@ Cache-Control: no-cache Connection: close %Content-Length: %CONTENT_LENGTH -Sozu-Id: %SOZU_ID +Sozu-Id: %REQUEST_ID

MyCluster: 503 Service Unavailable

-

No server seems to be alive, could not redirect request %SOZU_ID.

+

No server seems to be alive, could not redirect request %REQUEST_ID.

-%DETAILS
+%MESSAGE
 
\ No newline at end of file
diff --git a/lib/src/http.rs b/lib/src/http.rs
index 37c388605..ce7104d55 100644
--- a/lib/src/http.rs
+++ b/lib/src/http.rs
@@ -2,11 +2,9 @@ use std::{
     cell::RefCell,
     collections::{hash_map::Entry, BTreeMap, HashMap},
     io::ErrorKind,
-    mem,
     net::{Shutdown, SocketAddr},
     os::unix::io::AsRawFd,
     rc::{Rc, Weak},
-    str::from_utf8_unchecked,
     time::{Duration, Instant},
 };
 
@@ -32,15 +30,11 @@ use crate::{
     backends::BackendMap,
     pool::Pool,
     protocol::{
-        http::{
-            answers::HttpAnswers,
-            parser::{hostname_and_port, Method},
-            ResponseStream,
-        },
+        http::{answers::HttpAnswers, parser::Method, ResponseStream},
         proxy_protocol::expect::ExpectProxyProtocol,
         Http, Pipe, SessionState,
     },
-    router::{RouteDirection, RouteResult, Router},
+    router::{RouteResult, Router},
     server::{ListenToken, SessionManager},
     socket::server_bind,
     timer::TimeoutContainer,
diff --git a/lib/src/https.rs b/lib/src/https.rs
index b1501c15b..c9781c3c6 100644
--- a/lib/src/https.rs
+++ b/lib/src/https.rs
@@ -5,7 +5,7 @@ use std::{
     net::{Shutdown, SocketAddr as StdSocketAddr},
     os::unix::io::AsRawFd,
     rc::{Rc, Weak},
-    str::{from_utf8, from_utf8_unchecked},
+    str::from_utf8,
     sync::Arc,
     time::{Duration, Instant},
 };
@@ -53,16 +53,12 @@ use crate::{
     pool::Pool,
     protocol::{
         h2::Http2,
-        http::{
-            answers::HttpAnswers,
-            parser::{hostname_and_port, Method},
-            ResponseStream,
-        },
+        http::{answers::HttpAnswers, parser::Method, ResponseStream},
         proxy_protocol::expect::ExpectProxyProtocol,
         rustls::TlsHandshake,
         Http, Pipe, SessionState,
     },
-    router::{RouteDirection, RouteResult, Router},
+    router::{RouteResult, Router},
     server::{ListenToken, SessionManager},
     socket::{server_bind, FrontRustls},
     timer::TimeoutContainer,
diff --git a/lib/src/protocol/kawa_h1/editor.rs b/lib/src/protocol/kawa_h1/editor.rs
index e7e870c5f..7c72ce598 100644
--- a/lib/src/protocol/kawa_h1/editor.rs
+++ b/lib/src/protocol/kawa_h1/editor.rs
@@ -23,6 +23,8 @@ pub struct HttpContext {
     pub keep_alive_frontend: bool,
     /// the value of the sticky session cookie in the request
     pub sticky_session_found: Option,
+    /// position of the last authentication header, only valid until prepare is called
+    pub authentication_found: Option,
     // ---------- Status Line
     /// the value of the method in the request line
     pub method: Option,
@@ -135,7 +137,7 @@ impl HttpContext {
         let mut has_x_port = false;
         let mut has_x_proto = false;
         let mut has_connection = false;
-        for block in &mut request.blocks {
+        for (i, block) in request.blocks.iter_mut().enumerate() {
             match block {
                 kawa::Block::Header(header) if !header.is_elided() => {
                     let key = header.key.data(buf);
@@ -182,6 +184,8 @@ impl HttpContext {
                             .data_opt(buf)
                             .and_then(|data| from_utf8(data).ok())
                             .map(ToOwned::to_owned);
+                    } else if compare_no_case(key, b"Proxy-Authenticate") {
+                        self.authentication_found = Some(i);
                     }
                 }
                 _ => {}
diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs
index ba0a0fcf6..b95baeb1b 100644
--- a/lib/src/protocol/kawa_h1/mod.rs
+++ b/lib/src/protocol/kawa_h1/mod.rs
@@ -18,7 +18,7 @@ use rusty_ulid::Ulid;
 use sozu_command::{
     config::MAX_LOOP_ITERATIONS,
     logging::EndpointRecord,
-    proto::command::{Event, EventKind, ListenerType, RedirectScheme},
+    proto::command::{Event, EventKind, ListenerType, RedirectPolicy, RedirectScheme},
 };
 // use time::{Duration, Instant};
 
@@ -36,7 +36,7 @@ use crate::{
         SessionState,
     },
     retry::RetryPolicy,
-    router::{RouteDirection, RouteResult},
+    router::RouteResult,
     server::{push_event, CONN_RETRIES},
     socket::{stats::socket_rtt, SocketHandler, SocketResult, TransportProtocol},
     sozu_command::{logging::LogContext, ready::Ready},
@@ -245,6 +245,7 @@ impl Http Http route,
             Err(frontend_error) => {
                 self.set_answer(DefaultAnswer::Answer404 {});
@@ -1303,83 +1312,76 @@ impl Http {
+        if let Some(cluster_id) = &cluster_id {
+            time!(
+                "frontend_matching_time",
+                cluster_id,
+                start.elapsed().as_millis()
+            );
+        }
+
+        let host = rewritten_host.as_deref().unwrap_or(host);
+        let path = rewritten_path.as_deref().unwrap_or(path);
+        let port = rewritten_port.map_or_else(
+            || {
+                port.map_or(String::new(), |port| {
+                    format!(":{}", unsafe { from_utf8_unchecked(port) })
+                })
+            },
+            |port| format!(":{port}"),
+        );
+        let is_https = matches!(proxy.borrow().kind(), ListenerType::Https);
+        let proto = match (redirect_scheme, is_https) {
+            (RedirectScheme::UseHttp, _) | (RedirectScheme::UseSame, false) => "http",
+            (RedirectScheme::UseHttps, _) | (RedirectScheme::UseSame, true) => "https",
+        };
+
+        match (cluster_id, redirect, redirect_template) {
+            (_, RedirectPolicy::Unauthorized, _) | (None, RedirectPolicy::Forward, None) => {
                 self.set_answer(DefaultAnswer::Answer401 {});
-                Err(RetrieveClusterError::UnauthorizedRoute)
+                return Err(RetrieveClusterError::UnauthorizedRoute);
             }
-            RouteResult::Flow {
-                direction: flow,
-                rewritten_host,
-                rewritten_path,
-                rewritten_port,
-            } => {
-                let is_https = matches!(proxy.borrow().kind(), ListenerType::Https);
-                if let RouteDirection::Forward(cluster_id) = &flow {
-                    time!(
-                        "frontend_matching_time",
-                        cluster_id,
-                        start.elapsed().as_millis()
-                    );
-                    let (https_redirect, https_redirect_port, authentication) = proxy
-                        .borrow()
-                        .clusters()
-                        .get(cluster_id)
-                        .map(|cluster| {
-                            (
-                                cluster.https_redirect,
-                                cluster.https_redirect_port,
-                                None::<()>,
-                            )
-                        })
-                        .unwrap_or((false, None, None));
-                    if !is_https && https_redirect {
-                        let port = https_redirect_port
-                            .map_or(String::new(), |port| format!(":{}", port as u16));
-                        self.set_answer(DefaultAnswer::Answer301 {
-                            location: format!("https://{host}{port}{path}"),
-                        });
-                        return Err(RetrieveClusterError::Redirected);
-                    }
-                    if let Some(authentication) = authentication {
-                        return Err(RetrieveClusterError::UnauthorizedRoute);
-                    }
+            (_, RedirectPolicy::Permanent, _) => {
+                self.set_answer(DefaultAnswer::Answer301 {
+                    location: format!("{proto}://{host}{port}{path}"),
+                });
+                Err(RetrieveClusterError::Redirected)
+            }
+            (_, RedirectPolicy::Temporary, _) => todo!(),
+            (cluster_id, RedirectPolicy::Forward, Some(name)) => {
+                let location = format!("{proto}://{host}{port}{path}");
+                // TODO: this feels wrong
+                self.context.cluster_id = cluster_id;
+                self.set_answer(DefaultAnswer::AnswerCustom { name, location });
+                Err(RetrieveClusterError::Redirected)
+            }
+            (Some(cluster_id), RedirectPolicy::Forward, None) => {
+                let (https_redirect, https_redirect_port, authentication) = proxy
+                    .borrow()
+                    .clusters()
+                    .get(&cluster_id)
+                    .map(|cluster| {
+                        (
+                            cluster.https_redirect,
+                            cluster.https_redirect_port,
+                            None::<()>,
+                        )
+                    })
+                    .unwrap_or((false, None, None));
+                if !is_https && https_redirect {
+                    let port = rewritten_port
+                        .or_else(|| https_redirect_port.map(|port| port as u16))
+                        .map_or(String::new(), |port| format!(":{port}"));
+                    self.set_answer(DefaultAnswer::Answer301 {
+                        location: format!("https://{host}{port}{path}"),
+                    });
+                    return Err(RetrieveClusterError::Redirected);
                 }
-                let host = rewritten_host.as_deref().unwrap_or(host);
-                let path = rewritten_path.as_deref().unwrap_or(path);
-                let port = rewritten_port.map_or_else(
-                    || {
-                        port.map_or(String::new(), |port| {
-                            format!(":{}", unsafe { from_utf8_unchecked(port) })
-                        })
-                    },
-                    |port| format!(":{port}"),
-                );
-                match flow {
-                    RouteDirection::Forward(cluster_id) => Ok(cluster_id),
-                    RouteDirection::Permanent(redirect_scheme) => {
-                        let proto = match (redirect_scheme, is_https) {
-                            (RedirectScheme::UseHttp, _) | (RedirectScheme::UseSame, false) => {
-                                "http"
-                            }
-                            (RedirectScheme::UseHttps, _) | (RedirectScheme::UseSame, true) => {
-                                "https"
-                            }
-                        };
-                        self.set_answer(DefaultAnswer::Answer301 {
-                            location: format!("{proto}://{host}{port}{path}"),
-                        });
-                        Err(RetrieveClusterError::Redirected)
-                    }
-                    RouteDirection::Temporary(_) => todo!(),
-                    RouteDirection::Template(cluster_id, name) => {
-                        let location = format!("{host}{port}{path}");
-                        // TODO: this feels wrong
-                        self.context.cluster_id = cluster_id;
-                        self.set_answer(DefaultAnswer::AnswerCustom { name, location });
-                        Err(RetrieveClusterError::Redirected)
-                    }
+                if let Some(authentication) = authentication {
+                    self.set_answer(DefaultAnswer::Answer401 {});
+                    return Err(RetrieveClusterError::UnauthorizedRoute);
                 }
+                return Ok(cluster_id);
             }
         }
     }
diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs
index bcb2a7f19..6d798f3b7 100644
--- a/lib/src/router/mod.rs
+++ b/lib/src/router/mod.rs
@@ -719,26 +719,19 @@ impl RewriteParts {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RouteDirection {
-    Forward(ClusterId),
-    Temporary(RedirectScheme),
-    Permanent(RedirectScheme),
-    Template(Option, String),
-}
-
 /// What to do with the traffic
+/// TODO: tags should be moved here
 #[derive(Debug, Clone)]
-pub enum Route {
-    Deny,
-    Flow {
-        direction: RouteDirection,
-        capture_cap_host: usize,
-        capture_cap_path: usize,
-        rewrite_host: Option,
-        rewrite_path: Option,
-        rewrite_port: Option,
-    },
+pub struct Route {
+    cluster_id: Option,
+    redirect: RedirectPolicy,
+    redirect_scheme: RedirectScheme,
+    redirect_template: Option,
+    capture_cap_host: usize,
+    capture_cap_path: usize,
+    rewrite_host: Option,
+    rewrite_path: Option,
+    rewrite_port: Option,
 }
 
 impl Route {
@@ -753,14 +746,21 @@ impl Route {
         rewrite_path: Option,
         rewrite_port: Option,
     ) -> Result {
-        let flow = match (cluster_id, redirect, redirect_template) {
-            (cluster_id, RedirectPolicy::Forward, Some(template)) => {
-                RouteDirection::Template(cluster_id, template)
+        match (&cluster_id, redirect, &redirect_template) {
+            (None, RedirectPolicy::Forward, None) | (_, RedirectPolicy::Unauthorized, _) => {
+                return Ok(Self {
+                    cluster_id,
+                    redirect: RedirectPolicy::Unauthorized,
+                    redirect_scheme,
+                    redirect_template: None,
+                    capture_cap_host: 0,
+                    capture_cap_path: 0,
+                    rewrite_host: None,
+                    rewrite_path: None,
+                    rewrite_port: None,
+                })
             }
-            (Some(cluster_id), RedirectPolicy::Forward, _) => RouteDirection::Forward(cluster_id),
-            (_, RedirectPolicy::Temporary, _) => RouteDirection::Temporary(redirect_scheme),
-            (_, RedirectPolicy::Permanent, _) => RouteDirection::Permanent(redirect_scheme),
-            _ => return Ok(Route::Deny),
+            _ => {}
         };
         let mut capture_cap_host = match domain_rule {
             DomainRule::Any => 1,
@@ -809,8 +809,11 @@ impl Route {
         if used_capture_path == 0 {
             capture_cap_path = 0;
         }
-        Ok(Route::Flow {
-            direction: flow,
+        Ok(Route {
+            cluster_id,
+            redirect,
+            redirect_scheme,
+            redirect_template,
             capture_cap_host,
             capture_cap_path,
             rewrite_host,
@@ -821,8 +824,11 @@ impl Route {
 
     #[cfg(test)]
     pub fn simple(cluster_id: ClusterId) -> Self {
-        Self::Flow {
-            direction: RouteDirection::Forward(cluster_id),
+        Self {
+            cluster_id: Some(cluster_id),
+            redirect: RedirectPolicy::Forward,
+            redirect_scheme: RedirectScheme::UseSame,
+            redirect_template: None,
             capture_cap_host: 0,
             capture_cap_path: 0,
             rewrite_host: None,
@@ -833,63 +839,77 @@ impl Route {
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RouteResult {
-    Deny,
-    Flow {
-        direction: RouteDirection,
-        rewritten_host: Option,
-        rewritten_path: Option,
-        rewritten_port: Option,
-    },
+pub struct RouteResult {
+    pub cluster_id: Option,
+    pub redirect: RedirectPolicy,
+    pub redirect_scheme: RedirectScheme,
+    pub redirect_template: Option,
+    pub rewritten_host: Option,
+    pub rewritten_path: Option,
+    pub rewritten_port: Option,
 }
 
 impl RouteResult {
+    fn deny(cluster_id: &Option) -> Self {
+        Self {
+            cluster_id: cluster_id.clone(),
+            redirect: RedirectPolicy::Unauthorized,
+            redirect_scheme: RedirectScheme::UseSame,
+            redirect_template: None,
+            rewritten_host: None,
+            rewritten_path: None,
+            rewritten_port: None,
+        }
+    }
     fn new<'a>(
         captures_host: Vec<&'a str>,
         path: &'a [u8],
         path_rule: &PathRule,
         route: &Route,
     ) -> Self {
-        match route {
-            Route::Deny => Self::Deny,
-            Route::Flow {
-                direction: flow,
-                capture_cap_path,
-                rewrite_host,
-                rewrite_path,
-                rewrite_port,
-                ..
-            } => {
-                let mut captures_path = Vec::with_capacity(*capture_cap_path);
-                if *capture_cap_path > 0 {
-                    captures_path.push(unsafe { from_utf8_unchecked(path) });
-                    match path_rule {
-                        PathRule::Prefix(prefix) => captures_path
-                            .push(unsafe { from_utf8_unchecked(&path[prefix.len()..]) }),
-                        PathRule::Regex(regex) => captures_path.extend(
-                            regex
-                                .captures(&path)
-                                .unwrap()
-                                .iter()
-                                .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
-                        ),
-                        _ => {}
-                    }
-                }
-                println!("========HOST_CAPTURES: {captures_host:?}");
-                println!("========PATH_CAPTURES: {captures_path:?}");
-                Self::Flow {
-                    direction: flow.clone(),
-                    rewritten_host: rewrite_host
-                        .as_ref()
-                        .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
-                    rewritten_path: rewrite_path
-                        .as_ref()
-                        .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
-                    rewritten_port: *rewrite_port,
+        let Route {
+            cluster_id,
+            redirect,
+            redirect_scheme,
+            redirect_template,
+            capture_cap_path,
+            rewrite_host,
+            rewrite_path,
+            rewrite_port,
+            ..
+        } = route;
+        let mut captures_path = Vec::with_capacity(*capture_cap_path);
+        if *capture_cap_path > 0 {
+            captures_path.push(unsafe { from_utf8_unchecked(path) });
+            match path_rule {
+                PathRule::Prefix(prefix) => {
+                    captures_path.push(unsafe { from_utf8_unchecked(&path[prefix.len()..]) })
                 }
+                PathRule::Regex(regex) => captures_path.extend(
+                    regex
+                        .captures(&path)
+                        .unwrap()
+                        .iter()
+                        .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
+                ),
+                _ => {}
             }
         }
+        println!("========HOST_CAPTURES: {captures_host:?}");
+        println!("========PATH_CAPTURES: {captures_path:?}");
+        Self {
+            cluster_id: cluster_id.clone(),
+            redirect: *redirect,
+            redirect_scheme: *redirect_scheme,
+            redirect_template: redirect_template.clone(),
+            rewritten_host: rewrite_host
+                .as_ref()
+                .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
+            rewritten_path: rewrite_path
+                .as_ref()
+                .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
+            rewritten_port: *rewrite_port,
+        }
     }
     fn new_no_trie<'a>(
         domain: &'a [u8],
@@ -898,32 +918,33 @@ impl RouteResult {
         path_rule: &PathRule,
         route: &Route,
     ) -> Self {
-        match route {
-            Route::Deny => Self::Deny,
-            Route::Flow {
-                capture_cap_host, ..
-            } => {
-                let mut captures_host = Vec::with_capacity(*capture_cap_host);
-                if *capture_cap_host > 0 {
-                    captures_host.push(unsafe { from_utf8_unchecked(domain) });
-                    match domain_rule {
-                        DomainRule::Wildcard(suffix) => captures_host.push(unsafe {
-                            from_utf8_unchecked(&domain[..domain.len() - suffix.len()])
-                        }),
-                        DomainRule::Regex(regex) => captures_host.extend(
-                            regex
-                                .captures(&domain)
-                                .unwrap()
-                                .iter()
-                                .skip(1)
-                                .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
-                        ),
-                        _ => {}
-                    }
-                }
-                Self::new(captures_host, path, path_rule, route)
+        let Route {
+            cluster_id,
+            redirect,
+            capture_cap_host,
+            ..
+        } = route;
+        if *redirect == RedirectPolicy::Unauthorized {
+            return Self::deny(cluster_id);
+        }
+        let mut captures_host = Vec::with_capacity(*capture_cap_host);
+        if *capture_cap_host > 0 {
+            captures_host.push(unsafe { from_utf8_unchecked(domain) });
+            match domain_rule {
+                DomainRule::Wildcard(suffix) => captures_host
+                    .push(unsafe { from_utf8_unchecked(&domain[..domain.len() - suffix.len()]) }),
+                DomainRule::Regex(regex) => captures_host.extend(
+                    regex
+                        .captures(&domain)
+                        .unwrap()
+                        .iter()
+                        .skip(1)
+                        .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
+                ),
+                _ => {}
             }
         }
+        Self::new(captures_host, path, path_rule, route)
     }
     fn new_with_trie<'a>(
         domain: &'a [u8],
@@ -932,39 +953,44 @@ impl RouteResult {
         path_rule: &PathRule,
         route: &Route,
     ) -> Self {
-        match route {
-            Route::Deny => Self::Deny,
-            Route::Flow {
-                capture_cap_host, ..
-            } => {
-                let mut captures_host = Vec::with_capacity(*capture_cap_host);
-                if *capture_cap_host > 0 {
-                    captures_host.push(unsafe { from_utf8_unchecked(domain) });
-                    for submatch in domain_submatches {
-                        match submatch {
-                            TrieSubMatch::Wildcard(part) => {
-                                captures_host.push(unsafe { from_utf8_unchecked(part) })
-                            }
-                            TrieSubMatch::Regexp(part, regex) => captures_host.extend(
-                                regex
-                                    .captures(&part)
-                                    .unwrap()
-                                    .iter()
-                                    .skip(1)
-                                    .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
-                            ),
-                        }
+        let Route {
+            cluster_id,
+            redirect,
+            capture_cap_host,
+            ..
+        } = route;
+        if *redirect == RedirectPolicy::Unauthorized {
+            return Self::deny(cluster_id);
+        }
+        let mut captures_host = Vec::with_capacity(*capture_cap_host);
+        if *capture_cap_host > 0 {
+            captures_host.push(unsafe { from_utf8_unchecked(domain) });
+            for submatch in domain_submatches {
+                match submatch {
+                    TrieSubMatch::Wildcard(part) => {
+                        captures_host.push(unsafe { from_utf8_unchecked(part) })
                     }
+                    TrieSubMatch::Regexp(part, regex) => captures_host.extend(
+                        regex
+                            .captures(&part)
+                            .unwrap()
+                            .iter()
+                            .skip(1)
+                            .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
+                    ),
                 }
-                Self::new(captures_host, path, path_rule, route)
             }
         }
+        Self::new(captures_host, path, path_rule, route)
     }
 
     #[cfg(test)]
     pub fn simple(cluster_id: ClusterId) -> Self {
-        Self::Flow {
-            direction: RouteDirection::Forward(cluster_id),
+        Self {
+            cluster_id: Some(cluster_id),
+            redirect: RedirectPolicy::Forward,
+            redirect_scheme: RedirectScheme::UseSame,
+            redirect_template: None,
             rewritten_host: None,
             rewritten_path: None,
             rewritten_port: None,

From 6110de3b8da6f1818eb3cd92e2c571dc76b7cf5b Mon Sep 17 00:00:00 2001
From: Eloi DEMOLIS 
Date: Sat, 21 Dec 2024 00:41:20 +0100
Subject: [PATCH 08/18] PoC authentication

Signed-off-by: Eloi DEMOLIS 
---
 bin/src/ctl/request_builder.rs     |   2 +
 command/src/command.proto          |  17 ++--
 command/src/config.rs              |  11 +++
 command/src/request.rs             |   1 +
 command/src/response.rs            |   2 +
 lib/src/http.rs                    |  12 ++-
 lib/src/https.rs                   |  16 ++--
 lib/src/protocol/kawa_h1/editor.rs |  11 ++-
 lib/src/protocol/kawa_h1/mod.rs    |  94 +++++++++++++--------
 lib/src/router/mod.rs              | 130 ++++++++++++++++++-----------
 10 files changed, 185 insertions(+), 111 deletions(-)

diff --git a/bin/src/ctl/request_builder.rs b/bin/src/ctl/request_builder.rs
index dd7764b5e..2dbd58727 100644
--- a/bin/src/ctl/request_builder.rs
+++ b/bin/src/ctl/request_builder.rs
@@ -250,6 +250,7 @@ impl CommandManager {
                         Some(tags) => tags,
                         None => BTreeMap::new(),
                     },
+                    required_auth: todo!(),
                     redirect: todo!(),
                     redirect_scheme: todo!(),
                     redirect_template: todo!(),
@@ -304,6 +305,7 @@ impl CommandManager {
                         Some(tags) => tags,
                         None => BTreeMap::new(),
                     },
+                    required_auth: todo!(),
                     redirect: todo!(),
                     redirect_scheme: todo!(),
                     redirect_template: todo!(),
diff --git a/command/src/command.proto b/command/src/command.proto
index d89a60275..d929e0ba1 100644
--- a/command/src/command.proto
+++ b/command/src/command.proto
@@ -214,9 +214,8 @@ message ListenersList {
 
 enum RedirectPolicy {
     FORWARD = 0;
-    TEMPORARY = 1;
-    PERMANENT = 2;
-    UNAUTHORIZED = 3;
+    PERMANENT = 1;
+    UNAUTHORIZED = 2;
 }
 
 enum RedirectScheme {
@@ -236,11 +235,12 @@ message RequestHttpFrontend {
     // custom tags to identify the frontend in the access logs
     map tags = 7;
     optional RedirectPolicy redirect = 8;
-    optional RedirectScheme redirect_scheme = 9;
-    optional string redirect_template = 10;
-    optional string rewrite_host = 11;
-    optional string rewrite_path = 12;
-    optional uint32 rewrite_port = 13;
+    optional bool required_auth = 9;
+    optional RedirectScheme redirect_scheme = 10;
+    optional string redirect_template = 11;
+    optional string rewrite_host = 12;
+    optional string rewrite_path = 13;
+    optional uint32 rewrite_port = 14;
 }
 
 message RequestTcpFrontend {
@@ -371,6 +371,7 @@ message Cluster {
     optional LoadMetric load_metric = 7;
     optional uint32 https_redirect_port = 8;
     map answers = 9;
+    repeated uint64 authorized_hashes = 10;
 }
 
 enum LoadBalancingAlgorithms {
diff --git a/command/src/config.rs b/command/src/config.rs
index b4027fe7d..dc7290361 100644
--- a/command/src/config.rs
+++ b/command/src/config.rs
@@ -625,6 +625,7 @@ pub struct FileClusterFrontendConfig {
     #[serde(default)]
     pub position: RulePosition,
     pub tags: Option>,
+    pub required_auth: Option,
     pub redirect: Option,
     pub redirect_scheme: Option,
     pub redirect_template: Option,
@@ -716,6 +717,7 @@ impl FileClusterFrontendConfig {
             path,
             method: self.method.clone(),
             tags: self.tags.clone(),
+            required_auth: self.required_auth.unwrap_or(false),
             redirect: self.redirect,
             redirect_scheme: self.redirect_scheme,
             redirect_template: self.redirect_template.clone(),
@@ -758,6 +760,8 @@ pub struct FileClusterConfig {
     pub load_metric: Option,
     #[serde(default)]
     pub answers: Option>,
+    #[serde(default)]
+    pub authorized_hashes: Vec,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -845,6 +849,7 @@ impl FileClusterConfig {
                     load_balancing: self.load_balancing,
                     load_metric: self.load_metric,
                     answers: load_answers(self.answers.as_ref())?,
+                    authorized_hashes: self.authorized_hashes,
                 }))
             }
         }
@@ -866,6 +871,7 @@ pub struct HttpFrontendConfig {
     #[serde(default)]
     pub position: RulePosition,
     pub tags: Option>,
+    pub required_auth: bool,
     pub redirect: Option,
     pub redirect_scheme: Option,
     pub redirect_template: Option,
@@ -909,6 +915,7 @@ impl HttpFrontendConfig {
                     method: self.method.clone(),
                     position: self.position.into(),
                     tags,
+                    required_auth: Some(self.required_auth),
                     redirect: self.redirect.map(Into::into),
                     redirect_scheme: self.redirect_scheme.map(Into::into),
                     redirect_template: self.redirect_template.clone(),
@@ -929,6 +936,7 @@ impl HttpFrontendConfig {
                     method: self.method.clone(),
                     position: self.position.into(),
                     tags,
+                    required_auth: Some(self.required_auth),
                     redirect: self.redirect.map(Into::into),
                     redirect_scheme: self.redirect_scheme.map(Into::into),
                     redirect_template: self.redirect_template.clone(),
@@ -956,6 +964,7 @@ pub struct HttpClusterConfig {
     pub load_balancing: LoadBalancingAlgorithms,
     pub load_metric: Option,
     pub answers: BTreeMap,
+    pub authorized_hashes: Vec,
 }
 
 impl HttpClusterConfig {
@@ -969,6 +978,7 @@ impl HttpClusterConfig {
             load_balancing: self.load_balancing as i32,
             load_metric: self.load_metric.map(|s| s as i32),
             answers: self.answers.clone(),
+            authorized_hashes: self.authorized_hashes.clone(),
         })
         .into()];
 
@@ -1029,6 +1039,7 @@ impl TcpClusterConfig {
             load_balancing: self.load_balancing as i32,
             load_metric: self.load_metric.map(|s| s as i32),
             answers: Default::default(),
+            authorized_hashes: Default::default(),
         })
         .into()];
 
diff --git a/command/src/request.rs b/command/src/request.rs
index f40938ae8..d70b1b8fb 100644
--- a/command/src/request.rs
+++ b/command/src/request.rs
@@ -161,6 +161,7 @@ impl RequestHttpFrontend {
     pub fn to_frontend(self) -> Result {
         Ok(HttpFrontend {
             position: self.position(),
+            required_auth: self.required_auth.unwrap_or(false),
             redirect: self.redirect(),
             redirect_scheme: self.redirect_scheme(),
             redirect_template: self.redirect_template,
diff --git a/command/src/response.rs b/command/src/response.rs
index 2883b3794..777347420 100644
--- a/command/src/response.rs
+++ b/command/src/response.rs
@@ -39,6 +39,7 @@ pub struct HttpFrontend {
     #[serde(default)]
     pub position: RulePosition,
     pub tags: Option>,
+    pub required_auth: bool,
     pub redirect: RedirectPolicy,
     pub redirect_scheme: RedirectScheme,
     #[serde(skip_serializing_if = "Option::is_none")]
@@ -61,6 +62,7 @@ impl From for RequestHttpFrontend {
             method: val.method,
             position: val.position.into(),
             tags: val.tags.unwrap_or_default(),
+            required_auth: Some(val.required_auth),
             redirect: Some(val.redirect.into()),
             redirect_scheme: Some(val.redirect_scheme.into()),
             redirect_template: val.redirect_template,
diff --git a/lib/src/http.rs b/lib/src/http.rs
index ce7104d55..a430c1f17 100644
--- a/lib/src/http.rs
+++ b/lib/src/http.rs
@@ -1284,6 +1284,7 @@ mod tests {
                 path: PathRule::prefix(uri1),
                 position: RulePosition::Tree,
                 cluster_id: Some(cluster_id1),
+                required_auth: false,
                 redirect: RedirectPolicy::Forward,
                 redirect_scheme: RedirectScheme::UseSame,
                 redirect_template: None,
@@ -1301,6 +1302,7 @@ mod tests {
                 path: PathRule::prefix(uri2),
                 position: RulePosition::Tree,
                 cluster_id: Some(cluster_id2),
+                required_auth: false,
                 redirect: RedirectPolicy::Forward,
                 redirect_scheme: RedirectScheme::UseSame,
                 redirect_template: None,
@@ -1318,6 +1320,7 @@ mod tests {
                 path: PathRule::prefix(uri3),
                 position: RulePosition::Tree,
                 cluster_id: Some(cluster_id3),
+                required_auth: false,
                 redirect: RedirectPolicy::Forward,
                 redirect_scheme: RedirectScheme::UseSame,
                 redirect_template: None,
@@ -1335,6 +1338,7 @@ mod tests {
                 path: PathRule::prefix("/test".to_owned()),
                 position: RulePosition::Tree,
                 cluster_id: Some("cluster_1".to_owned()),
+                required_auth: false,
                 redirect: RedirectPolicy::Forward,
                 redirect_scheme: RedirectScheme::UseSame,
                 redirect_template: None,
@@ -1369,19 +1373,19 @@ mod tests {
         let frontend5 = listener.frontend_from_request("domain", "/", &Method::Get);
         assert_eq!(
             frontend1.expect("should find frontend"),
-            RouteResult::simple("cluster_1".to_string())
+            RouteResult::forward("cluster_1".to_string())
         );
         assert_eq!(
             frontend2.expect("should find frontend"),
-            RouteResult::simple("cluster_1".to_string())
+            RouteResult::forward("cluster_1".to_string())
         );
         assert_eq!(
             frontend3.expect("should find frontend"),
-            RouteResult::simple("cluster_2".to_string())
+            RouteResult::forward("cluster_2".to_string())
         );
         assert_eq!(
             frontend4.expect("should find frontend"),
-            RouteResult::simple("cluster_3".to_string())
+            RouteResult::forward("cluster_3".to_string())
         );
         assert!(frontend5.is_err());
     }
diff --git a/lib/src/https.rs b/lib/src/https.rs
index c9781c3c6..abaf162c0 100644
--- a/lib/src/https.rs
+++ b/lib/src/https.rs
@@ -1504,25 +1504,25 @@ mod tests {
             "lolcatho.st".as_bytes(),
             &PathRule::Prefix(uri1),
             &MethodRule::new(None),
-            &Route::simple(cluster_id1.clone())
+            &Route::forward(cluster_id1.clone())
         ));
         assert!(fronts.add_tree_rule(
             "lolcatho.st".as_bytes(),
             &PathRule::Prefix(uri2),
             &MethodRule::new(None),
-            &Route::simple(cluster_id2)
+            &Route::forward(cluster_id2)
         ));
         assert!(fronts.add_tree_rule(
             "lolcatho.st".as_bytes(),
             &PathRule::Prefix(uri3),
             &MethodRule::new(None),
-            &Route::simple(cluster_id3)
+            &Route::forward(cluster_id3)
         ));
         assert!(fronts.add_tree_rule(
             "other.domain".as_bytes(),
             &PathRule::Prefix("test".to_string()),
             &MethodRule::new(None),
-            &Route::simple(cluster_id1)
+            &Route::forward(cluster_id1)
         ));
 
         let address = SocketAddress::new_v4(127, 0, 0, 1, 1032);
@@ -1561,25 +1561,25 @@ mod tests {
         let frontend1 = listener.frontend_from_request("lolcatho.st", "/", &Method::Get);
         assert_eq!(
             frontend1.expect("should find a frontend"),
-            RouteResult::simple("cluster_1".to_string())
+            RouteResult::forward("cluster_1".to_string())
         );
         println!("TEST {}", line!());
         let frontend2 = listener.frontend_from_request("lolcatho.st", "/test", &Method::Get);
         assert_eq!(
             frontend2.expect("should find a frontend"),
-            RouteResult::simple("cluster_1".to_string())
+            RouteResult::forward("cluster_1".to_string())
         );
         println!("TEST {}", line!());
         let frontend3 = listener.frontend_from_request("lolcatho.st", "/yolo/test", &Method::Get);
         assert_eq!(
             frontend3.expect("should find a frontend"),
-            RouteResult::simple("cluster_2".to_string())
+            RouteResult::forward("cluster_2".to_string())
         );
         println!("TEST {}", line!());
         let frontend4 = listener.frontend_from_request("lolcatho.st", "/yolo/swag", &Method::Get);
         assert_eq!(
             frontend4.expect("should find a frontend"),
-            RouteResult::simple("cluster_3".to_string())
+            RouteResult::forward("cluster_3".to_string())
         );
         println!("TEST {}", line!());
         let frontend5 = listener.frontend_from_request("domain", "/", &Method::Get);
diff --git a/lib/src/protocol/kawa_h1/editor.rs b/lib/src/protocol/kawa_h1/editor.rs
index 7c72ce598..7f6f6a2c1 100644
--- a/lib/src/protocol/kawa_h1/editor.rs
+++ b/lib/src/protocol/kawa_h1/editor.rs
@@ -1,4 +1,5 @@
 use std::{
+    hash::{DefaultHasher, Hash, Hasher},
     net::{IpAddr, SocketAddr},
     str::{from_utf8, from_utf8_unchecked},
 };
@@ -24,7 +25,7 @@ pub struct HttpContext {
     /// the value of the sticky session cookie in the request
     pub sticky_session_found: Option,
     /// position of the last authentication header, only valid until prepare is called
-    pub authentication_found: Option,
+    pub authentication_found: Option,
     // ---------- Status Line
     /// the value of the method in the request line
     pub method: Option,
@@ -137,7 +138,7 @@ impl HttpContext {
         let mut has_x_port = false;
         let mut has_x_proto = false;
         let mut has_connection = false;
-        for (i, block) in request.blocks.iter_mut().enumerate() {
+        for block in &mut request.blocks {
             match block {
                 kawa::Block::Header(header) if !header.is_elided() => {
                     let key = header.key.data(buf);
@@ -185,7 +186,11 @@ impl HttpContext {
                             .and_then(|data| from_utf8(data).ok())
                             .map(ToOwned::to_owned);
                     } else if compare_no_case(key, b"Proxy-Authenticate") {
-                        self.authentication_found = Some(i);
+                        self.authentication_found = header.val.data_opt(buf).map(|auth| {
+                            let mut h = DefaultHasher::new();
+                            auth.hash(&mut h);
+                            h.finish()
+                        });
                     }
                 }
                 _ => {}
diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs
index b95baeb1b..efbd10b12 100644
--- a/lib/src/protocol/kawa_h1/mod.rs
+++ b/lib/src/protocol/kawa_h1/mod.rs
@@ -20,7 +20,6 @@ use sozu_command::{
     logging::EndpointRecord,
     proto::command::{Event, EventKind, ListenerType, RedirectPolicy, RedirectScheme},
 };
-// use time::{Duration, Instant};
 
 use crate::{
     backends::{Backend, BackendError},
@@ -116,8 +115,8 @@ pub enum DefaultAnswer {
         duration: String,
     },
     Answer507 {
-        phase: kawa::ParsingPhaseMarker,
         message: String,
+        phase: kawa::ParsingPhaseMarker,
         capacity: usize,
     },
 }
@@ -1024,8 +1023,8 @@ impl Http Http Http "https",
         };
 
-        match (cluster_id, redirect, redirect_template) {
-            (_, RedirectPolicy::Unauthorized, _) | (None, RedirectPolicy::Forward, None) => {
-                self.set_answer(DefaultAnswer::Answer401 {});
-                return Err(RetrieveClusterError::UnauthorizedRoute);
-            }
-            (_, RedirectPolicy::Permanent, _) => {
-                self.set_answer(DefaultAnswer::Answer301 {
-                    location: format!("{proto}://{host}{port}{path}"),
-                });
+        let (authorized, https_redirect, https_redirect_port) =
+            match (&cluster_id, redirect, &redirect_template, required_auth) {
+                // unauthorized frontends
+                (_, RedirectPolicy::Unauthorized, _, _) => (false, false, None),
+                // forward frontends with no target (no cluster nor template)
+                (None, RedirectPolicy::Forward, None, _) => (false, false, None),
+                // clusterless frontend with auth (unsupported)
+                (None, _, _, true) => (false, false, None),
+                // clusterless frontends
+                (None, _, _, false) => (true, false, None),
+                // "attached" frontends
+                (Some(cluster_id), _, _, _) => {
+                    proxy.borrow().clusters().get(cluster_id).map_or(
+                        (false, false, None), // cluster not found, consider unauthorized
+                        |cluster| {
+                            let authorized =
+                                match (required_auth, self.context.authentication_found) {
+                                    // auth not required
+                                    (false, _) => true,
+                                    // no auth found
+                                    (true, None) => false,
+                                    // validation
+                                    (true, Some(hash)) => {
+                                        println!("{hash:?}");
+                                        cluster.authorized_hashes.contains(&hash)
+                                    }
+                                };
+                            (
+                                authorized,
+                                cluster.https_redirect,
+                                cluster.https_redirect_port,
+                            )
+                        },
+                    )
+                }
+            };
+
+        match (cluster_id, redirect, redirect_template, authorized) {
+            (cluster_id, RedirectPolicy::Permanent, _, true) => {
+                let location = format!("{proto}://{host}{port}{path}");
+                self.context.cluster_id = cluster_id;
+                self.set_answer(DefaultAnswer::Answer301 { location });
                 Err(RetrieveClusterError::Redirected)
             }
-            (_, RedirectPolicy::Temporary, _) => todo!(),
-            (cluster_id, RedirectPolicy::Forward, Some(name)) => {
+            (cluster_id, RedirectPolicy::Forward, Some(name), true) => {
                 let location = format!("{proto}://{host}{port}{path}");
-                // TODO: this feels wrong
                 self.context.cluster_id = cluster_id;
                 self.set_answer(DefaultAnswer::AnswerCustom { name, location });
                 Err(RetrieveClusterError::Redirected)
             }
-            (Some(cluster_id), RedirectPolicy::Forward, None) => {
-                let (https_redirect, https_redirect_port, authentication) = proxy
-                    .borrow()
-                    .clusters()
-                    .get(&cluster_id)
-                    .map(|cluster| {
-                        (
-                            cluster.https_redirect,
-                            cluster.https_redirect_port,
-                            None::<()>,
-                        )
-                    })
-                    .unwrap_or((false, None, None));
+            (Some(cluster_id), RedirectPolicy::Forward, None, true) => {
                 if !is_https && https_redirect {
                     let port = rewritten_port
                         .or_else(|| https_redirect_port.map(|port| port as u16))
                         .map_or(String::new(), |port| format!(":{port}"));
-                    self.set_answer(DefaultAnswer::Answer301 {
-                        location: format!("https://{host}{port}{path}"),
-                    });
+                    let location = format!("https://{host}{port}{path}");
+                    self.context.cluster_id = Some(cluster_id);
+                    self.set_answer(DefaultAnswer::Answer301 { location });
                     return Err(RetrieveClusterError::Redirected);
                 }
-                if let Some(authentication) = authentication {
-                    self.set_answer(DefaultAnswer::Answer401 {});
-                    return Err(RetrieveClusterError::UnauthorizedRoute);
-                }
-                return Ok(cluster_id);
+                Ok(cluster_id)
+            }
+            (cluster_id, ..) => {
+                self.context.cluster_id = cluster_id;
+                self.set_answer(DefaultAnswer::Answer401 {});
+                Err(RetrieveClusterError::UnauthorizedRoute)
             }
         }
     }
diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs
index 6d798f3b7..e4c752cde 100644
--- a/lib/src/router/mod.rs
+++ b/lib/src/router/mod.rs
@@ -171,6 +171,7 @@ impl Router {
             front.cluster_id.clone(),
             &domain_rule,
             &path_rule,
+            front.required_auth,
             front.redirect,
             front.redirect_scheme,
             front.redirect_template.clone(),
@@ -724,6 +725,7 @@ impl RewriteParts {
 #[derive(Debug, Clone)]
 pub struct Route {
     cluster_id: Option,
+    required_auth: bool,
     redirect: RedirectPolicy,
     redirect_scheme: RedirectScheme,
     redirect_template: Option,
@@ -739,6 +741,7 @@ impl Route {
         cluster_id: Option,
         domain_rule: &DomainRule,
         path_rule: &PathRule,
+        required_auth: bool,
         redirect: RedirectPolicy,
         redirect_scheme: RedirectScheme,
         redirect_template: Option,
@@ -746,22 +749,39 @@ impl Route {
         rewrite_path: Option,
         rewrite_port: Option,
     ) -> Result {
-        match (&cluster_id, redirect, &redirect_template) {
-            (None, RedirectPolicy::Forward, None) | (_, RedirectPolicy::Unauthorized, _) => {
-                return Ok(Self {
-                    cluster_id,
-                    redirect: RedirectPolicy::Unauthorized,
-                    redirect_scheme,
-                    redirect_template: None,
-                    capture_cap_host: 0,
-                    capture_cap_path: 0,
-                    rewrite_host: None,
-                    rewrite_path: None,
-                    rewrite_port: None,
-                })
+        let deny = match (&cluster_id, redirect, &redirect_template, required_auth) {
+            (_, RedirectPolicy::Unauthorized, _, false) => true,
+            (_, RedirectPolicy::Unauthorized, _, true) => {
+                warn!("Frontend[cluster: {:?}, domain: {:?}, path: {:?}, redirect: {:?}]: unauthorized frontends ignore auth", cluster_id, domain_rule, path_rule, redirect);
+                true
+            }
+            (None, RedirectPolicy::Forward, None, _) => {
+                warn!("Frontend[domain: {:?}, path: {:?}]: forward on clusterless frontends are unauthorized", domain_rule, path_rule);
+                true
+            }
+            (None, _, _, true) => {
+                warn!(
+                    "Frontend[domain: {:?}, path: {:?}]: clusterless frontends ignore auth",
+                    domain_rule, path_rule
+                );
+                true
             }
-            _ => {}
+            _ => false,
         };
+        if deny {
+            return Ok(Self {
+                cluster_id,
+                required_auth,
+                redirect: RedirectPolicy::Unauthorized,
+                redirect_scheme,
+                redirect_template: None,
+                capture_cap_host: 0,
+                capture_cap_path: 0,
+                rewrite_host: None,
+                rewrite_path: None,
+                rewrite_port: None,
+            });
+        }
         let mut capture_cap_host = match domain_rule {
             DomainRule::Any => 1,
             DomainRule::Equals(_) => 1,
@@ -811,6 +831,7 @@ impl Route {
         }
         Ok(Route {
             cluster_id,
+            required_auth,
             redirect,
             redirect_scheme,
             redirect_template,
@@ -823,9 +844,10 @@ impl Route {
     }
 
     #[cfg(test)]
-    pub fn simple(cluster_id: ClusterId) -> Self {
+    pub fn forward(cluster_id: ClusterId) -> Self {
         Self {
             cluster_id: Some(cluster_id),
+            required_auth: false,
             redirect: RedirectPolicy::Forward,
             redirect_scheme: RedirectScheme::UseSame,
             redirect_template: None,
@@ -841,6 +863,7 @@ impl Route {
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct RouteResult {
     pub cluster_id: Option,
+    pub required_auth: bool,
     pub redirect: RedirectPolicy,
     pub redirect_scheme: RedirectScheme,
     pub redirect_template: Option,
@@ -853,6 +876,7 @@ impl RouteResult {
     fn deny(cluster_id: &Option) -> Self {
         Self {
             cluster_id: cluster_id.clone(),
+            required_auth: false,
             redirect: RedirectPolicy::Unauthorized,
             redirect_scheme: RedirectScheme::UseSame,
             redirect_template: None,
@@ -861,6 +885,21 @@ impl RouteResult {
             rewritten_port: None,
         }
     }
+
+    #[cfg(test)]
+    pub fn forward(cluster_id: ClusterId) -> Self {
+        Self {
+            cluster_id: Some(cluster_id),
+            required_auth: false,
+            redirect: RedirectPolicy::Forward,
+            redirect_scheme: RedirectScheme::UseSame,
+            redirect_template: None,
+            rewritten_host: None,
+            rewritten_path: None,
+            rewritten_port: None,
+        }
+    }
+
     fn new<'a>(
         captures_host: Vec<&'a str>,
         path: &'a [u8],
@@ -869,6 +908,7 @@ impl RouteResult {
     ) -> Self {
         let Route {
             cluster_id,
+            required_auth,
             redirect,
             redirect_scheme,
             redirect_template,
@@ -899,6 +939,7 @@ impl RouteResult {
         println!("========PATH_CAPTURES: {captures_path:?}");
         Self {
             cluster_id: cluster_id.clone(),
+            required_auth: *required_auth,
             redirect: *redirect,
             redirect_scheme: *redirect_scheme,
             redirect_template: redirect_template.clone(),
@@ -983,19 +1024,6 @@ impl RouteResult {
         }
         Self::new(captures_host, path, path_rule, route)
     }
-
-    #[cfg(test)]
-    pub fn simple(cluster_id: ClusterId) -> Self {
-        Self {
-            cluster_id: Some(cluster_id),
-            redirect: RedirectPolicy::Forward,
-            redirect_scheme: RedirectScheme::UseSame,
-            redirect_template: None,
-            rewritten_host: None,
-            rewritten_path: None,
-            rewritten_port: None,
-        }
-    }
 }
 
 #[cfg(test)]
@@ -1114,27 +1142,27 @@ mod tests {
             b"*.sozu.io",
             &PathRule::Prefix("".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("base".to_string())
+            &Route::forward("base".to_string())
         ));
         println!("{:#?}", router.tree);
         assert_eq!(
             router.lookup("www.sozu.io", "/api", &Method::Get),
-            Ok(RouteResult::simple("base".to_string()))
+            Ok(RouteResult::forward("base".to_string()))
         );
         assert!(router.add_tree_rule(
             b"*.sozu.io",
             &PathRule::Prefix("/api".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("api".to_string())
+            &Route::forward("api".to_string())
         ));
         println!("{:#?}", router.tree);
         assert_eq!(
             router.lookup("www.sozu.io", "/ap", &Method::Get),
-            Ok(RouteResult::simple("base".to_string()))
+            Ok(RouteResult::forward("base".to_string()))
         );
         assert_eq!(
             router.lookup("www.sozu.io", "/api", &Method::Get),
-            Ok(RouteResult::simple("api".to_string()))
+            Ok(RouteResult::forward("api".to_string()))
         );
     }
 
@@ -1153,27 +1181,27 @@ mod tests {
             b"*.sozu.io",
             &PathRule::Prefix("".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("base".to_string())
+            &Route::forward("base".to_string())
         ));
         println!("{:#?}", router.tree);
         assert_eq!(
             router.lookup("www.sozu.io", "/api", &Method::Get),
-            Ok(RouteResult::simple("base".to_string()))
+            Ok(RouteResult::forward("base".to_string()))
         );
         assert!(router.add_tree_rule(
             b"api.sozu.io",
             &PathRule::Prefix("".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("api".to_string())
+            &Route::forward("api".to_string())
         ));
         println!("{:#?}", router.tree);
         assert_eq!(
             router.lookup("www.sozu.io", "/api", &Method::Get),
-            Ok(RouteResult::simple("base".to_string()))
+            Ok(RouteResult::forward("base".to_string()))
         );
         assert_eq!(
             router.lookup("api.sozu.io", "/api", &Method::Get),
-            Ok(RouteResult::simple("api".to_string()))
+            Ok(RouteResult::forward("api".to_string()))
         );
     }
 
@@ -1185,23 +1213,23 @@ mod tests {
             b"www./.*/.io",
             &PathRule::Prefix("".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("base".to_string())
+            &Route::forward("base".to_string())
         ));
         println!("{:#?}", router.tree);
         assert!(router.add_tree_rule(
             b"www.doc./.*/.io",
             &PathRule::Prefix("".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("doc".to_string())
+            &Route::forward("doc".to_string())
         ));
         println!("{:#?}", router.tree);
         assert_eq!(
             router.lookup("www.sozu.io", "/", &Method::Get),
-            Ok(RouteResult::simple("base".to_string()))
+            Ok(RouteResult::forward("base".to_string()))
         );
         assert_eq!(
             router.lookup("www.doc.sozu.io", "/", &Method::Get),
-            Ok(RouteResult::simple("doc".to_string()))
+            Ok(RouteResult::forward("doc".to_string()))
         );
         assert!(router.remove_tree_rule(
             b"www./.*/.io",
@@ -1212,7 +1240,7 @@ mod tests {
         assert!(router.lookup("www.sozu.io", "/", &Method::Get).is_err());
         assert_eq!(
             router.lookup("www.doc.sozu.io", "/", &Method::Get),
-            Ok(RouteResult::simple("doc".to_string()))
+            Ok(RouteResult::forward("doc".to_string()))
         );
     }
 
@@ -1224,30 +1252,30 @@ mod tests {
             &"*".parse::().unwrap(),
             &PathRule::Prefix("/.well-known/acme-challenge".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("acme".to_string())
+            &Route::forward("acme".to_string())
         ));
         assert!(router.add_tree_rule(
             "www.example.com".as_bytes(),
             &PathRule::Prefix("/".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("example".to_string())
+            &Route::forward("example".to_string())
         ));
         assert!(router.add_tree_rule(
             "*.test.example.com".as_bytes(),
             &PathRule::Regex(Regex::new("/hello[A-Z]+/").unwrap()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("examplewildcard".to_string())
+            &Route::forward("examplewildcard".to_string())
         ));
         assert!(router.add_tree_rule(
             "/test[0-9]/.example.com".as_bytes(),
             &PathRule::Prefix("/".to_string()),
             &MethodRule::new(Some("GET".to_string())),
-            &Route::simple("exampleregex".to_string())
+            &Route::forward("exampleregex".to_string())
         ));
 
         assert_eq!(
             router.lookup("www.example.com", "/helloA", &Method::new(&b"GET"[..])),
-            Ok(RouteResult::simple("example".to_string()))
+            Ok(RouteResult::forward("example".to_string()))
         );
         assert_eq!(
             router.lookup(
@@ -1255,7 +1283,7 @@ mod tests {
                 "/.well-known/acme-challenge",
                 &Method::new(&b"GET"[..])
             ),
-            Ok(RouteResult::simple("acme".to_string()))
+            Ok(RouteResult::forward("acme".to_string()))
         );
         assert!(router
             .lookup("www.test.example.com", "/", &Method::new(&b"GET"[..]))
@@ -1266,11 +1294,11 @@ mod tests {
                 "/helloAB/",
                 &Method::new(&b"GET"[..])
             ),
-            Ok(RouteResult::simple("examplewildcard".to_string()))
+            Ok(RouteResult::forward("examplewildcard".to_string()))
         );
         assert_eq!(
             router.lookup("test1.example.com", "/helloAB/", &Method::new(&b"GET"[..])),
-            Ok(RouteResult::simple("exampleregex".to_string()))
+            Ok(RouteResult::forward("exampleregex".to_string()))
         );
     }
 }

From 33513c42ca9382938f22ccd4a1df620e1ebf72e1 Mon Sep 17 00:00:00 2001
From: Eloi DEMOLIS 
Date: Tue, 7 Jan 2025 18:18:46 +0100
Subject: [PATCH 09/18] Refactor backend logic with an Origin struct

I feel like it's going nowhere...
Backend logic is messy and unclear, buffers are needlessly kept alive,
borrowing subsets of the HTTP session is harder and harder, reseting
default answers request is way too hard for what it's worth (it's
probably broken right now), gauges are desynched and we are cloning
again and again String ids...

Signed-off-by: Eloi DEMOLIS 
---
 command/assets/custom_200.html      |   1 -
 command/assets/custom_404.html      |   1 -
 command/assets/custom_503.html      |   1 -
 command/src/state.rs                |   1 +
 e2e/src/http_utils/mod.rs           |  15 +-
 e2e/src/tests/tests.rs              |  20 +-
 lib/src/http.rs                     |  25 +-
 lib/src/https.rs                    |  23 +-
 lib/src/lib.rs                      |   4 +-
 lib/src/protocol/kawa_h1/answers.rs |  83 +++--
 lib/src/protocol/kawa_h1/editor.rs  |   2 +
 lib/src/protocol/kawa_h1/mod.rs     | 544 +++++++++++++---------------
 lib/src/router/mod.rs               |   5 +-
 lib/src/tcp.rs                      |  12 +-
 14 files changed, 360 insertions(+), 377 deletions(-)

diff --git a/command/assets/custom_200.html b/command/assets/custom_200.html
index 73e5926d9..12f2ca492 100644
--- a/command/assets/custom_200.html
+++ b/command/assets/custom_200.html
@@ -1,5 +1,4 @@
 HTTP/1.1 200 OK
-%Content-Length: %CONTENT_LENGTH
 Sozu-Id: %REQUEST_ID
 
 

%CLUSTER_ID Custom 200

diff --git a/command/assets/custom_404.html b/command/assets/custom_404.html index 8302d4319..e331b285e 100644 --- a/command/assets/custom_404.html +++ b/command/assets/custom_404.html @@ -1,6 +1,5 @@ HTTP/1.1 404 Not Found Cache-Control: no-cache -Connection: close Sozu-Id: %REQUEST_ID

My own 404 error page

diff --git a/command/assets/custom_503.html b/command/assets/custom_503.html index ac12b8825..4484f8a4e 100644 --- a/command/assets/custom_503.html +++ b/command/assets/custom_503.html @@ -1,7 +1,6 @@ HTTP/1.1 503 Service Unavailable Cache-Control: no-cache Connection: close -%Content-Length: %CONTENT_LENGTH Sozu-Id: %REQUEST_ID

MyCluster: 503 Service Unavailable

diff --git a/command/src/state.rs b/command/src/state.rs index 9413fa203..0d6524b23 100644 --- a/command/src/state.rs +++ b/command/src/state.rs @@ -1724,6 +1724,7 @@ mod tests { hostname: String::from("test.local"), path: PathRule::prefix(String::from("/abc")), address: SocketAddress::new_v4(0, 0, 0, 0, 8080), + required_auth: Some(false), redirect: Some(RedirectPolicy::Forward.into()), redirect_scheme: Some(RedirectScheme::UseSame.into()), ..Default::default() diff --git a/e2e/src/http_utils/mod.rs b/e2e/src/http_utils/mod.rs index 9e6248df2..345ebc1a3 100644 --- a/e2e/src/http_utils/mod.rs +++ b/e2e/src/http_utils/mod.rs @@ -24,12 +24,17 @@ pub fn http_request, S2: Into, S3: Into, S4: In ) } -pub fn immutable_answer(status: u16) -> String { +pub fn immutable_answer(status: u16, content_length: bool) -> String { + let content_length = if content_length { + "\r\nContent-Length: 0" + } else { + "" + }; match status { - 400 => String::from("HTTP/1.1 400 Bad Request\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), - 404 => String::from("HTTP/1.1 404 Not Found\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), - 502 => String::from("HTTP/1.1 502 Bad Gateway\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), - 503 => String::from("HTTP/1.1 503 Service Unavailable\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n"), + 400 => format!("HTTP/1.1 400 Bad Request\r\nCache-Control: no-cache\r\nConnection: close{content_length}\r\n\r\n"), + 404 => format!("HTTP/1.1 404 Not Found\r\nCache-Control: no-cache\r\nConnection: close{content_length}\r\n\r\n"), + 502 => format!("HTTP/1.1 502 Bad Gateway\r\nCache-Control: no-cache\r\nConnection: close{content_length}\r\n\r\n"), + 503 => format!("HTTP/1.1 503 Service Unavailable\r\nCache-Control: no-cache\r\nConnection: close{content_length}\r\n\r\n"), _ => unimplemented!() } } diff --git a/e2e/src/tests/tests.rs b/e2e/src/tests/tests.rs index 89727f53e..4fb54f908 100644 --- a/e2e/src/tests/tests.rs +++ b/e2e/src/tests/tests.rs @@ -645,10 +645,10 @@ fn try_http_behaviors() -> State { .to_http(None) .unwrap(); http_config.answers = BTreeMap::from([ - ("400".to_string(), immutable_answer(400)), - ("404".to_string(), immutable_answer(404)), - ("502".to_string(), immutable_answer(502)), - ("503".to_string(), immutable_answer(503)), + ("400".to_string(), immutable_answer(400, false)), + ("404".to_string(), immutable_answer(404, false)), + ("502".to_string(), immutable_answer(502, false)), + ("503".to_string(), immutable_answer(503, false)), ]); worker.send_proxy_request_type(RequestType::AddHttpListener(http_config)); @@ -671,7 +671,7 @@ fn try_http_behaviors() -> State { let response = client.receive(); println!("response: {response:?}"); - assert_eq!(response, Some(immutable_answer(404))); + assert_eq!(response, Some(immutable_answer(404, true))); assert_eq!(client.receive(), None); worker.send_proxy_request_type(RequestType::AddHttpFrontend(RequestHttpFrontend { @@ -686,7 +686,7 @@ fn try_http_behaviors() -> State { let response = client.receive(); println!("response: {response:?}"); - assert_eq!(response, Some(immutable_answer(503))); + assert_eq!(response, Some(immutable_answer(503, true))); assert_eq!(client.receive(), None); let back_address = create_local_address(); @@ -706,7 +706,7 @@ fn try_http_behaviors() -> State { let response = client.receive(); println!("response: {response:?}"); - assert_eq!(response, Some(immutable_answer(400))); + assert_eq!(response, Some(immutable_answer(400, true))); assert_eq!(client.receive(), None); let mut backend = SyncBackend::new("backend", back_address, "TEST\r\n\r\n"); @@ -723,7 +723,7 @@ fn try_http_behaviors() -> State { let response = client.receive(); println!("request: {request:?}"); println!("response: {response:?}"); - assert_eq!(response, Some(immutable_answer(502))); + assert_eq!(response, Some(immutable_answer(502, true))); assert_eq!(client.receive(), None); info!("expecting 200"); @@ -786,7 +786,7 @@ fn try_http_behaviors() -> State { let response = client.receive(); println!("request: {request:?}"); println!("response: {response:?}"); - assert_eq!(response, Some(immutable_answer(503))); + assert_eq!(response, Some(immutable_answer(503, true))); assert_eq!(client.receive(), None); worker.send_proxy_request_type(RequestType::RemoveBackend(RemoveBackend { @@ -984,7 +984,7 @@ fn try_https_redirect() -> State { client.connect(); client.send(); let answer = client.receive(); - let expected_answer = format!("{answer_301_prefix}https://example.com/redirected?true\r\n\r\n"); + let expected_answer = format!("{answer_301_prefix}https://example.com/redirected?true\r\nContent-Length: 0\r\n\r\n"); assert_eq!(answer, Some(expected_answer)); State::Success diff --git a/lib/src/http.rs b/lib/src/http.rs index a430c1f17..8c2b20c07 100644 --- a/lib/src/http.rs +++ b/lib/src/http.rs @@ -209,11 +209,14 @@ impl HttpSession { } } - fn upgrade_http(&mut self, http: Http) -> Option { + fn upgrade_http( + &mut self, + mut http: Http, + ) -> Option { debug!("http switching to ws"); - let front_token = self.frontend_token; - let back_token = match http.backend_token { - Some(back_token) => back_token, + let frontend_token = self.frontend_token; + let origin = match http.origin.take() { + Some(origin) => origin, None => { warn!( "Could not upgrade http request on cluster '{:?}' ({:?}) using backend '{:?}' into websocket for request '{}'", @@ -223,7 +226,7 @@ impl HttpSession { } }; - let ws_context = http.websocket_context(); + let websocket_context = http.websocket_context(); let mut container_frontend_timeout = http.container_frontend_timeout; let mut container_backend_timeout = http.container_backend_timeout; container_frontend_timeout.reset(); @@ -237,25 +240,25 @@ impl HttpSession { let mut pipe = Pipe::new( backend_buffer, - http.context.backend_id, - http.backend_socket, - http.backend, + Some(origin.backend_id), + Some(origin.socket), + Some(origin.backend), Some(container_backend_timeout), Some(container_frontend_timeout), http.context.cluster_id, http.request_stream.storage.buffer, - front_token, + frontend_token, http.frontend_socket, self.listener.clone(), Protocol::HTTP, http.context.id, http.context.session_address, - ws_context, + websocket_context, ); pipe.frontend_readiness.event = http.frontend_readiness.event; pipe.backend_readiness.event = http.backend_readiness.event; - pipe.set_back_token(back_token); + pipe.set_back_token(origin.token); gauge_add!("protocol.http", -1); gauge_add!("protocol.ws", 1); diff --git a/lib/src/https.rs b/lib/src/https.rs index abaf162c0..58e632ef3 100644 --- a/lib/src/https.rs +++ b/lib/src/https.rs @@ -329,11 +329,14 @@ impl HttpsSession { } } - fn upgrade_http(&self, http: Http) -> Option { + fn upgrade_http( + &self, + mut http: Http, + ) -> Option { debug!("https switching to wss"); let front_token = self.frontend_token; - let back_token = match http.backend_token { - Some(back_token) => back_token, + let origin = match http.origin.take() { + Some(origin) => origin, None => { warn!( "Could not upgrade https request on cluster '{:?}' ({:?}) using backend '{:?}' into secure websocket for request '{}'", @@ -343,7 +346,7 @@ impl HttpsSession { } }; - let ws_context = http.websocket_context(); + let websocket_context = http.websocket_context(); let mut container_frontend_timeout = http.container_frontend_timeout; let mut container_backend_timeout = http.container_backend_timeout; container_frontend_timeout.reset(); @@ -357,9 +360,9 @@ impl HttpsSession { let mut pipe = Pipe::new( backend_buffer, - http.context.backend_id, - http.backend_socket, - http.backend, + Some(origin.backend_id), + Some(origin.socket), + Some(origin.backend), Some(container_backend_timeout), Some(container_frontend_timeout), http.context.cluster_id, @@ -367,15 +370,15 @@ impl HttpsSession { front_token, http.frontend_socket, self.listener.clone(), - Protocol::HTTP, + Protocol::HTTPS, http.context.id, http.context.session_address, - ws_context, + websocket_context, ); pipe.frontend_readiness.event = http.frontend_readiness.event; pipe.backend_readiness.event = http.backend_readiness.event; - pipe.set_back_token(back_token); + pipe.set_back_token(origin.token); gauge_add!("protocol.https", -1); gauge_add!("protocol.wss", 1); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 311e83f16..eb858aafe 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -953,7 +953,6 @@ pub struct SessionMetrics { pub service_start: Option, pub wait_start: Instant, - pub backend_id: Option, pub backend_start: Option, pub backend_connected: Option, pub backend_stop: Option, @@ -971,7 +970,6 @@ impl SessionMetrics { bout: 0, service_start: None, wait_start: Instant::now(), - backend_id: None, backend_start: None, backend_connected: None, backend_stop: None, @@ -1072,7 +1070,7 @@ impl SessionMetrics { time!("request_time", request_time.as_millis()); time!("service_time", service_time.as_millis()); - if let Some(backend_id) = self.backend_id.as_ref() { + if let Some(backend_id) = context.backend_id { if let Some(backend_response_time) = self.backend_response_time() { record_backend_metrics!( context.cluster_id.as_str_or("-"), diff --git a/lib/src/protocol/kawa_h1/answers.rs b/lib/src/protocol/kawa_h1/answers.rs index 6a8ae2b76..a4b4651ad 100644 --- a/lib/src/protocol/kawa_h1/answers.rs +++ b/lib/src/protocol/kawa_h1/answers.rs @@ -1,7 +1,7 @@ use crate::{protocol::http::DefaultAnswer, sozu_command::state::ClusterId}; use kawa::{ - h1::NoCallbacks, AsBuffer, Block, BodySize, Buffer, Chunk, Kawa, Kind, Pair, ParsingPhase, - ParsingPhaseMarker, StatusLine, Store, + h1::NoCallbacks, AsBuffer, Block, BodySize, Buffer, Chunk, Flags, Kawa, Kind, Pair, + ParsingPhase, ParsingPhaseMarker, StatusLine, Store, }; use nom::AsBytes; use std::{ @@ -11,6 +11,8 @@ use std::{ str::from_utf8_unchecked, }; +use super::parser::compare_no_case; + #[derive(Clone)] pub struct SharedBuffer(Rc<[u8]>); @@ -34,6 +36,8 @@ pub enum TemplateError { InvalidTemplate(ParsingPhase), #[error("unexpected status code: {0}")] InvalidStatusCode(u16), + #[error("unexpected size info: {0:?}")] + InvalidSizeInfo(BodySize), #[error("streaming is not supported in templates")] UnsupportedStreaming, #[error("template variable {0} is not allowed in headers")] @@ -68,6 +72,7 @@ pub struct Replacement { // TODO: rename for clarity, for instance HttpAnswerTemplate pub struct Template { status: u16, + keep_alive: bool, kawa: DefaultAnswerStream, body_replacements: Vec, header_replacements: Vec, @@ -126,6 +131,9 @@ impl Template { if !kawa.is_main_phase() { return Err(TemplateError::InvalidTemplate(kawa.parsing_phase)); } + if kawa.body_size != BodySize::Empty { + return Err(TemplateError::InvalidSizeInfo(kawa.body_size)); + } let status = if let StatusLine::Response { code, .. } = &kawa.detached.status_line { if let Some(expected_code) = status { if expected_code != *code { @@ -141,16 +149,35 @@ impl Template { let mut header_replacements = Vec::new(); let mut body_replacements = Vec::new(); let mut body_size = 0; + let mut keep_alive = true; let mut used_once = Vec::new(); for mut block in kawa.blocks.into_iter() { match &mut block { Block::ChunkHeader(_) => return Err(TemplateError::UnsupportedStreaming), + Block::Flags(Flags { + end_header: true, .. + }) => { + header_replacements.push(Replacement { + block_index: blocks.len(), + typ: ReplacementType::ContentLength, + }); + blocks.push_back(Block::Header(Pair { + key: Store::Static(b"Content-Length"), + val: Store::Static(b"PLACEHOLDER"), + })); + blocks.push_back(block); + } Block::StatusLine | Block::Cookies | Block::Flags(_) => { blocks.push_back(block); } Block::Header(Pair { key, val }) => { let val_data = val.data(buf); let key_data = key.data(buf); + if compare_no_case(key_data, b"connection") + && compare_no_case(val_data, b"close") + { + keep_alive = false; + } if let Some(b'%') = val_data.first() { for variable in &variables { if &val_data[1..] == variable.name.as_bytes() { @@ -168,11 +195,7 @@ impl Template { } used_once.push(var_index); } - ReplacementType::ContentLength => { - if let Some(b'%') = key_data.first() { - *key = Store::new_slice(buf, &key_data[1..]); - } - } + ReplacementType::ContentLength => {} } header_replacements.push(Replacement { block_index: blocks.len(), @@ -240,6 +263,7 @@ impl Template { kawa.blocks = blocks; Ok(Self { status, + keep_alive, kawa, body_replacements, header_replacements, @@ -306,7 +330,6 @@ pub struct HttpAnswers { } // const HEADERS: &str = "Connection: close\r -// Content-Length: 0\r // Sozu-Id: %REQUEST_ID\r // \r"; // const STYLE: &str = "