Skip to content

Commit

Permalink
Finalize authentication
Browse files Browse the repository at this point in the history
- Use Sha256 hashes
- Add WWW_AUTHENTICATE variable to 401 templates

Signed-off-by: Eloi DEMOLIS <[email protected]>
  • Loading branch information
Wonshtrum committed Feb 3, 2025
1 parent 309c091 commit 1309262
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 34 deletions.
3 changes: 2 additions & 1 deletion command/src/command.proto
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,8 @@ message Cluster {
optional LoadMetric load_metric = 7;
optional uint32 https_redirect_port = 8;
map<string, string> answers = 9;
repeated uint64 authorized_hashes = 10;
repeated string authorized_hashes = 10;
optional string www_authenticate = 11;
}

enum LoadBalancingAlgorithms {
Expand Down
31 changes: 25 additions & 6 deletions command/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -761,7 +763,9 @@ pub struct FileClusterConfig {
#[serde(default)]
pub answers: Option<BTreeMap<String, String>>,
#[serde(default)]
pub authorized_hashes: Vec<u64>,
pub authorized_hashes: Vec<String>,
#[serde(default)]
pub www_authenticate: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
Expand Down Expand Up @@ -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::<Result<Vec<[u8; 32]>, ConfigError>>()?;

Ok(ClusterConfig::Http(HttpClusterConfig {
cluster_id: cluster_id.to_string(),
Expand All @@ -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,
}))
}
}
Expand Down Expand Up @@ -964,7 +980,8 @@ pub struct HttpClusterConfig {
pub load_balancing: LoadBalancingAlgorithms,
pub load_metric: Option<LoadMetric>,
pub answers: BTreeMap<String, String>,
pub authorized_hashes: Vec<u64>,
pub authorized_hashes: Vec<String>,
pub www_authenticate: Option<String>,
}

impl HttpClusterConfig {
Expand All @@ -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()];

Expand Down Expand Up @@ -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()];

Expand Down
6 changes: 3 additions & 3 deletions e2e/src/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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:"));
Expand Down
36 changes: 33 additions & 3 deletions lib/src/protocol/kawa_h1/answers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub struct TemplateVariable {
name: &'static str,
valid_in_body: bool,
valid_in_header: bool,
or_elide_header: bool,
typ: ReplacementType,
}

Expand All @@ -66,6 +67,7 @@ pub enum ReplacementType {
#[derive(Clone, Copy, Debug)]
pub struct Replacement {
block_index: usize,
or_elide_header: bool,
typ: ReplacementType,
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -660,79 +670,99 @@ 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),
};

let location = TemplateVariable {
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),
};

Expand All @@ -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),
Expand Down Expand Up @@ -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 {} => {
Expand Down
20 changes: 12 additions & 8 deletions lib/src/protocol/kawa_h1/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{
};

use rusty_ulid::Ulid;
use sha2::{Digest, Sha256};

use crate::{
pool::Checkout,
Expand All @@ -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<String>,
/// hashed value of the last authentication header
pub authentication_found: Option<u64>,
/// hashed value of the last authorization header
pub authorization_found: Option<String>,
/// position of the last header (the "Sozu-Id"), only valid until prepare is called
pub last_header: Option<usize>,
// ---------- Status Line
Expand Down Expand Up @@ -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() => {
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 1309262

Please sign in to comment.