Skip to main content

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