Skip to content

Commit 1309262

Browse files
committed
Finalize authentication
- Use Sha256 hashes - Add WWW_AUTHENTICATE variable to 401 templates Signed-off-by: Eloi DEMOLIS <eloi.demolis@clever-cloud.com>
1 parent 309c091 commit 1309262

File tree

6 files changed

+90
-34
lines changed

6 files changed

+90
-34
lines changed

command/src/command.proto

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,8 @@ message Cluster {
371371
optional LoadMetric load_metric = 7;
372372
optional uint32 https_redirect_port = 8;
373373
map<string, string> answers = 9;
374-
repeated uint64 authorized_hashes = 10;
374+
repeated string authorized_hashes = 10;
375+
optional string www_authenticate = 11;
375376
}
376377

377378
enum LoadBalancingAlgorithms {

command/src/config.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,15 @@ pub enum ConfigError {
210210
},
211211
#[error("Invalid '{0}' field for a TCP frontend")]
212212
InvalidFrontendConfig(String),
213-
#[error("invalid path {0:?}")]
213+
#[error("Invalid path {0:?}")]
214214
InvalidPath(PathBuf),
215-
#[error("listening address {0:?} is already used in the configuration")]
215+
#[error("Invalid Sha256 hash '{0}'")]
216+
InvalidHash(String),
217+
#[error("Listening address {0:?} is already used in the configuration")]
216218
ListenerAddressAlreadyInUse(SocketAddr),
217-
#[error("missing {0:?}")]
219+
#[error("Missing {0:?}")]
218220
Missing(MissingKind),
219-
#[error("could not get parent directory for file {0}")]
221+
#[error("Could not get parent directory for file {0}")]
220222
NoFileParent(String),
221223
#[error("Could not get the path of the saved state")]
222224
SaveStatePath(String),
@@ -761,7 +763,9 @@ pub struct FileClusterConfig {
761763
#[serde(default)]
762764
pub answers: Option<BTreeMap<String, String>>,
763765
#[serde(default)]
764-
pub authorized_hashes: Vec<u64>,
766+
pub authorized_hashes: Vec<String>,
767+
#[serde(default)]
768+
pub www_authenticate: Option<String>,
765769
}
766770

767771
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -838,6 +842,17 @@ impl FileClusterConfig {
838842
let http_frontend = frontend.to_http_front(cluster_id)?;
839843
frontends.push(http_frontend);
840844
}
845+
self.authorized_hashes
846+
.iter()
847+
.map(|hash| {
848+
hex::decode(&hash)
849+
.map_err(|_| ConfigError::InvalidHash(hash.clone()))
850+
.and_then(|v| {
851+
v.try_into()
852+
.map_err(|_| ConfigError::InvalidHash(hash.clone()))
853+
})
854+
})
855+
.collect::<Result<Vec<[u8; 32]>, ConfigError>>()?;
841856

842857
Ok(ClusterConfig::Http(HttpClusterConfig {
843858
cluster_id: cluster_id.to_string(),
@@ -850,6 +865,7 @@ impl FileClusterConfig {
850865
load_metric: self.load_metric,
851866
answers: load_answers(self.answers.as_ref())?,
852867
authorized_hashes: self.authorized_hashes,
868+
www_authenticate: self.www_authenticate,
853869
}))
854870
}
855871
}
@@ -964,7 +980,8 @@ pub struct HttpClusterConfig {
964980
pub load_balancing: LoadBalancingAlgorithms,
965981
pub load_metric: Option<LoadMetric>,
966982
pub answers: BTreeMap<String, String>,
967-
pub authorized_hashes: Vec<u64>,
983+
pub authorized_hashes: Vec<String>,
984+
pub www_authenticate: Option<String>,
968985
}
969986

970987
impl HttpClusterConfig {
@@ -979,6 +996,7 @@ impl HttpClusterConfig {
979996
load_metric: self.load_metric.map(|s| s as i32),
980997
answers: self.answers.clone(),
981998
authorized_hashes: self.authorized_hashes.clone(),
999+
www_authenticate: self.www_authenticate.clone(),
9821000
})
9831001
.into()];
9841002

@@ -1040,6 +1058,7 @@ impl TcpClusterConfig {
10401058
load_metric: self.load_metric.map(|s| s as i32),
10411059
answers: Default::default(),
10421060
authorized_hashes: Default::default(),
1061+
www_authenticate: None,
10431062
})
10441063
.into()];
10451064

e2e/src/tests/tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ pub fn try_stick() -> State {
12411241
backend1.send(0);
12421242
let response = client.receive();
12431243
println!("response: {response:?}");
1244-
assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:"));
1244+
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:"));
12451245
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:"));
12461246

12471247
// invalid sticky_session
@@ -1254,7 +1254,7 @@ pub fn try_stick() -> State {
12541254
backend2.send(0);
12551255
let response = client.receive();
12561256
println!("response: {response:?}");
1257-
assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:"));
1257+
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:"));
12581258
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:"));
12591259

12601260
// good sticky_session (force use backend2, round-robin would have chosen backend1)
@@ -1267,7 +1267,7 @@ pub fn try_stick() -> State {
12671267
backend2.send(0);
12681268
let response = client.receive();
12691269
println!("response: {response:?}");
1270-
assert!(request.unwrap().starts_with("GET /api HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nCookie: foo=bar\r\nX-Forwarded-For:"));
1270+
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:"));
12711271
assert!(response
12721272
.unwrap()
12731273
.starts_with("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSozu-Id:"));

lib/src/protocol/kawa_h1/answers.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub struct TemplateVariable {
5353
name: &'static str,
5454
valid_in_body: bool,
5555
valid_in_header: bool,
56+
or_elide_header: bool,
5657
typ: ReplacementType,
5758
}
5859

@@ -66,6 +67,7 @@ pub enum ReplacementType {
6667
#[derive(Clone, Copy, Debug)]
6768
pub struct Replacement {
6869
block_index: usize,
70+
or_elide_header: bool,
6971
typ: ReplacementType,
7072
}
7173

@@ -159,6 +161,7 @@ impl Template {
159161
}) => {
160162
header_replacements.push(Replacement {
161163
block_index: blocks.len(),
164+
or_elide_header: false,
162165
typ: ReplacementType::ContentLength,
163166
});
164167
blocks.push_back(Block::Header(Pair {
@@ -199,6 +202,7 @@ impl Template {
199202
}
200203
header_replacements.push(Replacement {
201204
block_index: blocks.len(),
205+
or_elide_header: variable.or_elide_header,
202206
typ: variable.typ,
203207
});
204208
break;
@@ -241,6 +245,7 @@ impl Template {
241245
}
242246
body_replacements.push(Replacement {
243247
block_index: blocks.len(),
248+
or_elide_header: false,
244249
typ: variable.typ,
245250
});
246251
blocks.push_back(Block::Chunk(Chunk {
@@ -307,6 +312,10 @@ impl Template {
307312
pair.val = Store::from_string(body_size.to_string())
308313
}
309314
}
315+
if pair.val.len() == 0 && replacement.or_elide_header {
316+
pair.elide();
317+
continue;
318+
}
310319
}
311320
}
312321
Kawa {
@@ -429,6 +438,7 @@ fn default_401() -> String {
429438
String::from(
430439
"\
431440
HTTP/1.1 401 Unauthorized\r
441+
WWW-Authenticate: %WWW_AUTHENTICATE\r
432442
Cache-Control: no-cache\r
433443
Connection: close\r
434444
Sozu-Id: %REQUEST_ID\r
@@ -660,79 +670,99 @@ impl HttpAnswers {
660670
name: "ROUTE",
661671
valid_in_body: true,
662672
valid_in_header: true,
673+
or_elide_header: false,
663674
typ: ReplacementType::Variable(0),
664675
};
665676
let request_id = TemplateVariable {
666677
name: "REQUEST_ID",
667678
valid_in_body: true,
668679
valid_in_header: true,
680+
or_elide_header: false,
669681
typ: ReplacementType::Variable(0),
670682
};
671683
let cluster_id = TemplateVariable {
672684
name: "CLUSTER_ID",
673685
valid_in_body: true,
674686
valid_in_header: true,
687+
or_elide_header: false,
675688
typ: ReplacementType::Variable(0),
676689
};
677690
let backend_id = TemplateVariable {
678691
name: "BACKEND_ID",
679692
valid_in_body: true,
680693
valid_in_header: true,
694+
or_elide_header: false,
681695
typ: ReplacementType::Variable(0),
682696
};
683697
let duration = TemplateVariable {
684698
name: "DURATION",
685699
valid_in_body: true,
686700
valid_in_header: true,
701+
or_elide_header: false,
687702
typ: ReplacementType::Variable(0),
688703
};
689704
let capacity = TemplateVariable {
690705
name: "CAPACITY",
691706
valid_in_body: true,
692707
valid_in_header: true,
708+
or_elide_header: false,
693709
typ: ReplacementType::Variable(0),
694710
};
695711
let phase = TemplateVariable {
696712
name: "PHASE",
697713
valid_in_body: true,
698714
valid_in_header: true,
715+
or_elide_header: false,
699716
typ: ReplacementType::Variable(0),
700717
};
701718

702719
let location = TemplateVariable {
703720
name: "REDIRECT_LOCATION",
704721
valid_in_body: true,
705722
valid_in_header: true,
723+
or_elide_header: false,
724+
typ: ReplacementType::VariableOnce(0),
725+
};
726+
let www_authenticate = TemplateVariable {
727+
name: "WWW_AUTHENTICATE",
728+
valid_in_body: false,
729+
valid_in_header: true,
730+
or_elide_header: true,
706731
typ: ReplacementType::VariableOnce(0),
707732
};
708733
let message = TemplateVariable {
709734
name: "MESSAGE",
710735
valid_in_body: true,
711736
valid_in_header: false,
737+
or_elide_header: false,
712738
typ: ReplacementType::VariableOnce(0),
713739
};
714740
let successfully_parsed = TemplateVariable {
715741
name: "SUCCESSFULLY_PARSED",
716742
valid_in_body: true,
717743
valid_in_header: false,
744+
or_elide_header: false,
718745
typ: ReplacementType::Variable(0),
719746
};
720747
let partially_parsed = TemplateVariable {
721748
name: "PARTIALLY_PARSED",
722749
valid_in_body: true,
723750
valid_in_header: false,
751+
or_elide_header: false,
724752
typ: ReplacementType::Variable(0),
725753
};
726754
let invalid = TemplateVariable {
727755
name: "INVALID",
728756
valid_in_body: true,
729757
valid_in_header: false,
758+
or_elide_header: false,
730759
typ: ReplacementType::Variable(0),
731760
};
732761
let template_name = TemplateVariable {
733762
name: "TEMPLATE_NAME",
734763
valid_in_body: true,
735764
valid_in_header: true,
765+
or_elide_header: false,
736766
typ: ReplacementType::Variable(0),
737767
};
738768

@@ -750,7 +780,7 @@ impl HttpAnswers {
750780
"401" => Template::new(
751781
Some(401),
752782
answer,
753-
&[route, request_id]
783+
&[route, request_id, www_authenticate]
754784
),
755785
"404" => Template::new(
756786
Some(404),
@@ -883,9 +913,9 @@ impl HttpAnswers {
883913
variables_once = vec![message.into()];
884914
"400"
885915
}
886-
DefaultAnswer::Answer401 {} => {
916+
DefaultAnswer::Answer401 { www_authenticate } => {
887917
variables = vec![route.into(), request_id.into()];
888-
variables_once = vec![];
918+
variables_once = vec![www_authenticate.map(Into::into).unwrap_or_default()];
889919
"401"
890920
}
891921
DefaultAnswer::Answer404 {} => {

lib/src/protocol/kawa_h1/editor.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::{
55
};
66

77
use rusty_ulid::Ulid;
8+
use sha2::{Digest, Sha256};
89

910
use crate::{
1011
pool::Checkout,
@@ -24,8 +25,8 @@ pub struct HttpContext {
2425
pub keep_alive_frontend: bool,
2526
/// the value of the sticky session cookie in the request
2627
pub sticky_session_found: Option<String>,
27-
/// hashed value of the last authentication header
28-
pub authentication_found: Option<u64>,
28+
/// hashed value of the last authorization header
29+
pub authorization_found: Option<String>,
2930
/// position of the last header (the "Sozu-Id"), only valid until prepare is called
3031
pub last_header: Option<usize>,
3132
// ---------- Status Line
@@ -139,11 +140,14 @@ impl HttpContext {
139140
// - store X-Forwarded-For
140141
// - store Forwarded
141142
// - store User-Agent
143+
// - compute sha256 of Authorization
142144
let mut x_for = None;
143145
let mut forwarded = None;
144146
let mut has_x_port = false;
145147
let mut has_x_proto = false;
146148
let mut has_connection = false;
149+
150+
let mut auth = None;
147151
for block in &mut request.blocks {
148152
match block {
149153
kawa::Block::Header(header) if !header.is_elided() => {
@@ -191,18 +195,18 @@ impl HttpContext {
191195
.data_opt(buf)
192196
.and_then(|data| from_utf8(data).ok())
193197
.map(ToOwned::to_owned);
194-
} else if compare_no_case(key, b"Proxy-Authenticate") {
195-
self.authentication_found = header.val.data_opt(buf).map(|auth| {
196-
let mut h = DefaultHasher::new();
197-
auth.hash(&mut h);
198-
h.finish()
199-
});
198+
} else if compare_no_case(key, b"Authorization") {
199+
auth = Some(header);
200200
}
201201
}
202202
_ => {}
203203
}
204204
}
205205

206+
self.authorization_found = auth
207+
.and_then(|header| header.val.data_opt(buf))
208+
.map(|auth| hex::encode(Sha256::digest(auth)));
209+
206210
// If session_address is set:
207211
// - append its ip address to the list of "X-Forwarded-For" if it was found, creates it if not
208212
// - append "proto=[PROTO];for=[PEER];by=[PUBLIC]" to the list of "Forwarded" if it was found, creates it if not

0 commit comments

Comments
 (0)