Skip to main content

httpsig/message_component/
component.rs

1use super::{
2  component_id::HttpMessageComponentId,
3  component_name::{DerivedComponentName, HttpMessageComponentName},
4  component_param::{handle_params_key_into, handle_params_sf, HttpMessageComponentParam},
5  component_value::HttpMessageComponentValue,
6};
7use crate::{
8  error::{HttpSigError, HttpSigResult},
9  trace::*,
10};
11
12/* ---------------------------------------------------------------- */
13#[derive(Debug, Clone)]
14/// Http message component
15pub struct HttpMessageComponent {
16  /// Http message component id
17  pub id: HttpMessageComponentId,
18  /// Http message component value
19  pub value: HttpMessageComponentValue,
20}
21
22impl TryFrom<&str> for HttpMessageComponent {
23  type Error = HttpSigError;
24  /// Create HttpMessageComponent from serialized string, i.e., `"<id>": <value>` of lines in the signature base of HTTP header.
25  /// We suppose that the value was correctly serialized as a line of signature base.
26  fn try_from(val: &str) -> Result<Self, Self::Error> {
27    let Some((id, value)) = val.split_once(':') else {
28      return Err(HttpSigError::InvalidComponent(format!(
29        "Invalid http message component: {val}"
30      )));
31    };
32    let id = id.trim();
33
34    // check if id is wrapped by double quotations
35    if !(id.starts_with('"') && (id.ends_with('"') || id[1..].contains("\";"))) {
36      return Err(HttpSigError::InvalidComponentId(format!(
37        "Invalid http message component id: {id}"
38      )));
39    }
40
41    Ok(Self {
42      id: HttpMessageComponentId::try_from(id)?,
43      value: HttpMessageComponentValue::from(value.trim()),
44    })
45  }
46}
47
48impl TryFrom<(&HttpMessageComponentId, &[String])> for HttpMessageComponent {
49  type Error = HttpSigError;
50
51  /// Build http message component from given id and its associated field values
52  fn try_from((id, field_values): (&HttpMessageComponentId, &[String])) -> Result<Self, Self::Error> {
53    match &id.name {
54      HttpMessageComponentName::HttpField(_) => build_http_field_component(id, field_values),
55      HttpMessageComponentName::Derived(_) => build_derived_component(id, field_values),
56    }
57  }
58}
59
60impl std::fmt::Display for HttpMessageComponent {
61  /// This always can append single trailing space (SP) for empty value
62  /// https://datatracker.ietf.org/doc/html/rfc9421#section-2.1
63  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64    write!(f, "{}: {}", self.id, self.value)
65  }
66}
67
68/* ---------------------------------------------------------------- */
69/// Build derived component from given id and its associated field values
70pub(super) fn build_derived_component(
71  id: &HttpMessageComponentId,
72  field_values: &[String],
73) -> HttpSigResult<HttpMessageComponent> {
74  let HttpMessageComponentName::Derived(derived_id) = &id.name else {
75    return Err(HttpSigError::InvalidComponent(
76      "invalid http message component name as derived component".to_string(),
77    ));
78  };
79  if field_values.is_empty() {
80    return Err(HttpSigError::InvalidComponent(
81      "derived component requires field values".to_string(),
82    ));
83  }
84  // ensure only `req` and `name` are allowed for derived component parameters
85  if !id
86    .params
87    .0
88    .iter()
89    .all(|p| matches!(p, HttpMessageComponentParam::Req | HttpMessageComponentParam::Name(_)))
90  {
91    return Err(HttpSigError::InvalidComponent(
92      "invalid parameter for derived component".to_string(),
93    ));
94  }
95
96  let value = match derived_id {
97    DerivedComponentName::Method => HttpMessageComponentValue::from(field_values[0].to_ascii_uppercase().as_ref()),
98    DerivedComponentName::TargetUri => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()),
99    DerivedComponentName::Authority => HttpMessageComponentValue::from(field_values[0].to_ascii_lowercase().as_ref()),
100    DerivedComponentName::Scheme => HttpMessageComponentValue::from(field_values[0].to_ascii_lowercase().as_ref()),
101    DerivedComponentName::RequestTarget => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()),
102    DerivedComponentName::Path => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()),
103    DerivedComponentName::Query => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()),
104    DerivedComponentName::Status => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()),
105    DerivedComponentName::QueryParam => {
106      let name = id.params.0.iter().find_map(|p| match p {
107        HttpMessageComponentParam::Name(name) => Some(name),
108        _ => None,
109      });
110      if name.is_none() {
111        return Err(HttpSigError::InvalidComponent(
112          "query-param derived component requires name parameter".to_string(),
113        ));
114      };
115      let name = name.unwrap();
116      let kvs = field_values
117        .iter()
118        .filter(|v| v.contains('='))
119        .map(|v| v.split_once('=').unwrap())
120        .filter(|(k, _)| *k == name.as_str())
121        .map(|(_, v)| v)
122        .collect::<Vec<_>>();
123      HttpMessageComponentValue::from(kvs.join(", ").as_ref())
124    }
125    DerivedComponentName::SignatureParams => {
126      let value = field_values[0].to_string();
127      let opt_pair = value.trim().split_once('=');
128      if opt_pair.is_none() {
129        return Err(HttpSigError::InvalidComponent(
130          "invalid signature-params derived component".to_string(),
131        ));
132      }
133      let (key, value) = opt_pair.unwrap();
134      HttpMessageComponentValue::from((key, value))
135    }
136  };
137  let component = HttpMessageComponent { id: id.clone(), value };
138  Ok(component)
139}
140
141/* ---------------------------------------------------------------- */
142/// Build http field component from given id and its associated field values
143/// NOTE: field_value must be ones of request for `req` param
144pub(super) fn build_http_field_component(
145  id: &HttpMessageComponentId,
146  field_values: &[String],
147) -> HttpSigResult<HttpMessageComponent> {
148  let mut field_values = field_values.to_vec();
149  let params = &id.params;
150
151  for p in params.0.iter() {
152    match p {
153      HttpMessageComponentParam::Sf => {
154        handle_params_sf(&mut field_values)?;
155      }
156      HttpMessageComponentParam::Key(key) => {
157        field_values = handle_params_key_into(&field_values, key)?;
158      }
159      HttpMessageComponentParam::Bs => {
160        return Err(HttpSigError::NotYetImplemented("`bs` is not supported yet".to_string()));
161      }
162      HttpMessageComponentParam::Req => {
163        debug!("`req` is given for http field component");
164      }
165      HttpMessageComponentParam::Tr => return Err(HttpSigError::NotYetImplemented("`tr` is not supported yet".to_string())),
166      HttpMessageComponentParam::Name(_) => {
167        return Err(HttpSigError::NotYetImplemented(
168          "`name` is only for derived component query-params".to_string(),
169        ));
170      }
171    }
172  }
173
174  // TODO: case: some values contains ','
175
176  let field_values_str = field_values.join(", ");
177
178  let component = HttpMessageComponent {
179    id: id.clone(),
180    value: HttpMessageComponentValue::from(field_values_str.as_ref()),
181  };
182  Ok(component)
183}
184
185/* ---------------------------------------------------------------- */
186#[cfg(test)]
187mod tests {
188  use super::*;
189  type IndexSet<K> = indexmap::IndexSet<K, rustc_hash::FxBuildHasher>;
190
191  #[test]
192  fn test_from_serialized_string_derived() {
193    let tuples = vec![
194      ("\"@method\"", "POST", DerivedComponentName::Method),
195      ("\"@target-uri\"", "https://example.com/", DerivedComponentName::TargetUri),
196      ("\"@authority\"", "example.com", DerivedComponentName::Authority),
197      ("\"@scheme\"", "https", DerivedComponentName::Scheme),
198      ("\"@request-target\"", "/path?query", DerivedComponentName::RequestTarget),
199      ("\"@path\"", "/path", DerivedComponentName::Path),
200      ("\"@query\"", "query", DerivedComponentName::Query),
201      ("\"@query-param\";name=\"key\"", "\"value\"", DerivedComponentName::QueryParam),
202      ("\"@status\"", "200", DerivedComponentName::Status),
203    ];
204    for (id, value, name) in tuples {
205      let comp = HttpMessageComponent::try_from(format!("{}: {}", id, value).as_ref()).unwrap();
206      assert_eq!(comp.id.name, HttpMessageComponentName::Derived(name));
207      if !id.contains(';') {
208        assert_eq!(comp.id.params.0, IndexSet::default());
209      } else {
210        assert!(!comp.id.params.0.is_empty());
211      }
212      assert_eq!(comp.value.as_field_value(), value);
213      assert_eq!(comp.value.key(), None);
214      assert_eq!(comp.to_string(), format!("{}: {}", id, value));
215    }
216  }
217
218  #[test]
219  fn test_from_serialized_string_derived_query_params() {
220    let (id, value, name) = ("\"@query-param\";name=\"key\"", "\"value\"", DerivedComponentName::QueryParam);
221    let comp = HttpMessageComponent::try_from(format!("{}: {}", id, value).as_ref()).unwrap();
222    assert_eq!(comp.id.name, HttpMessageComponentName::Derived(name));
223    assert_eq!(
224      comp.id.params.0.get(&HttpMessageComponentParam::Name("key".to_string())),
225      Some(&HttpMessageComponentParam::Name("key".to_string()))
226    );
227    assert_eq!(comp.value.as_field_value(), value);
228    assert_eq!(comp.value.key(), None);
229    assert_eq!(comp.to_string(), format!("{}: {}", id, value));
230  }
231
232  #[test]
233  fn test_from_serialized_string_http_field() {
234    let tuples = vec![
235      ("\"example-header\"", "example-value", "example-header"),
236      ("\"example-header\";bs;tr", "example-value", "example-header"),
237      ("\"example-header\";bs", "example-value", "example-header"),
238      ("\"x-empty-header\"", "", "x-empty-header"),
239    ];
240    for (id, value, inner_name) in tuples {
241      let comp = HttpMessageComponent::try_from(format!("{}: {}", id, value).as_ref()).unwrap();
242      assert_eq!(comp.id.name, HttpMessageComponentName::HttpField(inner_name.to_string()));
243      if !id.contains(';') {
244        assert_eq!(comp.id.params.0, IndexSet::default());
245      } else {
246        assert!(!comp.id.params.0.is_empty());
247      }
248      assert_eq!(comp.value.as_field_value(), value);
249      assert_eq!(comp.to_string(), format!("{}: {}", id, value));
250    }
251  }
252
253  #[test]
254  fn test_from_serialized_string_http_field_params() {
255    let comp = HttpMessageComponent::try_from("\"example-header\";bs;tr: example-value").unwrap();
256    assert_eq!(
257      comp.id.name,
258      HttpMessageComponentName::HttpField("example-header".to_string())
259    );
260    assert_eq!(
261      comp.id.params.0,
262      vec![HttpMessageComponentParam::Bs, HttpMessageComponentParam::Tr]
263        .into_iter()
264        .collect::<IndexSet<_>>()
265    );
266  }
267
268  #[test]
269  fn test_from_serialized_string_http_field_params_key() {
270    let comp = HttpMessageComponent::try_from("\"example-header\";key=\"hoge\": example-value").unwrap();
271    assert_eq!(
272      comp.id.name,
273      HttpMessageComponentName::HttpField("example-header".to_string())
274    );
275    assert_eq!(
276      comp.id.params.0,
277      vec![HttpMessageComponentParam::Key("hoge".to_string())]
278        .into_iter()
279        .collect::<IndexSet<_>>()
280    );
281  }
282
283  #[test]
284  fn test_field_params_derived_component() {
285    // params check
286    // only req and query-param field params are allowed
287    let comp = HttpMessageComponent::try_from("\"@method\";req: POST");
288    assert!(comp.is_ok());
289    let comp = HttpMessageComponent::try_from("\"@query-param\";name=\"id\": POST");
290    assert!(comp.is_ok());
291
292    let comp = HttpMessageComponent::try_from("\"@method\";bs: POST");
293    assert!(comp.is_err());
294    let comp = HttpMessageComponent::try_from("\"@method\";key=\"hoge\": POST");
295    assert!(comp.is_err());
296  }
297
298  #[test]
299  fn test_build_http_field_component() {
300    let id = HttpMessageComponentId::try_from("content-type").unwrap();
301    let field_values = vec!["application/json".to_owned()];
302    let component = build_http_field_component(&id, &field_values).unwrap();
303    assert_eq!(component.id, id);
304    assert_eq!(component.value, HttpMessageComponentValue::from("application/json"));
305    assert_eq!(component.to_string(), "\"content-type\": application/json");
306  }
307  #[test]
308  fn test_build_http_field_component_multiple_values() {
309    let id = HttpMessageComponentId::try_from("\"content-type\"").unwrap();
310    let field_values = vec!["application/json".to_owned(), "application/json-patch+json".to_owned()];
311    let component = build_http_field_component(&id, &field_values).unwrap();
312    assert_eq!(component.id, id);
313    assert_eq!(
314      component.value,
315      HttpMessageComponentValue::from("application/json, application/json-patch+json")
316    );
317    assert_eq!(
318      component.to_string(),
319      "\"content-type\": application/json, application/json-patch+json"
320    );
321  }
322  #[test]
323  fn test_build_http_field_component_sf() {
324    let id = HttpMessageComponentId::try_from("\"content-type\";sf").unwrap();
325    let field_values = vec![
326      "application/json; patched=true".to_owned(),
327      "application/json-patch+json;patched".to_owned(),
328    ];
329    let component = build_http_field_component(&id, &field_values).unwrap();
330    assert_eq!(component.id, id);
331    assert_eq!(
332      component.value,
333      HttpMessageComponentValue::from("application/json;patched=true, application/json-patch+json;patched")
334    );
335    assert_eq!(
336      component.to_string(),
337      "\"content-type\";sf: application/json;patched=true, application/json-patch+json;patched"
338    );
339  }
340  #[test]
341  fn test_build_http_field_component_key() {
342    let id = HttpMessageComponentId::try_from("\"example-header\";key=\"patched\"").unwrap();
343    let field_values = vec!["patched=12345678".to_owned()];
344    let component = build_http_field_component(&id, &field_values).unwrap();
345    assert_eq!(component.id, id);
346    assert_eq!(component.value, HttpMessageComponentValue::from("12345678"));
347    assert_eq!(component.to_string(), "\"example-header\";key=\"patched\": 12345678");
348  }
349  #[test]
350  fn test_build_http_field_component_key_multiple_values() {
351    let id = HttpMessageComponentId::try_from("\"example-header\";key=\"patched\"").unwrap();
352    let field_values = vec![
353      "patched=12345678".to_owned(),
354      "patched=87654321".to_owned(),
355      "not-patched=12345678".to_owned(),
356    ];
357    let component = build_http_field_component(&id, &field_values).unwrap();
358    assert_eq!(component.id, id);
359    assert_eq!(component.value, HttpMessageComponentValue::from("12345678, 87654321"));
360    assert_eq!(
361      component.to_string(),
362      "\"example-header\";key=\"patched\": 12345678, 87654321"
363    );
364  }
365
366  #[test]
367  fn test_build_derived_component() {
368    let id = HttpMessageComponentId::try_from("@method").unwrap();
369    let field_values = vec!["GET".to_owned()];
370    let component = build_derived_component(&id, &field_values).unwrap();
371    assert_eq!(component.id, id);
372    assert_eq!(component.value, HttpMessageComponentValue::from("GET"));
373    assert_eq!(component.to_string(), "\"@method\": GET");
374
375    let id = HttpMessageComponentId::try_from("@target-uri").unwrap();
376    let field_values = vec!["https://example.com/foo".to_owned()];
377    let component = build_derived_component(&id, &field_values).unwrap();
378    assert_eq!(component.id, id);
379    assert_eq!(component.value, HttpMessageComponentValue::from("https://example.com/foo"));
380    assert_eq!(component.to_string(), "\"@target-uri\": https://example.com/foo");
381  }
382  #[test]
383  fn test_build_http_field_component_query_param() {
384    let id = HttpMessageComponentId::try_from("\"@query-param\";name=\"var\"").unwrap();
385    let query_param = "var=this%20is%20a%20big%0Amultiline%20value&bar=with+plus+whitespace&fa%C3%A7ade%22%3A%20=something&ok";
386    let field_values = query_param.split('&').map(|v| v.to_owned()).collect::<Vec<_>>();
387    let component = build_derived_component(&id, &field_values).unwrap();
388    assert_eq!(component.id, id);
389    assert_eq!(
390      component.value,
391      HttpMessageComponentValue::from("this%20is%20a%20big%0Amultiline%20value")
392    );
393    assert_eq!(
394      component.to_string(),
395      "\"@query-param\";name=\"var\": this%20is%20a%20big%0Amultiline%20value"
396    );
397  }
398
399  #[test]
400  fn test_disallow_invalid_params() {
401    let id = HttpMessageComponentId::try_from("\"@method\";key=\"patched\"");
402    assert!(id.is_err());
403  }
404}