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#[derive(Debug, 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 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 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 pub fn project_id(&self) -> Option<ProjectId> {
137 self.project_id
138 }
139
140 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 FromStr for PartialDsn {
158 type Err = ParseDsnError;
159
160 fn from_str(s: &str) -> Result<Self, Self::Err> {
161 Self::from_dsn(Dsn::from_str(s)?)
162 }
163}
164
165impl<'de> Deserialize<'de> for PartialDsn {
166 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167 where
168 D: serde::Deserializer<'de>,
169 {
170 let dsn = Dsn::deserialize(deserializer)?;
171 Self::from_dsn(dsn).map_err(serde::de::Error::custom)
172 }
173}
174
175impl Serialize for PartialDsn {
176 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177 where
178 S: serde::Serializer,
179 {
180 serializer.collect_str(self)
181 }
182}
183
184const fn default_version() -> u16 {
185 relay_event_schema::protocol::PROTOCOL_VERSION
186}
187
188fn is_false(value: &bool) -> bool {
189 !*value
190}
191
192fn make_false() -> bool {
193 false
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198pub struct RequestMeta<D = PartialDsn> {
199 dsn: D,
201
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 client: Option<String>,
205
206 #[serde(default = "default_version")]
208 version: u16,
209
210 #[serde(default, skip_serializing_if = "Option::is_none")]
212 origin: Option<Url>,
213
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 remote_addr: Option<IpAddr>,
217
218 #[serde(default, skip_serializing_if = "String::is_empty")]
220 forwarded_for: String,
221
222 #[serde(default, skip_serializing_if = "Option::is_none")]
224 user_agent: Option<String>,
225
226 #[serde(default, skip_serializing_if = "ClientHints::is_empty")]
227 client_hints: ClientHints<String>,
228
229 #[serde(default = "make_false", skip_serializing_if = "is_false")]
231 no_cache: bool,
232
233 #[serde(skip, default = "Utc::now")]
237 received_at: DateTime<Utc>,
238
239 #[serde(skip)]
245 signature: Option<Signature>,
246
247 #[serde(skip)]
251 request_trust: Option<RequestTrust>,
252}
253
254impl<D> RequestMeta<D> {
255 pub fn client(&self) -> Option<&str> {
259 self.client.as_deref()
260 }
261
262 pub fn client_name(&self) -> ClientName<'_> {
266 self.client()
267 .and_then(|client| client.split_once('/'))
268 .map(|(client, _)| client)
269 .map_or(ClientName::Other("proprietary"), ClientName::from)
270 }
271
272 #[allow(dead_code)] pub fn version(&self) -> u16 {
275 self.version
276 }
277
278 pub fn origin(&self) -> Option<&Url> {
280 self.origin.as_ref()
281 }
282
283 #[allow(unused)]
285 pub fn remote_addr(&self) -> Option<IpAddr> {
286 self.remote_addr
287 }
288
289 pub fn client_addr(&self) -> Option<IpAddr> {
294 let client = self.forwarded_for().split(',').next()?;
295 client.trim().parse().ok()
296 }
297
298 pub fn forwarded_for(&self) -> &str {
300 &self.forwarded_for
301 }
302
303 pub fn user_agent(&self) -> Option<&str> {
308 self.user_agent.as_deref()
309 }
310
311 pub fn client_hints(&self) -> ClientHints<&str> {
312 self.client_hints.as_deref()
313 }
314
315 pub fn no_cache(&self) -> bool {
317 self.no_cache
318 }
319
320 pub fn received_at(&self) -> DateTime<Utc> {
322 self.received_at
323 }
324
325 pub fn set_received_at(&mut self, received_at: DateTime<Utc>) {
327 self.received_at = received_at
328 }
329
330 pub fn request_trust(&self) -> RequestTrust {
333 self.request_trust.unwrap_or(RequestTrust::Untrusted)
334 }
335
336 pub fn set_request_trust(&mut self, value: RequestTrust) {
338 self.request_trust = Some(value);
339 }
340
341 pub fn set_client(&mut self, client: String) {
343 self.client = Some(client);
344 }
345
346 pub fn signature(&self) -> Option<&Signature> {
348 self.signature.as_ref()
349 }
350}
351
352impl RequestMeta {
353 pub fn outbound(dsn: PartialDsn) -> Self {
355 Self {
356 dsn,
357 client: Some(crate::constants::CLIENT.to_owned()),
358 version: default_version(),
359 origin: None,
360 remote_addr: None,
361 forwarded_for: "".to_owned(),
362 user_agent: Some(crate::constants::SERVER.to_owned()),
363 no_cache: false,
364 received_at: Utc::now(),
365 client_hints: ClientHints::default(),
366 signature: None,
367 request_trust: None,
368 }
369 }
370
371 pub fn dsn(&self) -> &PartialDsn {
376 &self.dsn
377 }
378
379 pub fn project_id(&self) -> Option<ProjectId> {
385 self.dsn.project_id
386 }
387
388 pub fn set_project_id(&mut self, project_id: ProjectId) {
390 self.dsn.project_id = Some(project_id);
391 }
392
393 pub fn public_key(&self) -> ProjectKey {
395 self.dsn.public_key
396 }
397
398 pub fn auth_header(&self) -> String {
402 let mut auth = format!(
403 "Sentry sentry_key={}, sentry_version={}",
404 self.public_key(),
405 self.version
406 );
407
408 if let Some(ref client) = self.client {
409 use std::fmt::Write;
410 write!(auth, ", sentry_client={client}").ok();
411 }
412
413 auth
414 }
415
416 pub fn get_partial_scoping(&self) -> PartialScoping {
420 PartialScoping {
421 organization_id: None,
422 project_id: self.project_id(),
423 project_key: self.public_key(),
424 }
425 }
426}
427
428#[derive(Debug, Copy, Clone)]
432pub struct PartialScoping {
433 pub organization_id: Option<OrganizationId>,
435 pub project_id: Option<ProjectId>,
437 pub project_key: ProjectKey,
439}
440
441impl PartialScoping {
442 pub fn into_scoping(self) -> Scoping {
446 Scoping {
447 organization_id: self
448 .organization_id
449 .unwrap_or_else(|| OrganizationId::new(0)),
450 project_id: self.project_id.unwrap_or_else(|| ProjectId::new(0)),
451 project_key: self.project_key,
452 key_id: None,
453 }
454 }
455}
456
457#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
466pub enum RequestTrust {
467 #[default]
469 Untrusted,
470 Trusted,
472}
473
474impl RequestTrust {
475 pub fn is_trusted(self) -> bool {
477 matches!(self, Self::Trusted)
478 }
479
480 pub fn is_untrusted(self) -> bool {
482 matches!(self, Self::Untrusted)
483 }
484}
485
486pub type PartialMeta = RequestMeta<Option<PartialDsn>>;
491
492impl PartialMeta {
493 pub fn dsn(&self) -> Option<&PartialDsn> {
498 self.dsn.as_ref()
499 }
500
501 pub fn copy_to(self, mut complete: RequestMeta) -> RequestMeta {
505 if self.client.is_some() {
508 complete.client = self.client;
509 }
510 if self.version != default_version() {
511 complete.version = self.version;
512 }
513 if self.origin.is_some() {
514 complete.origin = self.origin;
515 }
516 if self.remote_addr.is_some() {
517 complete.remote_addr = self.remote_addr;
518 }
519 if !self.forwarded_for.is_empty() {
520 complete.forwarded_for = self.forwarded_for;
521 }
522 if self.user_agent.is_some() {
523 complete.user_agent = self.user_agent;
524 }
525 if self.request_trust.is_some() {
526 complete.request_trust = self.request_trust;
527 }
528 complete.client_hints.copy_from(self.client_hints);
529
530 if self.no_cache {
531 complete.no_cache = true;
532 }
533
534 complete
535 }
536}
537
538impl FromRequestParts<ServiceState> for PartialMeta {
539 type Rejection = BadEventMeta;
540
541 async fn from_request_parts(
542 parts: &mut Parts,
543 state: &ServiceState,
544 ) -> Result<Self, Self::Rejection> {
545 let mut ua = RawUserAgentInfo::default();
546 for (key, value) in &parts.headers {
547 ua.set_ua_field_from_header(key.as_str(), value.to_str().ok().map(str::to_string));
548 }
549
550 let mut from_internal_relay = false;
551 let relay_id = parts
552 .headers
553 .get("x-sentry-relay-id")
554 .and_then(|h| h.to_str().ok())
555 .and_then(|h| h.parse::<RelayId>().ok());
556
557 if let Some(relay_id) = relay_id {
558 relay_log::configure_scope(|s| s.set_tag("relay_id", relay_id));
559 from_internal_relay = state
560 .config()
561 .static_relays()
562 .get(&relay_id)
563 .is_some_and(|ri| ri.internal);
564 }
565
566 let request_trust = Some(match from_internal_relay {
567 true => RequestTrust::Trusted,
568 false => RequestTrust::Untrusted,
569 });
570
571 let ReceivedAt(received_at) = ReceivedAt::from_request_parts(parts, state).await?;
572
573 let signature = Signature::from_request_parts(parts, state)
574 .await
575 .map_err(BadEventMeta::SignatureError)?;
576
577 Ok(RequestMeta {
578 dsn: None,
579 version: default_version(),
580 client: None,
581 origin: parse_header_url(parts, header::ORIGIN)
582 .or_else(|| parse_header_url(parts, header::REFERER)),
583 remote_addr: ConnectInfo::<SocketAddr>::from_request_parts(parts, state)
584 .await
585 .map(|ConnectInfo(peer)| peer.ip())
586 .ok(),
587 forwarded_for: ForwardedFor::from_request_parts(parts, state)
588 .await?
589 .into_inner(),
590 user_agent: ua.user_agent,
591 no_cache: false,
592 received_at,
593 client_hints: ua.client_hints,
594 signature,
595 request_trust,
596 })
597 }
598}
599
600fn get_auth_header(req: &Parts, header_name: impl AsHeaderName) -> Option<&str> {
601 req.headers
602 .get(header_name)
603 .and_then(|x| x.to_str().ok())
604 .filter(|h| h.len() >= 7 && h[..7].eq_ignore_ascii_case("sentry "))
605}
606
607fn auth_from_parts(req: &Parts, path_key: Option<String>) -> Result<Auth, BadEventMeta> {
608 let mut auth = None;
609
610 if let Some(header) = get_auth_header(req, "x-sentry-auth") {
612 auth = Some(header.parse::<Auth>()?);
613 }
614
615 if let Some(header) = get_auth_header(req, header::AUTHORIZATION) {
617 if auth.is_some() {
618 return Err(BadEventMeta::MultipleAuth);
619 }
620
621 auth = Some(header.parse::<Auth>()?);
622 }
623
624 if let Some(basic_auth) = req
626 .headers
627 .get("authorization")
628 .and_then(|value| value.to_str().ok())
629 .and_then(|x| {
630 if x.len() >= 6 && x[..6].eq_ignore_ascii_case("basic ") {
631 x.get(6..)
632 } else {
633 None
634 }
635 })
636 .and_then(|value| {
637 let decoded = String::from_utf8(BASE64.decode(value.as_bytes()).ok()?).ok()?;
638 let (public_key, _) = decoded.split_once(':')?;
639 Auth::from_pairs([("sentry_key", public_key)]).ok()
640 })
641 {
642 if auth.is_some() {
643 return Err(BadEventMeta::MultipleAuth);
644 }
645 auth = Some(basic_auth);
646 }
647
648 let query = req.uri.query().unwrap_or_default();
650 if query.contains("sentry_") {
651 if auth.is_some() {
652 return Err(BadEventMeta::MultipleAuth);
653 }
654
655 auth = Some(Auth::from_querystring(query.as_bytes())?);
656 }
657
658 if let Some(sentry_key) = path_key {
660 if auth.is_some() {
661 return Err(BadEventMeta::MultipleAuth);
662 }
663
664 auth = Some(
665 Auth::from_pairs(std::iter::once(("sentry_key", sentry_key)))
666 .map_err(|_| BadEventMeta::MissingAuth)?,
667 );
668 }
669
670 auth.ok_or(BadEventMeta::MissingAuth)
671}
672
673fn parse_header_url(req: &Parts, header: impl AsHeaderName) -> Option<Url> {
674 req.headers
675 .get(header)
676 .and_then(|h| h.to_str().ok())
677 .and_then(|s| s.parse::<Url>().ok())
678 .and_then(|u| match u.scheme() {
679 "http" | "https" => Some(u),
680 _ => None,
681 })
682}
683
684#[derive(Debug, serde::Deserialize)]
689struct StorePath {
690 project_id: Option<ProjectId>,
701
702 sentry_key: Option<String>,
707}
708
709impl FromRequestParts<ServiceState> for RequestMeta {
710 type Rejection = BadEventMeta;
711
712 async fn from_request_parts(
713 parts: &mut Parts,
714 state: &ServiceState,
715 ) -> Result<Self, Self::Rejection> {
716 let Path(store_path): Path<StorePath> =
717 parts.extract().await.map_err(BadEventMeta::BadProject)?;
718
719 let auth = auth_from_parts(parts, store_path.sentry_key)?;
720 let partial_meta: PartialMeta = parts.extract_with_state(state).await?;
721 let (public_key, key_flags) = ProjectKey::parse_with_flags(auth.public_key())?;
722
723 let config = state.config();
724 let upstream = config.upstream_descriptor();
725
726 let dsn = PartialDsn {
727 scheme: upstream.scheme(),
728 public_key,
729 host: upstream.host().to_owned(),
730 port: upstream.port(),
731 path: String::new(),
732 project_id: store_path.project_id,
733 };
734
735 let version = auth.version();
737 if version > relay_event_schema::protocol::PROTOCOL_VERSION {
738 return Err(BadEventMeta::UnsupportedProtocolVersion(version));
739 }
740
741 relay_statsd::metric!(
742 counter(RelayCounters::EventProtocol) += 1,
743 version = &version.to_string()
744 );
745
746 Ok(RequestMeta {
747 dsn,
748 version,
749 client: auth.client_agent().map(str::to_owned),
750 origin: partial_meta.origin,
751 remote_addr: partial_meta.remote_addr,
752 forwarded_for: partial_meta.forwarded_for,
753 user_agent: partial_meta.user_agent,
754 no_cache: key_flags.contains(&"no-cache"),
755 received_at: partial_meta.received_at,
756 client_hints: partial_meta.client_hints,
757 signature: partial_meta.signature,
758 request_trust: partial_meta.request_trust,
759 })
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766
767 impl RequestMeta {
768 pub fn new(dsn: relay_common::Dsn) -> Self {
770 Self {
771 dsn: PartialDsn::from_dsn(dsn).expect("invalid DSN"),
772 client: Some("sentry/client".to_owned()),
773 version: 7,
774 origin: Some("http://origin/".parse().unwrap()),
775 remote_addr: Some("192.168.0.1".parse().unwrap()),
776 forwarded_for: String::new(),
777 user_agent: Some("sentry/agent".to_owned()),
778 no_cache: false,
779 received_at: Utc::now(),
780 client_hints: ClientHints::default(),
781 request_trust: None,
782 signature: None,
783 }
784 }
785 }
786
787 #[test]
788 fn test_request_meta_roundtrip() {
789 let json = r#"{
790 "dsn": "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42",
791 "client": "sentry-client",
792 "version": 7,
793 "origin": "http://origin/",
794 "remote_addr": "192.168.0.1",
795 "forwarded_for": "8.8.8.8",
796 "user_agent": "0x8000",
797 "no_cache": false,
798 "client_hints": {
799 "sec_ch_ua_platform": "macOS",
800 "sec_ch_ua_platform_version": "13.1.0",
801 "sec_ch_ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\""
802 }
803 }"#;
804
805 let mut deserialized: RequestMeta = serde_json::from_str(json).unwrap();
806
807 let reqmeta = RequestMeta {
808 dsn: PartialDsn::from_str("https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42")
809 .unwrap(),
810 client: Some("sentry-client".to_owned()),
811 version: 7,
812 origin: Some(Url::parse("http://origin/").unwrap()),
813 remote_addr: Some(IpAddr::from_str("192.168.0.1").unwrap()),
814 forwarded_for: "8.8.8.8".to_owned(),
815 user_agent: Some("0x8000".to_owned()),
816 no_cache: false,
817 received_at: Utc::now(),
818 client_hints: ClientHints {
819 sec_ch_ua_platform: Some("macOS".to_owned()),
820 sec_ch_ua_platform_version: Some("13.1.0".to_owned()),
821 sec_ch_ua: Some(
822 "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\""
823 .to_owned(),
824 ),
825 sec_ch_ua_model: None,
826 },
827 request_trust: None,
828 signature: None,
829 };
830 deserialized.received_at = reqmeta.received_at;
831 assert_eq!(deserialized, reqmeta);
832 }
833
834 #[test]
835 fn test_signature_not_serialized() {
836 let dsn: relay_common::Dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
837 .parse()
838 .unwrap();
839 let without_signature = RequestMeta::new(dsn.clone());
840 let mut with_signature = RequestMeta::new(dsn);
841 with_signature.signature = Some(Signature("test-signature".to_owned()));
842
843 let serialized_without_signature = serde_json::to_string(&without_signature).unwrap();
844 let serialized_with_signature = serde_json::to_string(&with_signature).unwrap();
845 assert_eq!(serialized_with_signature, serialized_without_signature);
846 }
847
848 #[test]
849 fn test_trusted_is_not_untrusted() {
850 let x = RequestTrust::Trusted;
851 assert!(x.is_trusted());
852 assert!(!x.is_untrusted());
853
854 let x = RequestTrust::Untrusted;
855 assert!(!x.is_trusted());
856 assert!(x.is_untrusted());
857 }
858}