Skip to main content

clankerdiff_client/
header.rs

1use http::{HeaderName, HeaderValue};
2use std::{fmt, str::FromStr};
3use thiserror::Error;
4
5#[derive(Clone)]
6pub struct ConnectionHeader {
7    name: HeaderName,
8    value: HeaderValue,
9}
10
11impl ConnectionHeader {
12    pub fn name(&self) -> &HeaderName {
13        &self.name
14    }
15
16    pub fn value(&self) -> &HeaderValue {
17        &self.value
18    }
19}
20
21impl fmt::Debug for ConnectionHeader {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        formatter
24            .debug_struct("ConnectionHeader")
25            .field("name", &self.name)
26            .field("value", &"<redacted>")
27            .finish()
28    }
29}
30
31impl FromStr for ConnectionHeader {
32    type Err = ConnectionHeaderError;
33
34    fn from_str(header: &str) -> Result<Self, Self::Err> {
35        let (name, value) = header
36            .split_once(':')
37            .ok_or(ConnectionHeaderError::MissingSeparator)?;
38        let name = name.trim();
39        if name.is_empty() {
40            return Err(ConnectionHeaderError::EmptyName);
41        }
42        let name = HeaderName::from_bytes(name.as_bytes())
43            .map_err(|_| ConnectionHeaderError::InvalidName)?;
44        if is_reserved(&name) {
45            return Err(ConnectionHeaderError::ReservedName(name.to_string()));
46        }
47        let mut value =
48            HeaderValue::from_str(value.trim()).map_err(|_| ConnectionHeaderError::InvalidValue)?;
49        value.set_sensitive(true);
50        Ok(Self { name, value })
51    }
52}
53
54#[derive(Debug, Error)]
55pub enum ConnectionHeaderError {
56    #[error("header must use NAME: VALUE syntax")]
57    MissingSeparator,
58    #[error("header name must not be empty")]
59    EmptyName,
60    #[error("invalid header name")]
61    InvalidName,
62    #[error("invalid value for header")]
63    InvalidValue,
64    #[error("header `{0}` is managed by the WebSocket client")]
65    ReservedName(String),
66}
67
68fn is_reserved(name: &HeaderName) -> bool {
69    matches!(
70        name.as_str(),
71        "host"
72            | "connection"
73            | "upgrade"
74            | "sec-websocket-key"
75            | "sec-websocket-version"
76            | "sec-websocket-protocol"
77            | "sec-websocket-extensions"
78    )
79}