From 663b67f347cd891e441b610e1ecda72d6b4df515 Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Tue, 17 Dec 2024 21:12:02 +0100 Subject: [PATCH] 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 {