rama_http_headers/common/
origin.rs1use std::borrow::Cow;
2use std::convert::TryFrom;
3use std::fmt;
4
5use rama_core::bytes::Bytes;
6use rama_core::error::{BoxError, ErrorContext as _};
7use rama_core::telemetry::tracing;
8use rama_http_types::HeaderValue;
9use rama_net::Protocol;
10use rama_net::address::Authority;
11use rama_net::uri::Uri;
12
13use crate::Error;
14use crate::util::{IterExt, TryFromValues};
15
16#[derive(Clone, Debug, PartialEq, Eq, Hash)]
34pub struct Origin(OriginOrNull);
35
36impl crate::TypedHeader for Origin {
37 fn name() -> &'static ::rama_http_types::header::HeaderName {
38 &::rama_http_types::header::ORIGIN
39 }
40}
41
42impl crate::HeaderDecode for Origin {
43 fn decode<'i, I>(values: &mut I) -> Result<Self, crate::Error>
44 where
45 I: Iterator<Item = &'i ::rama_http_types::header::HeaderValue>,
46 {
47 crate::util::TryFromValues::try_from_values(values).map(Origin)
48 }
49}
50
51impl crate::HeaderEncode for Origin {
52 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
53 match HeaderValue::try_from(&self.0) {
54 Ok(value) => values.extend(::std::iter::once(value)),
55 Err(err) => {
56 tracing::debug!("failed to encode origin value as header: {err}");
57 }
58 }
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Hash)]
63enum OriginOrNull {
64 Origin(Protocol, Authority),
65 Null,
66}
67
68impl Origin {
69 pub const NULL: Self = Self(OriginOrNull::Null);
71
72 #[inline]
74 pub fn is_null(&self) -> bool {
75 matches!(self.0, OriginOrNull::Null)
76 }
77
78 #[inline]
80 pub fn scheme(&self) -> &str {
81 match self.0 {
82 OriginOrNull::Origin(ref scheme, _) => scheme.as_str(),
83 OriginOrNull::Null => "",
84 }
85 }
86
87 #[inline]
89 pub fn hostname(&self) -> Cow<'_, str> {
90 match self.0 {
91 OriginOrNull::Origin(_, ref auth) => auth.address.host.to_str(),
92 OriginOrNull::Null => Cow::Borrowed(""),
93 }
94 }
95
96 #[inline]
98 pub fn port(&self) -> Option<u16> {
99 match self.0 {
100 OriginOrNull::Origin(_, ref auth) => auth.address.port_u16(),
101 OriginOrNull::Null => None,
102 }
103 }
104
105 pub fn try_from_parts(
107 scheme: &str,
108 host: &str,
109 port: impl Into<Option<u16>>,
110 ) -> Result<Self, InvalidOrigin> {
111 struct MaybePort(Option<u16>);
112
113 impl fmt::Display for MaybePort {
114 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115 if let Some(port) = self.0 {
116 write!(f, ":{port}")
117 } else {
118 Ok(())
119 }
120 }
121 }
122
123 let bytes = Bytes::from(format!("{}://{}{}", scheme, host, MaybePort(port.into())));
124 HeaderValue::from_maybe_shared(bytes)
125 .ok()
126 .and_then(|val| Self::try_from_header_value(&val))
127 .ok_or(InvalidOrigin)
128 }
129
130 #[must_use]
135 pub fn try_from_header_value(value: &HeaderValue) -> Option<Self> {
136 OriginOrNull::try_from_value(value).map(Origin)
137 }
138}
139
140impl fmt::Display for Origin {
141 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
142 match self.0 {
143 OriginOrNull::Origin(ref scheme, ref auth) => write!(f, "{scheme}://{auth}"),
144 OriginOrNull::Null => f.write_str("null"),
145 }
146 }
147}
148
149rama_utils::macros::error::static_str_error! {
150 #[doc = "origin is not valid"]
151 pub struct InvalidOrigin;
152}
153
154impl OriginOrNull {
155 fn try_from_value(value: &HeaderValue) -> Option<Self> {
156 if value == "null" {
157 return Some(Self::Null);
158 }
159
160 let uri = Uri::try_from(value.as_bytes()).ok()?;
161
162 let scheme = uri.scheme()?.clone();
166 let auth = uri.authority()?.into_owned();
167
168 let path_ok = uri.path_or_root() == "/";
169 if !path_ok || uri.query().is_some() || uri.fragment().is_some() {
170 return None;
171 }
172
173 Some(Self::Origin(scheme, auth))
174 }
175}
176
177impl TryFromValues for OriginOrNull {
178 fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
179 where
180 I: Iterator<Item = &'i HeaderValue>,
181 {
182 values
183 .just_one()
184 .and_then(Self::try_from_value)
185 .ok_or_else(Error::invalid)
186 }
187}
188
189impl<'a> TryFrom<&'a OriginOrNull> for HeaderValue {
190 type Error = BoxError;
191
192 fn try_from(origin: &'a OriginOrNull) -> Result<Self, Self::Error> {
193 match origin {
194 OriginOrNull::Origin(scheme, auth) => {
195 let s = format!("{scheme}://{auth}");
196 let bytes = Bytes::from(s);
197 Self::from_maybe_shared(bytes)
198 .context("parse Scheme and Authority as a valid header value")
199 }
200 OriginOrNull::Null => Ok(Self::from_static("null")),
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::super::{test_decode, test_encode};
210 use super::*;
211
212 #[test]
213 fn origin() {
214 let s = "http://web-platform.test:8000";
215 let origin = test_decode::<Origin>(&[s]).unwrap();
216 assert_eq!(origin.scheme(), "http");
217 assert_eq!(origin.hostname(), "web-platform.test");
218 assert_eq!(origin.port(), Some(8000));
219
220 let headers = test_encode(origin);
221 assert_eq!(headers["origin"], s);
222 }
223
224 #[test]
225 fn parse_from_header_value() {
226 let value = HeaderValue::from_static("http://localhost:8000");
227 let origin = Origin::try_from_header_value(&value).unwrap();
228
229 assert_eq!(origin.scheme(), "http");
230 assert_eq!(origin.hostname(), "localhost");
231 assert_eq!(origin.port(), Some(8000));
232
233 assert!(Origin::try_from_header_value(&HeaderValue::from_static("localhost")).is_none());
234 }
235
236 #[test]
237 fn null() {
238 assert_eq!(test_decode::<Origin>(&["null"]), Some(Origin::NULL),);
239
240 let headers = test_encode(Origin::NULL);
241 assert_eq!(headers["origin"], "null");
242 }
243}