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#[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 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 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 pub fn project_id(&self) -> Option<ProjectId> {
136 self.project_id
137 }
138
139 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
197pub struct RequestMeta<D = PartialDsn> {
198 dsn: D,
200
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 client: Option<String>,
204
205 #[serde(default = "default_version")]
207 version: u16,
208
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 origin: Option<Url>,
212
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 remote_addr: Option<IpAddr>,
216
217 #[serde(default, skip_serializing_if = "String::is_empty")]
219 forwarded_for: String,
220
221 #[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 #[serde(default = "make_false", skip_serializing_if = "is_false")]
230 no_cache: bool,
231
232 #[serde(skip, default = "Utc::now")]
236 received_at: DateTime<Utc>,
237
238 #[serde(skip)]
244 signature: Option<Signature>,
245
246 #[serde(skip)]
250 request_trust: Option<RequestTrust>,
251}
252
253impl<D> RequestMeta<D> {
254 pub fn client(&self) -> Option<&str> {
258 self.client.as_deref()
259 }
260
261 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 #[allow(dead_code)] pub fn version(&self) -> u16 {
274 self.version
275 }
276
277 pub fn origin(&self) -> Option<&Url> {
279 self.origin.as_ref()
280 }
281
282 #[allow(unused)]
284 pub fn remote_addr(&self) -> Option<IpAddr> {
285 self.remote_addr
286 }
287
288 pub fn client_addr(&self) -> Option<IpAddr> {
293 let client = self.forwarded_for().split(',').next()?;
294 client.trim().parse().ok()
295 }
296
297 pub fn forwarded_for(&self) -> &str {
299 &self.forwarded_for
300 }
301
302 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 pub fn no_cache(&self) -> bool {
316 self.no_cache
317 }
318
319 pub fn received_at(&self) -> DateTime<Utc> {
321 self.received_at
322 }
323
324 pub fn set_received_at(&mut self, received_at: DateTime<Utc>) {
326 self.received_at = received_at
327 }
328
329 pub fn request_trust(&self) -> RequestTrust {
332 self.request_trust.unwrap_or(RequestTrust::Untrusted)
333 }
334
335 pub fn set_request_trust(&mut self, value: RequestTrust) {
337 self.request_trust = Some(value);
338 }
339
340 pub fn set_client(&mut self, client: String) {
342 self.client = Some(client);
343 }
344
345 pub fn signature(&self) -> Option<&Signature> {
347 self.signature.as_ref()
348 }
349}
350
351impl RequestMeta {
352 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 pub fn dsn(&self) -> &PartialDsn {
375 &self.dsn
376 }
377
378 pub fn project_id(&self) -> Option<ProjectId> {
384 self.dsn.project_id
385 }
386
387 pub fn set_project_id(&mut self, project_id: ProjectId) {
389 self.dsn.project_id = Some(project_id);
390 }
391
392 pub fn public_key(&self) -> ProjectKey {
394 self.dsn.public_key
395 }
396
397 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 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#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
438pub enum RequestTrust {
439 #[default]
441 Untrusted,
442 Trusted,
444}
445
446impl RequestTrust {
447 pub fn is_trusted(self) -> bool {
449 matches!(self, Self::Trusted)
450 }
451
452 pub fn is_untrusted(self) -> bool {
454 matches!(self, Self::Untrusted)
455 }
456}
457
458pub type PartialMeta = RequestMeta<Option<PartialDsn>>;
463
464impl PartialMeta {
465 pub fn dsn(&self) -> Option<&PartialDsn> {
470 self.dsn.as_ref()
471 }
472
473 pub fn copy_to(self, mut complete: RequestMeta) -> RequestMeta {
477 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 if let Some(header) = get_auth_header(req, "x-sentry-auth") {
584 auth = Some(header.parse::<Auth>()?);
585 }
586
587 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 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 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 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#[derive(Debug, serde::Deserialize)]
661struct StorePath {
662 project_id: Option<ProjectId>,
673
674 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 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 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}