object_storage_proxy/parsers/
credentials.rs1use std::collections::HashMap;
2
3use nom::{
4 IResult, Parser,
5 bytes::complete::{tag, take_until, take_while1},
6 multi::separated_list1,
7 sequence::{preceded, separated_pair},
8};
9
10use nom::character::complete::char as nomchar;
11use nom::error::{Error, ErrorKind, make_error};
12use percent_encoding::percent_decode_str;
13
14fn miss(i: &str) -> nom::Err<Error<&str>> {
15 nom::Err::Error(make_error(i, ErrorKind::Tag))
16}
17
18pub fn parse_token_from_header(header: &str) -> IResult<&str, &str> {
27 let (_, token) =
28 (preceded(tag("AWS4-HMAC-SHA256 Credential="), take_until("/"))).parse(header)?;
29
30 Ok(("", token))
31}
32
33pub fn parse_credential_scope(input: &str) -> IResult<&str, (&str, &str)> {
46 let (input, _) = take_until("Credential=")(input)?;
47 let (remaining, (_, _, _, _, _, region, _, service, _)) = (
48 tag("Credential="), take_until("/"), tag("/"),
51 take_until("/"), tag("/"),
53 take_until("/"), tag("/"),
55 take_until("/aws4_request"), tag("/aws4_request"), )
58 .parse(input)?;
59 Ok((remaining, (region, service)))
60}
61
62#[derive(Debug, PartialEq)]
66pub struct PresignedParams {
67 pub algorithm: String,
69 pub access_key: String,
71 pub credential_date: String,
73 pub region: String,
75 pub service: String,
77 pub amz_date: String,
79 pub expires: String,
81 pub signed_headers: String,
83 pub signature: String,
85}
86
87fn is_key_char(c: char) -> bool {
89 c.is_alphanumeric() || c == '-' || c == '.'
90}
91fn is_val_char(c: char) -> bool {
93 c != '&'
94}
95
96fn query_pairs(input: &str) -> IResult<&str, Vec<(&str, &str)>> {
98 let (input, _) = if let Some(i) = input.find('?') {
100 nom::bytes::complete::take::<_, _, nom::error::Error<_>>(i + 1usize)(input)?
102 } else {
103 ("", input)
105 }; separated_list1(
107 nomchar('&'),
108 separated_pair(
109 take_while1(is_key_char),
110 nomchar('='),
111 take_while1(is_val_char),
112 ),
113 )
114 .parse(input)
115}
116
117pub fn parse_presigned_params(input: &str) -> IResult<&str, PresignedParams> {
131 let (rest, pairs) = query_pairs(input)?;
132 let mut m = HashMap::new();
134 for (k, v) in pairs {
135 let val = percent_decode_str(v).decode_utf8_lossy().into_owned();
137 m.insert(k, val);
138 }
139
140 let algorithm = m.remove("X-Amz-Algorithm").ok_or_else(|| miss(rest))?;
142 let credential_raw = m.remove("X-Amz-Credential").ok_or_else(|| miss(rest))?;
143 let amz_date = m.remove("X-Amz-Date").ok_or_else(|| miss(rest))?;
144 let expires = m.remove("X-Amz-Expires").ok_or_else(|| miss(rest))?;
145 let signed_headers = m.remove("X-Amz-SignedHeaders").ok_or_else(|| miss(rest))?;
146 let signature = m.remove("X-Amz-Signature").ok_or_else(|| miss(rest))?;
147
148 let mut parts = credential_raw.split('/');
151 let access_key = parts.next().unwrap_or("").to_string();
152 let credential_date = parts.next().unwrap_or("").to_string();
153 let region = parts.next().unwrap_or("").to_string();
154 let service = parts.next().unwrap_or("").to_string();
155 Ok((
158 rest,
159 PresignedParams {
160 algorithm,
161 access_key,
162 credential_date,
163 region,
164 service,
165 amz_date,
166 expires,
167 signed_headers,
168 signature,
169 },
170 ))
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use nom::Err;
177
178 #[test]
179 fn test_parse_token_from_header() {
180 let input = "AWS4-HMAC-SHA256 Credential=MYLOCAL123/20250417/eu-west-3/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ec323a7db4d0b8bd27eced3b2bb0d59f9b9dd";
181 let result = parse_token_from_header(input);
182 assert_eq!(result, Ok(("", ("MYLOCAL123"))));
183 }
184
185 #[test]
186 fn parse_token_from_header_success_and_error() {
187 let input = "AWS4-HMAC-SHA256 Credential=TOKEN123/20250417/eu-west-1/s3/aws4_request, SignedHeaders=host,Signature=abc";
188 let result = parse_token_from_header(input);
189 assert!(result.is_ok());
190 let (remaining, token) = result.unwrap();
191 assert_eq!(token, "TOKEN123");
192 assert_eq!(remaining, "");
193
194 let bad = "NoCredentialHere";
195 assert!(parse_token_from_header(bad).is_err());
196 }
197
198 #[test]
199 fn parse_credential_scope_missing_prefix_fails() {
200 assert!(parse_credential_scope("no-credential-here").is_err());
201 }
202
203 #[test]
204 fn parse_token_from_header_empty_fails() {
205 assert!(parse_token_from_header("").is_err());
206 }
207
208 #[test]
209 fn parse_token_from_header_no_credential_fails() {
210 assert!(parse_token_from_header("AWS4-HMAC-SHA256 SignedHeaders=host").is_err());
211 }
212
213 #[test]
214 fn test_parse_valid_scope() {
215 let header = "Credential=AKIAEXAMPLE/20250425/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date";
216 let (rem, (region, service)) = parse_credential_scope(header).expect("parse failed");
217 assert_eq!(region, "us-west-2");
218 assert_eq!(service, "s3");
219 assert!(rem.starts_with(", SignedHeaders"));
220 }
221
222 #[test]
223 fn test_parse_invalid_scope() {
224 let header = "Credential=AKIAEXAMPLE/20250425/us-west-2/s3/some_request";
225 assert!(matches!(parse_credential_scope(header), Err(Err::Error(_))));
226 }
227
228 #[test]
229 fn test_parse_with_prefix() {
230 let header = "Authorization: AWS4-HMAC-SHA256 Credential=XYZ/20250425/eu-central-1/dynamodb/aws4_request/extra";
231 let idx = header.find("Credential=").unwrap();
232 let substr = &header[idx..];
233 let (rem, (region, service)) = parse_credential_scope(substr).expect("parse failed");
234 assert_eq!(region, "eu-central-1");
235 assert_eq!(service, "dynamodb");
236 assert!(rem.starts_with("/extra"));
237 }
238
239 #[test]
240 fn parses_all_fields() {
241 let url = "http://localhost:6190/proxy-aws-bucket01/mandelbrot/?\
242 X-Amz-Algorithm=AWS4-HMAC-SHA256&\
243 X-Amz-Credential=MYLOCAL123%2F20250426%2Feu-west-3%2Fs3%2Faws4_request&\
244 X-Amz-Date=20250426T143249Z&\
245 X-Amz-Expires=3600&\
246 X-Amz-SignedHeaders=host&\
247 X-Amz-Signature=53cb3d8a12c8c1078fba3fcd55ced9c93fcdc8e2f98184e9ffea50245f4ebea5";
248
249 let (_, p) = parse_presigned_params(url).unwrap();
250 assert_eq!(p.algorithm, "AWS4-HMAC-SHA256");
251 assert_eq!(p.access_key, "MYLOCAL123");
252 assert_eq!(p.credential_date, "20250426");
253 assert_eq!(p.region, "eu-west-3");
254 assert_eq!(p.service, "s3");
255 assert_eq!(p.amz_date, "20250426T143249Z");
256 assert_eq!(p.expires, "3600");
257 assert_eq!(p.signed_headers, "host");
258 assert_eq!(
259 p.signature,
260 "53cb3d8a12c8c1078fba3fcd55ced9c93fcdc8e2f98184e9ffea50245f4ebea5"
261 );
262 }
263
264 #[test]
265 fn fails_if_missing_signature() {
266 let url = "https://example.com/?X-Amz-Credential=AK/20250426/us-west-2/s3/aws4_request";
267 assert!(parse_presigned_params(url).is_err());
268 }
269
270 #[test]
271 fn parses_presigned_params_from_raw_query() {
272 let q = "X-Amz-Algorithm=AWS4-HMAC-SHA256&\
274 X-Amz-Credential=MYLOCAL123%2F20250426%2Feu-west-3%2Fs3%2Faws4_request&\
275 X-Amz-Date=20250426T143249Z&\
276 X-Amz-Expires=3600&\
277 X-Amz-SignedHeaders=host&\
278 X-Amz-Signature=53cb3d8a12c8c1078fba3fcd55ced9c93fcdc8e2f98184e9ffea50245f4ebea5";
279
280 assert!(
282 parse_presigned_params(q).is_err(),
283 "the parser should reject a query that has no leading '?'"
284 );
285
286 let wrapped = format!("?{q}");
288 let (_, p) = parse_presigned_params(&wrapped)
289 .expect("parser must succeed when a leading '?' is present");
290
291 assert_eq!(p.algorithm, "AWS4-HMAC-SHA256");
292 assert_eq!(p.access_key, "MYLOCAL123");
293 assert_eq!(p.credential_date, "20250426");
294 assert_eq!(p.region, "eu-west-3");
295 assert_eq!(p.service, "s3");
296 assert_eq!(p.amz_date, "20250426T143249Z");
297 assert_eq!(p.expires, "3600");
298 assert_eq!(p.signed_headers, "host");
299 assert_eq!(
300 p.signature,
301 "53cb3d8a12c8c1078fba3fcd55ced9c93fcdc8e2f98184e9ffea50245f4ebea5"
302 );
303 }
304}