relay_server/extractors/
request_meta.rs

1use std::convert::Infallible;
2use std::fmt;
3use std::net::{IpAddr, SocketAddr};
4use std::str::FromStr;
5
6use axum::RequestPartsExt;
7use axum::extract::rejection::PathRejection;
8use axum::extract::{ConnectInfo, FromRequestParts, OptionalFromRequestParts, Path};
9use axum::http::StatusCode;
10use axum::http::header::{self, AsHeaderName};
11use axum::http::request::Parts;
12use axum::response::{IntoResponse, Response};
13use chrono::{DateTime, Utc};
14use data_encoding::BASE64;
15use relay_auth::{RelayId, Signature};
16use relay_base_schema::organization::OrganizationId;
17use relay_base_schema::project::{ParseProjectKeyError, ProjectId, ProjectKey};
18use relay_common::{Auth, Dsn, ParseAuthError, ParseDsnError, Scheme};
19use relay_config::UpstreamDescriptor;
20use relay_event_normalization::{ClientHints, RawUserAgentInfo};
21use relay_quotas::Scoping;
22use serde::{Deserialize, Serialize};
23use url::Url;
24
25use crate::extractors::{ForwardedFor, ReceivedAt, SignatureError};
26use crate::service::ServiceState;
27use crate::statsd::{ClientName, RelayCounters};
28use crate::utils::ApiErrorResponse;
29
30#[derive(Debug, thiserror::Error)]
31pub enum BadEventMeta {
32    #[error("missing authorization information")]
33    MissingAuth,
34
35    #[error("multiple authorization payloads detected")]
36    MultipleAuth,
37
38    #[error("unsupported protocol version ({0})")]
39    UnsupportedProtocolVersion(u16),
40
41    #[error("bad envelope authentication header")]
42    BadEnvelopeAuth(#[source] serde_json::Error),
43
44    #[error("bad project path parameter")]
45    BadProject(#[from] PathRejection),
46
47    #[error("bad x-sentry-auth header")]
48    BadAuth(#[from] ParseAuthError),
49
50    #[error("bad sentry DSN public key")]
51    BadPublicKey(#[from] ParseProjectKeyError),
52
53    #[error("bad x-sentry-relay-signature header")]
54    SignatureError(SignatureError),
55}
56
57impl From<Infallible> for BadEventMeta {
58    fn from(infallible: Infallible) -> Self {
59        match infallible {}
60    }
61}
62
63impl IntoResponse for BadEventMeta {
64    fn into_response(self) -> Response {
65        let code = match self {
66            Self::MissingAuth
67            | Self::MultipleAuth
68            | Self::BadAuth(_)
69            | Self::BadEnvelopeAuth(_) => StatusCode::UNAUTHORIZED,
70            Self::UnsupportedProtocolVersion(_)
71            | Self::BadProject(_)
72            | Self::BadPublicKey(_)
73            | Self::SignatureError(_) => StatusCode::BAD_REQUEST,
74        };
75
76        (code, ApiErrorResponse::from_error(&self)).into_response()
77    }
78}
79
80/// Wrapper around a Sentry DSN with parsed public key.
81///
82/// The Sentry DSN type carries a plain public key string. However, Relay handles copy `ProjectKey`
83/// types internally. Converting from `String` to `ProjectKey` is fallible and should be caught when
84/// deserializing the request.
85///
86/// This type caches the parsed project key in addition to the DSN. Other than that, it
87/// transparently serializes to and deserializes from a DSN string.
88#[derive(Debug, Clone, Eq, PartialEq)]
89pub struct PartialDsn {
90    pub scheme: Scheme,
91    pub public_key: ProjectKey,
92    pub host: String,
93    pub port: u16,
94    pub path: String,
95    pub project_id: Option<ProjectId>,
96}
97
98impl PartialDsn {
99    /// Ensures a valid public key and project ID in the DSN.
100    fn from_dsn(dsn: Dsn) -> Result<Self, ParseDsnError> {
101        let project_id = dsn
102            .project_id()
103            .value()
104            .parse()
105            .map_err(|_| ParseDsnError::NoProjectId)?;
106
107        let public_key = dsn
108            .public_key()
109            .parse()
110            .map_err(|_| ParseDsnError::NoUsername)?;
111
112        Ok(Self {
113            scheme: dsn.scheme(),
114            public_key,
115            host: dsn.host().to_owned(),
116            port: dsn.port(),
117            path: dsn.path().to_owned(),
118            project_id: Some(project_id),
119        })
120    }
121
122    /// Creates a new [`PartialDsn`] for a Relay outbound request.
123    pub fn outbound(scoping: &Scoping, upstream: &UpstreamDescriptor<'_>) -> Self {
124        Self {
125            scheme: upstream.scheme(),
126            public_key: scoping.project_key,
127            host: upstream.host().to_owned(),
128            port: upstream.port(),
129            path: "".to_owned(),
130            project_id: Some(scoping.project_id),
131        }
132    }
133
134    /// Returns the project identifier that the DSN points to.
135    pub fn project_id(&self) -> Option<ProjectId> {
136        self.project_id
137    }
138
139    /// Returns the public key part of the DSN for authentication.
140    pub fn public_key(&self) -> ProjectKey {
141        self.public_key
142    }
143}
144
145impl fmt::Display for PartialDsn {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}://{}:@{}", self.scheme, self.public_key, self.host)?;
148        if self.port != self.scheme.default_port() {
149            write!(f, ":{}", self.port)?;
150        }
151        let project_id = self.project_id.unwrap_or_else(|| ProjectId::new(0));
152        write!(f, "/{}{}", self.path.trim_start_matches('/'), project_id)
153    }
154}
155
156impl FromStr for PartialDsn {
157    type Err = ParseDsnError;
158
159    fn from_str(s: &str) -> Result<Self, Self::Err> {
160        Self::from_dsn(Dsn::from_str(s)?)
161    }
162}
163
164impl<'de> Deserialize<'de> for PartialDsn {
165    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
166    where
167        D: serde::Deserializer<'de>,
168    {
169        let dsn = Dsn::deserialize(deserializer)?;
170        Self::from_dsn(dsn).map_err(serde::de::Error::custom)
171    }
172}
173
174impl Serialize for PartialDsn {
175    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176    where
177        S: serde::Serializer,
178    {
179        serializer.collect_str(self)
180    }
181}
182
183const fn default_version() -> u16 {
184    relay_event_schema::protocol::PROTOCOL_VERSION
185}
186
187fn is_false(value: &bool) -> bool {
188    !*value
189}
190
191fn make_false() -> bool {
192    false
193}
194
195/// Request information for sentry ingest data, such as events, envelopes or metrics.
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
197pub struct RequestMeta<D = PartialDsn> {
198    /// The DSN describing the target of this envelope.
199    dsn: D,
200
201    /// The client SDK that sent this event.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    client: Option<String>,
204
205    /// The protocol version that the client speaks.
206    #[serde(default = "default_version")]
207    version: u16,
208
209    /// Value of the origin header in the incoming request, if present.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    origin: Option<Url>,
212
213    /// IP address of the submitting remote.
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    remote_addr: Option<IpAddr>,
216
217    /// The full chain of request forward addresses, including the `remote_addr`.
218    #[serde(default, skip_serializing_if = "String::is_empty")]
219    forwarded_for: String,
220
221    /// The user agent that sent this event.
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    user_agent: Option<String>,
224
225    #[serde(default, skip_serializing_if = "ClientHints::is_empty")]
226    client_hints: ClientHints<String>,
227
228    /// A flag that indicates that project options caching should be bypassed.
229    #[serde(default = "make_false", skip_serializing_if = "is_false")]
230    no_cache: bool,
231
232    /// The time at which the request started.
233    ///
234    /// NOTE: This is internal-only and not exposed to Envelope headers.
235    #[serde(skip, default = "Utc::now")]
236    received_at: DateTime<Utc>,
237
238    /// Contains the signature information extracted from the request.
239    ///
240    /// This can be used, for example, to verify a trusted relay during ingestion.
241    ///
242    /// NOTE: This is internal only.
243    #[serde(skip)]
244    signature: Option<Signature>,
245
246    /// Whether the request is coming from an statically configured internal Relay.
247    ///
248    /// NOTE: This is internal-only and not exposed to Envelope headers.
249    #[serde(skip)]
250    request_trust: Option<RequestTrust>,
251}
252
253impl<D> RequestMeta<D> {
254    /// Returns the client that sent this event (Sentry SDK identifier).
255    ///
256    /// The client is formatted as `"sdk/version"`, for example `"raven-node/2.6.3"`.
257    pub fn client(&self) -> Option<&str> {
258        self.client.as_deref()
259    }
260
261    /// Returns the name of the client that sent the event without version.
262    ///
263    /// If the client is not sent in standard format, this method returns `None`.
264    pub fn client_name(&self) -> ClientName<'_> {
265        self.client()
266            .and_then(|client| client.split_once('/'))
267            .map(|(client, _)| client)
268            .map_or(ClientName::Other("proprietary"), ClientName::from)
269    }
270
271    /// Returns the protocol version of the event payload.
272    #[allow(dead_code)] // used in tests and processing mode
273    pub fn version(&self) -> u16 {
274        self.version
275    }
276
277    /// Returns a reference to the origin URL
278    pub fn origin(&self) -> Option<&Url> {
279        self.origin.as_ref()
280    }
281
282    /// The IP address of the Relay or client that ingested the event.
283    #[allow(unused)]
284    pub fn remote_addr(&self) -> Option<IpAddr> {
285        self.remote_addr
286    }
287
288    /// The IP address of the client that this event originates from.
289    ///
290    /// This differs from `remote_addr` if the event was sent through a Relay or any other proxy
291    /// before.
292    pub fn client_addr(&self) -> Option<IpAddr> {
293        let client = self.forwarded_for().split(',').next()?;
294        client.trim().parse().ok()
295    }
296
297    /// Returns the value of the forwarded for header
298    pub fn forwarded_for(&self) -> &str {
299        &self.forwarded_for
300    }
301
302    /// The user agent that sent this event.
303    ///
304    /// This is the value of the `User-Agent` header. In contrast, `auth.client_agent()` identifies
305    /// the SDK that sent the event.
306    pub fn user_agent(&self) -> Option<&str> {
307        self.user_agent.as_deref()
308    }
309
310    pub fn client_hints(&self) -> ClientHints<&str> {
311        self.client_hints.as_deref()
312    }
313
314    /// Indicates that caches should be bypassed.
315    pub fn no_cache(&self) -> bool {
316        self.no_cache
317    }
318
319    /// The time at which the request started.
320    pub fn received_at(&self) -> DateTime<Utc> {
321        self.received_at
322    }
323
324    /// Sets the received at for this [`RequestMeta`] on the current envelope.
325    pub fn set_received_at(&mut self, received_at: DateTime<Utc>) {
326        self.received_at = received_at
327    }
328
329    /// Returns the request trust, indicating whether the request was sent
330    /// from a statically configured, trusted Relay or some external, untrusted, source.
331    pub fn request_trust(&self) -> RequestTrust {
332        self.request_trust.unwrap_or(RequestTrust::Untrusted)
333    }
334
335    /// Overwrites the request trust property.
336    pub fn set_request_trust(&mut self, value: RequestTrust) {
337        self.request_trust = Some(value);
338    }
339
340    /// Sets the client for this [`RequestMeta`] on the current envelope.
341    pub fn set_client(&mut self, client: String) {
342        self.client = Some(client);
343    }
344
345    /// Returns the trusted relay signature.
346    pub fn signature(&self) -> Option<&Signature> {
347        self.signature.as_ref()
348    }
349}
350
351impl RequestMeta {
352    /// Creates meta for an outbound request of this Relay.
353    pub fn outbound(dsn: PartialDsn) -> Self {
354        Self {
355            dsn,
356            client: Some(crate::constants::CLIENT.to_owned()),
357            version: default_version(),
358            origin: None,
359            remote_addr: None,
360            forwarded_for: "".to_owned(),
361            user_agent: Some(crate::constants::SERVER.to_owned()),
362            no_cache: false,
363            received_at: Utc::now(),
364            client_hints: ClientHints::default(),
365            signature: None,
366            request_trust: None,
367        }
368    }
369
370    /// Returns a reference to the DSN.
371    ///
372    /// The DSN declares the project and auth information and upstream address. When RequestMeta is
373    /// constructed from a web request, the DSN is set to point to the upstream host.
374    pub fn dsn(&self) -> &PartialDsn {
375        &self.dsn
376    }
377
378    /// Returns the project identifier that the DSN points to.
379    ///
380    /// Returns `None` if the envelope was sent to the legacy `/api/store/` endpoint. In this case,
381    /// the DSN will be filled in during normalization. In all other cases, this will return
382    /// `Some(ProjectId)`.
383    pub fn project_id(&self) -> Option<ProjectId> {
384        self.dsn.project_id
385    }
386
387    /// Updates the DSN to the given project ID.
388    pub fn set_project_id(&mut self, project_id: ProjectId) {
389        self.dsn.project_id = Some(project_id);
390    }
391
392    /// Returns the public key part of the DSN for authentication.
393    pub fn public_key(&self) -> ProjectKey {
394        self.dsn.public_key
395    }
396
397    /// Formats the Sentry authentication header.
398    ///
399    /// This header must be included in store requests.
400    pub fn auth_header(&self) -> String {
401        let mut auth = format!(
402            "Sentry sentry_key={}, sentry_version={}",
403            self.public_key(),
404            self.version
405        );
406
407        if let Some(ref client) = self.client {
408            use std::fmt::Write;
409            write!(auth, ", sentry_client={client}").ok();
410        }
411
412        auth
413    }
414
415    /// Returns scoping information from the request.
416    ///
417    /// The scoping returned from this function is not complete since it lacks info from the Project
418    /// state. To fetch full scoping information, invoke the `GetScoping` message on `Project`.
419    pub fn get_partial_scoping(&self) -> Scoping {
420        Scoping {
421            organization_id: OrganizationId::new(0),
422            project_id: self.project_id().unwrap_or_else(|| ProjectId::new(0)),
423            project_key: self.public_key(),
424            key_id: None,
425        }
426    }
427}
428
429/// Whether a request can be considered coming from a trusted source.
430///
431/// An internal/trusted source is a Relay which is statically configured as internal
432/// in Relay's configuration. Notably this does not include fully authenticated
433/// customer Relays, as they can still be tampered with.
434///
435/// Data coming from a trusted source may skip processing steps, as we can rely
436/// on the behaviour/implementation of the downstream Relay.
437#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
438pub enum RequestTrust {
439    /// The result came from a potentially untrusted source.
440    #[default]
441    Untrusted,
442    /// The request was sent from an internally trusted Relay.
443    Trusted,
444}
445
446impl RequestTrust {
447    /// Returns `true` if self is [`Self::Trusted`].
448    pub fn is_trusted(self) -> bool {
449        matches!(self, Self::Trusted)
450    }
451
452    /// Returns `true` if self is [`Self::Untrusted`].
453    pub fn is_untrusted(self) -> bool {
454        matches!(self, Self::Untrusted)
455    }
456}
457
458/// Request information without required authentication parts.
459///
460/// This is identical to [`RequestMeta`] with the exception that the DSN, used to authenticate, is
461/// optional.
462pub type PartialMeta = RequestMeta<Option<PartialDsn>>;
463
464impl PartialMeta {
465    /// Returns a reference to the DSN.
466    ///
467    /// The DSN declares the project and auth information and upstream address. When RequestMeta is
468    /// constructed from a web request, the DSN is set to point to the upstream host.
469    pub fn dsn(&self) -> Option<&PartialDsn> {
470        self.dsn.as_ref()
471    }
472
473    /// Completes missing information with complete `RequestMeta`.
474    ///
475    /// All fields that set in this instance will remain.
476    pub fn copy_to(self, mut complete: RequestMeta) -> RequestMeta {
477        // DSN needs to be validated by the caller and will not be copied over.
478
479        if self.client.is_some() {
480            complete.client = self.client;
481        }
482        if self.version != default_version() {
483            complete.version = self.version;
484        }
485        if self.origin.is_some() {
486            complete.origin = self.origin;
487        }
488        if self.remote_addr.is_some() {
489            complete.remote_addr = self.remote_addr;
490        }
491        if !self.forwarded_for.is_empty() {
492            complete.forwarded_for = self.forwarded_for;
493        }
494        if self.user_agent.is_some() {
495            complete.user_agent = self.user_agent;
496        }
497        if self.request_trust.is_some() {
498            complete.request_trust = self.request_trust;
499        }
500        complete.client_hints.copy_from(self.client_hints);
501
502        if self.no_cache {
503            complete.no_cache = true;
504        }
505
506        complete
507    }
508}
509
510impl FromRequestParts<ServiceState> for PartialMeta {
511    type Rejection = BadEventMeta;
512
513    async fn from_request_parts(
514        parts: &mut Parts,
515        state: &ServiceState,
516    ) -> Result<Self, Self::Rejection> {
517        let mut ua = RawUserAgentInfo::default();
518        for (key, value) in &parts.headers {
519            ua.set_ua_field_from_header(key.as_str(), value.to_str().ok().map(str::to_string));
520        }
521
522        let mut from_internal_relay = false;
523        let relay_id = parts
524            .headers
525            .get("x-sentry-relay-id")
526            .and_then(|h| h.to_str().ok())
527            .and_then(|h| h.parse::<RelayId>().ok());
528
529        if let Some(relay_id) = relay_id {
530            relay_log::configure_scope(|s| s.set_tag("relay_id", relay_id));
531            from_internal_relay = state
532                .config()
533                .static_relays()
534                .get(&relay_id)
535                .is_some_and(|ri| ri.internal);
536        }
537
538        let request_trust = Some(match from_internal_relay {
539            true => RequestTrust::Trusted,
540            false => RequestTrust::Untrusted,
541        });
542
543        let ReceivedAt(received_at) = ReceivedAt::from_request_parts(parts, state).await?;
544
545        let signature = Signature::from_request_parts(parts, state)
546            .await
547            .map_err(BadEventMeta::SignatureError)?;
548
549        Ok(RequestMeta {
550            dsn: None,
551            version: default_version(),
552            client: None,
553            origin: parse_header_url(parts, header::ORIGIN)
554                .or_else(|| parse_header_url(parts, header::REFERER)),
555            remote_addr: ConnectInfo::<SocketAddr>::from_request_parts(parts, state)
556                .await
557                .map(|ConnectInfo(peer)| peer.ip())
558                .ok(),
559            forwarded_for: ForwardedFor::from_request_parts(parts, state)
560                .await?
561                .into_inner(),
562            user_agent: ua.user_agent,
563            no_cache: false,
564            received_at,
565            client_hints: ua.client_hints,
566            signature,
567            request_trust,
568        })
569    }
570}
571
572fn get_auth_header(req: &Parts, header_name: impl AsHeaderName) -> Option<&str> {
573    req.headers
574        .get(header_name)
575        .and_then(|x| x.to_str().ok())
576        .filter(|h| h.len() >= 7 && h[..7].eq_ignore_ascii_case("sentry "))
577}
578
579fn auth_from_parts(req: &Parts, path_key: Option<String>) -> Result<Auth, BadEventMeta> {
580    let mut auth = None;
581
582    // try to extract authentication info from http header "x-sentry-auth"
583    if let Some(header) = get_auth_header(req, "x-sentry-auth") {
584        auth = Some(header.parse::<Auth>()?);
585    }
586
587    // try to extract authentication info from http header "authorization"
588    if let Some(header) = get_auth_header(req, header::AUTHORIZATION) {
589        if auth.is_some() {
590            return Err(BadEventMeta::MultipleAuth);
591        }
592
593        auth = Some(header.parse::<Auth>()?);
594    }
595
596    // try to get authentication info from basic auth
597    if let Some(basic_auth) = req
598        .headers
599        .get("authorization")
600        .and_then(|value| value.to_str().ok())
601        .and_then(|x| {
602            if x.len() >= 6 && x[..6].eq_ignore_ascii_case("basic ") {
603                x.get(6..)
604            } else {
605                None
606            }
607        })
608        .and_then(|value| {
609            let decoded = String::from_utf8(BASE64.decode(value.as_bytes()).ok()?).ok()?;
610            let (public_key, _) = decoded.split_once(':')?;
611            Auth::from_pairs([("sentry_key", public_key)]).ok()
612        })
613    {
614        if auth.is_some() {
615            return Err(BadEventMeta::MultipleAuth);
616        }
617        auth = Some(basic_auth);
618    }
619
620    // try to extract authentication info from URL query_param .../?sentry_...=<key>...
621    let query = req.uri.query().unwrap_or_default();
622    if query.contains("sentry_") {
623        if auth.is_some() {
624            return Err(BadEventMeta::MultipleAuth);
625        }
626
627        auth = Some(Auth::from_querystring(query.as_bytes())?);
628    }
629
630    // try to extract authentication info from URL path segment .../{sentry_key}/...
631    if let Some(sentry_key) = path_key {
632        if auth.is_some() {
633            return Err(BadEventMeta::MultipleAuth);
634        }
635
636        auth = Some(
637            Auth::from_pairs(std::iter::once(("sentry_key", sentry_key)))
638                .map_err(|_| BadEventMeta::MissingAuth)?,
639        );
640    }
641
642    auth.ok_or(BadEventMeta::MissingAuth)
643}
644
645fn parse_header_url(req: &Parts, header: impl AsHeaderName) -> Option<Url> {
646    req.headers
647        .get(header)
648        .and_then(|h| h.to_str().ok())
649        .and_then(|s| s.parse::<Url>().ok())
650        .and_then(|u| match u.scheme() {
651            "http" | "https" => Some(u),
652            _ => None,
653        })
654}
655
656/// Path parameters containing authentication information for store endpoints.
657///
658/// These parameters implement part of the authentication mechanism. For more information, see
659/// [`RequestMeta`].
660#[derive(Debug, serde::Deserialize)]
661struct StorePath {
662    /// The numeric identifier of the Sentry project.
663    ///
664    /// This parameter is part of the store endpoint paths, which are generally located under
665    /// `/api/:project_id/*`. By default, all store endpoints have the project ID in the path. To
666    /// resolve the project and associated information, Relay actually uses the DSN's
667    /// [`ProjectKey`]. During ingestion, the stated project ID from the URI path is validated
668    /// against information resolved from the upstream.
669    ///
670    /// The legacy endpoint (`/api/store/`) does not have the project ID. In this case, Relay skips
671    /// ID validation during ingestion.
672    project_id: Option<ProjectId>,
673
674    /// The DSN's public key, also referred to as project key.
675    ///
676    /// Some endpoints require this key in the path. On all other endpoints, the key is either sent
677    /// as header or query parameter.
678    sentry_key: Option<String>,
679}
680
681impl FromRequestParts<ServiceState> for RequestMeta {
682    type Rejection = BadEventMeta;
683
684    async fn from_request_parts(
685        parts: &mut Parts,
686        state: &ServiceState,
687    ) -> Result<Self, Self::Rejection> {
688        let Path(store_path): Path<StorePath> =
689            parts.extract().await.map_err(BadEventMeta::BadProject)?;
690
691        let auth = auth_from_parts(parts, store_path.sentry_key)?;
692        let partial_meta: PartialMeta = parts.extract_with_state(state).await?;
693        let (public_key, key_flags) = ProjectKey::parse_with_flags(auth.public_key())?;
694
695        let config = state.config();
696        let upstream = config.upstream_descriptor();
697
698        let dsn = PartialDsn {
699            scheme: upstream.scheme(),
700            public_key,
701            host: upstream.host().to_owned(),
702            port: upstream.port(),
703            path: String::new(),
704            project_id: store_path.project_id,
705        };
706
707        // For now, we only handle <= v8 and drop everything else
708        let version = auth.version();
709        if version > relay_event_schema::protocol::PROTOCOL_VERSION {
710            return Err(BadEventMeta::UnsupportedProtocolVersion(version));
711        }
712
713        relay_statsd::metric!(
714            counter(RelayCounters::EventProtocol) += 1,
715            version = &version.to_string()
716        );
717
718        Ok(RequestMeta {
719            dsn,
720            version,
721            client: auth.client_agent().map(str::to_owned),
722            origin: partial_meta.origin,
723            remote_addr: partial_meta.remote_addr,
724            forwarded_for: partial_meta.forwarded_for,
725            user_agent: partial_meta.user_agent,
726            no_cache: key_flags.contains(&"no-cache"),
727            received_at: partial_meta.received_at,
728            client_hints: partial_meta.client_hints,
729            signature: partial_meta.signature,
730            request_trust: partial_meta.request_trust,
731        })
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    impl RequestMeta {
740        // TODO: Remove Dsn here?
741        pub fn new(dsn: relay_common::Dsn) -> Self {
742            Self {
743                dsn: PartialDsn::from_dsn(dsn).expect("invalid DSN"),
744                client: Some("sentry/client".to_owned()),
745                version: 7,
746                origin: Some("http://origin/".parse().unwrap()),
747                remote_addr: Some("192.168.0.1".parse().unwrap()),
748                forwarded_for: String::new(),
749                user_agent: Some("sentry/agent".to_owned()),
750                no_cache: false,
751                received_at: Utc::now(),
752                client_hints: ClientHints::default(),
753                request_trust: None,
754                signature: None,
755            }
756        }
757    }
758
759    #[test]
760    fn test_request_meta_roundtrip() {
761        let json = r#"{
762            "dsn": "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42",
763            "client": "sentry-client",
764            "version": 7,
765            "origin": "http://origin/",
766            "remote_addr": "192.168.0.1",
767            "forwarded_for": "8.8.8.8",
768            "user_agent": "0x8000",
769            "no_cache": false,
770            "client_hints":  {
771            "sec_ch_ua_platform": "macOS",
772            "sec_ch_ua_platform_version": "13.1.0",
773            "sec_ch_ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\""
774            }
775        }"#;
776
777        let mut deserialized: RequestMeta = serde_json::from_str(json).unwrap();
778
779        let reqmeta = RequestMeta {
780            dsn: PartialDsn::from_str("https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42")
781                .unwrap(),
782            client: Some("sentry-client".to_owned()),
783            version: 7,
784            origin: Some(Url::parse("http://origin/").unwrap()),
785            remote_addr: Some(IpAddr::from_str("192.168.0.1").unwrap()),
786            forwarded_for: "8.8.8.8".to_owned(),
787            user_agent: Some("0x8000".to_owned()),
788            no_cache: false,
789            received_at: Utc::now(),
790            client_hints: ClientHints {
791                sec_ch_ua_platform: Some("macOS".to_owned()),
792                sec_ch_ua_platform_version: Some("13.1.0".to_owned()),
793                sec_ch_ua: Some(
794                    "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\""
795                        .to_owned(),
796                ),
797                sec_ch_ua_model: None,
798            },
799            request_trust: None,
800            signature: None,
801        };
802        deserialized.received_at = reqmeta.received_at;
803        assert_eq!(deserialized, reqmeta);
804    }
805
806    #[test]
807    fn test_signature_not_serialized() {
808        let dsn: relay_common::Dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
809            .parse()
810            .unwrap();
811        let without_signature = RequestMeta::new(dsn.clone());
812        let mut with_signature = RequestMeta::new(dsn);
813        with_signature.signature = Some(Signature("test-signature".to_owned()));
814
815        let serialized_without_signature = serde_json::to_string(&without_signature).unwrap();
816        let serialized_with_signature = serde_json::to_string(&with_signature).unwrap();
817        assert_eq!(serialized_with_signature, serialized_without_signature);
818    }
819
820    #[test]
821    fn test_trusted_is_not_untrusted() {
822        let x = RequestTrust::Trusted;
823        assert!(x.is_trusted());
824        assert!(!x.is_untrusted());
825
826        let x = RequestTrust::Untrusted;
827        assert!(!x.is_trusted());
828        assert!(x.is_untrusted());
829    }
830}