diff --git a/command/src/command.proto b/command/src/command.proto index d929e0ba1..e2fa453c5 100644 --- a/command/src/command.proto +++ b/command/src/command.proto @@ -371,7 +371,8 @@ message Cluster { optional LoadMetric load_metric = 7; optional uint32 https_redirect_port = 8; map answers = 9; - repeated uint64 authorized_hashes = 10; + repeated string authorized_hashes = 10; + optional string www_authenticate = 11; } enum LoadBalancingAlgorithms { diff --git a/command/src/config.rs b/command/src/config.rs index dc7290361..b785fee6e 100644 --- a/command/src/config.rs +++ b/command/src/config.rs @@ -210,13 +210,15 @@ pub enum ConfigError { }, #[error("Invalid '{0}' field for a TCP frontend")] InvalidFrontendConfig(String), - #[error("invalid path {0:?}")] + #[error("Invalid path {0:?}")] InvalidPath(PathBuf), - #[error("listening address {0:?} is already used in the configuration")] + #[error("Invalid Sha256 hash '{0}'")] + InvalidHash(String), + #[error("Listening address {0:?} is already used in the configuration")] ListenerAddressAlreadyInUse(SocketAddr), - #[error("missing {0:?}")] + #[error("Missing {0:?}")] Missing(MissingKind), - #[error("could not get parent directory for file {0}")] + #[error("Could not get parent directory for file {0}")] NoFileParent(String), #[error("Could not get the path of the saved state")] SaveStatePath(String), @@ -761,7 +763,9 @@ pub struct FileClusterConfig { #[serde(default)] pub answers: Option>, #[serde(default)] - pub authorized_hashes: Vec, + pub authorized_hashes: Vec, + #[serde(default)] + pub www_authenticate: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -838,6 +842,17 @@ impl FileClusterConfig { let http_frontend = frontend.to_http_front(cluster_id)?; frontends.push(http_frontend); } + self.authorized_hashes + .iter() + .map(|hash| { + hex::decode(&hash) + .map_err(|_| ConfigError::InvalidHash(hash.clone())) + .and_then(|v| { + v.try_into() + .map_err(|_| ConfigError::InvalidHash(hash.clone())) + }) + }) + .collect::, ConfigError>>()?; Ok(ClusterConfig::Http(HttpClusterConfig { cluster_id: cluster_id.to_string(), @@ -850,6 +865,7 @@ impl FileClusterConfig { load_metric: self.load_metric, answers: load_answers(self.answers.as_ref())?, authorized_hashes: self.authorized_hashes, + www_authenticate: self.www_authenticate, })) } } @@ -964,7 +980,8 @@ pub struct HttpClusterConfig { pub load_balancing: LoadBalancingAlgorithms, pub load_metric: Option, pub answers: BTreeMap, - pub authorized_hashes: Vec, + pub authorized_hashes: Vec, + pub www_authenticate: Option, } impl HttpClusterConfig { @@ -979,6 +996,7 @@ impl HttpClusterConfig { load_metric: self.load_metric.map(|s| s as i32), answers: self.answers.clone(), authorized_hashes: self.authorized_hashes.clone(), + www_authenticate: self.www_authenticate.clone(), }) .into()]; @@ -1040,6 +1058,7 @@ impl TcpClusterConfig { load_metric: self.load_metric.map(|s| s as i32), answers: Default::default(), authorized_hashes: Default::default(), + www_authenticate: None, }) .into()]; diff --git a/e2e/src/tests/tests.rs b/e2e/src/tests/tests.rs index afa32de7a..e2cd874c1 100644 --- a/e2e/src/tests/tests.rs +++ b/e2e/src/tests/tests.rs @@ -1241,7 +1241,7 @@ pub fn try_stick() -> State { backend1.send(0); let response = client.receive(); println!("response: {response:?}"); - assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:")); + assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nContent-Length: 0\r\nX-Forwarded-For:")); assert!(response.unwrap().starts_with("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: SOZUBALANCEID=sticky_cluster_0-0; Path=/\r\nSozu-Id:")); // invalid sticky_session @@ -1254,7 +1254,7 @@ pub fn try_stick() -> State { backend2.send(0); let response = client.receive(); println!("response: {response:?}"); - assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:")); + assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nContent-Length: 0\r\nX-Forwarded-For:")); assert!(response.unwrap().starts_with("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: SOZUBALANCEID=sticky_cluster_0-1; Path=/\r\nSozu-Id:")); // good sticky_session (force use backend2, round-robin would have chosen backend1) @@ -1267,7 +1267,7 @@ pub fn try_stick() -> State { backend2.send(0); let response = client.receive(); println!("response: {response:?}"); - assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:")); + assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nContent-Length: 0\r\nX-Forwarded-For:")); assert!(response .unwrap() .starts_with("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSozu-Id:")); diff --git a/lib/src/protocol/kawa_h1/answers.rs b/lib/src/protocol/kawa_h1/answers.rs index a4b4651ad..4069c6f77 100644 --- a/lib/src/protocol/kawa_h1/answers.rs +++ b/lib/src/protocol/kawa_h1/answers.rs @@ -53,6 +53,7 @@ pub struct TemplateVariable { name: &'static str, valid_in_body: bool, valid_in_header: bool, + or_elide_header: bool, typ: ReplacementType, } @@ -66,6 +67,7 @@ pub enum ReplacementType { #[derive(Clone, Copy, Debug)] pub struct Replacement { block_index: usize, + or_elide_header: bool, typ: ReplacementType, } @@ -159,6 +161,7 @@ impl Template { }) => { header_replacements.push(Replacement { block_index: blocks.len(), + or_elide_header: false, typ: ReplacementType::ContentLength, }); blocks.push_back(Block::Header(Pair { @@ -199,6 +202,7 @@ impl Template { } header_replacements.push(Replacement { block_index: blocks.len(), + or_elide_header: variable.or_elide_header, typ: variable.typ, }); break; @@ -241,6 +245,7 @@ impl Template { } body_replacements.push(Replacement { block_index: blocks.len(), + or_elide_header: false, typ: variable.typ, }); blocks.push_back(Block::Chunk(Chunk { @@ -307,6 +312,10 @@ impl Template { pair.val = Store::from_string(body_size.to_string()) } } + if pair.val.len() == 0 && replacement.or_elide_header { + pair.elide(); + continue; + } } } Kawa { @@ -429,6 +438,7 @@ fn default_401() -> String { String::from( "\ HTTP/1.1 401 Unauthorized\r +WWW-Authenticate: %WWW_AUTHENTICATE\r Cache-Control: no-cache\r Connection: close\r Sozu-Id: %REQUEST_ID\r @@ -660,42 +670,49 @@ impl HttpAnswers { name: "ROUTE", valid_in_body: true, valid_in_header: true, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let request_id = TemplateVariable { name: "REQUEST_ID", valid_in_body: true, valid_in_header: true, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let cluster_id = TemplateVariable { name: "CLUSTER_ID", valid_in_body: true, valid_in_header: true, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let backend_id = TemplateVariable { name: "BACKEND_ID", valid_in_body: true, valid_in_header: true, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let duration = TemplateVariable { name: "DURATION", valid_in_body: true, valid_in_header: true, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let capacity = TemplateVariable { name: "CAPACITY", valid_in_body: true, valid_in_header: true, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let phase = TemplateVariable { name: "PHASE", valid_in_body: true, valid_in_header: true, + or_elide_header: false, typ: ReplacementType::Variable(0), }; @@ -703,36 +720,49 @@ impl HttpAnswers { name: "REDIRECT_LOCATION", valid_in_body: true, valid_in_header: true, + or_elide_header: false, + typ: ReplacementType::VariableOnce(0), + }; + let www_authenticate = TemplateVariable { + name: "WWW_AUTHENTICATE", + valid_in_body: false, + valid_in_header: true, + or_elide_header: true, typ: ReplacementType::VariableOnce(0), }; let message = TemplateVariable { name: "MESSAGE", valid_in_body: true, valid_in_header: false, + or_elide_header: false, typ: ReplacementType::VariableOnce(0), }; let successfully_parsed = TemplateVariable { name: "SUCCESSFULLY_PARSED", valid_in_body: true, valid_in_header: false, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let partially_parsed = TemplateVariable { name: "PARTIALLY_PARSED", valid_in_body: true, valid_in_header: false, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let invalid = TemplateVariable { name: "INVALID", valid_in_body: true, valid_in_header: false, + or_elide_header: false, typ: ReplacementType::Variable(0), }; let template_name = TemplateVariable { name: "TEMPLATE_NAME", valid_in_body: true, valid_in_header: true, + or_elide_header: false, typ: ReplacementType::Variable(0), }; @@ -750,7 +780,7 @@ impl HttpAnswers { "401" => Template::new( Some(401), answer, - &[route, request_id] + &[route, request_id, www_authenticate] ), "404" => Template::new( Some(404), @@ -883,9 +913,9 @@ impl HttpAnswers { variables_once = vec![message.into()]; "400" } - DefaultAnswer::Answer401 {} => { + DefaultAnswer::Answer401 { www_authenticate } => { variables = vec![route.into(), request_id.into()]; - variables_once = vec![]; + variables_once = vec![www_authenticate.map(Into::into).unwrap_or_default()]; "401" } DefaultAnswer::Answer404 {} => { diff --git a/lib/src/protocol/kawa_h1/editor.rs b/lib/src/protocol/kawa_h1/editor.rs index 4d12f1036..70f9c1b82 100644 --- a/lib/src/protocol/kawa_h1/editor.rs +++ b/lib/src/protocol/kawa_h1/editor.rs @@ -5,6 +5,7 @@ use std::{ }; use rusty_ulid::Ulid; +use sha2::{Digest, Sha256}; use crate::{ pool::Checkout, @@ -24,8 +25,8 @@ pub struct HttpContext { pub keep_alive_frontend: bool, /// the value of the sticky session cookie in the request pub sticky_session_found: Option, - /// hashed value of the last authentication header - pub authentication_found: Option, + /// hashed value of the last authorization header + pub authorization_found: Option, /// position of the last header (the "Sozu-Id"), only valid until prepare is called pub last_header: Option, // ---------- Status Line @@ -139,11 +140,14 @@ impl HttpContext { // - store X-Forwarded-For // - store Forwarded // - store User-Agent + // - compute sha256 of Authorization let mut x_for = None; let mut forwarded = None; let mut has_x_port = false; let mut has_x_proto = false; let mut has_connection = false; + + let mut auth = None; for block in &mut request.blocks { match block { kawa::Block::Header(header) if !header.is_elided() => { @@ -191,18 +195,18 @@ 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 = header.val.data_opt(buf).map(|auth| { - let mut h = DefaultHasher::new(); - auth.hash(&mut h); - h.finish() - }); + } else if compare_no_case(key, b"Authorization") { + auth = Some(header); } } _ => {} } } + self.authorization_found = auth + .and_then(|header| header.val.data_opt(buf)) + .map(|auth| hex::encode(Sha256::digest(auth))); + // If session_address is set: // - append its ip address to the list of "X-Forwarded-For" if it was found, creates it if not // - append "proto=[PROTO];for=[PEER];by=[PUBLIC]" to the list of "Forwarded" if it was found, creates it if not diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs index 506e6f84f..452d04274 100644 --- a/lib/src/protocol/kawa_h1/mod.rs +++ b/lib/src/protocol/kawa_h1/mod.rs @@ -91,7 +91,9 @@ pub enum DefaultAnswer { partially_parsed: String, invalid: String, }, - Answer401 {}, + Answer401 { + www_authenticate: Option, + }, Answer404 {}, Answer408 { duration: String, @@ -259,7 +261,7 @@ impl Http Http "https", }; - let (authorized, https_redirect, https_redirect_port) = + let (authorized, www_authenticate, https_redirect, https_redirect_port) = match (&cluster_id, redirect, &redirect_template, required_auth) { // unauthorized frontends - (_, RedirectPolicy::Unauthorized, _, _) => (false, false, None), + (_, RedirectPolicy::Unauthorized, _, _) => (false, None, false, None), // forward frontends with no target (no cluster nor template) - (None, RedirectPolicy::Forward, None, _) => (false, false, None), + (None, RedirectPolicy::Forward, None, _) => (false, None, false, None), // clusterless frontend with auth (unsupported) - (None, _, _, true) => (false, false, None), + (None, _, _, true) => (false, None, false, None), // clusterless frontends - (None, _, _, false) => (true, false, None), + (None, _, _, false) => (true, None, false, None), // "attached" frontends (Some(cluster_id), _, _, _) => { proxy.borrow().clusters().get(cluster_id).map_or( - (true, false, None), // cluster not found, consider authorized? + (true, None, false, None), // cluster not found, consider authorized? |cluster| { let authorized = - match (required_auth, self.context.authentication_found) { + match (required_auth, &self.context.authorization_found) { // auth not required (false, _) => true, // no auth found @@ -1348,11 +1350,12 @@ impl Http { println!("{hash:?}"); - cluster.authorized_hashes.contains(&hash) + cluster.authorized_hashes.contains(hash) } }; ( authorized, + cluster.www_authenticate.clone(), cluster.https_redirect, cluster.https_redirect_port, ) @@ -1388,7 +1391,7 @@ impl Http { self.context.cluster_id = cluster_id; - self.set_answer(DefaultAnswer::Answer401 {}); + self.set_answer(DefaultAnswer::Answer401 { www_authenticate }); Err(RetrieveClusterError::UnauthorizedRoute) } } @@ -1465,8 +1468,7 @@ impl Http