Skip to main content

reth_rpc_builder/
cors.rs

1use http::{header::ORIGIN, HeaderValue, Method, Request, Response, StatusCode};
2use jsonrpsee::server::{ws, HttpBody};
3use tower_http::{
4    cors::{AllowOrigin, Any, CorsLayer},
5    validate_request::ValidateRequest,
6};
7
8/// Error thrown when parsing cors domains went wrong
9#[derive(Debug, thiserror::Error)]
10pub enum CorsDomainError {
11    /// Represents an invalid header value for a domain
12    #[error("{domain} is an invalid header value")]
13    InvalidHeader {
14        /// The domain that caused the invalid header
15        domain: String,
16    },
17
18    /// Indicates that a wildcard origin was used incorrectly in a list
19    #[error("wildcard origin (`*`) cannot be passed as part of a list: {input}")]
20    WildCardNotAllowed {
21        /// The input string containing the incorrectly used wildcard
22        input: String,
23    },
24}
25
26/// Creates a [`CorsLayer`] from the given domains
27pub(crate) fn create_cors_layer(http_cors_domains: &str) -> Result<CorsLayer, CorsDomainError> {
28    let cors = match parse_origins(http_cors_domains)? {
29        None => CorsLayer::new()
30            .allow_methods([Method::GET, Method::POST])
31            .allow_origin(Any)
32            .allow_headers(Any),
33        Some(origins) => {
34            let origin = AllowOrigin::list(origins);
35            CorsLayer::new()
36                .allow_methods([Method::GET, Method::POST])
37                .allow_origin(origin)
38                .allow_headers(Any)
39        }
40    };
41    Ok(cors)
42}
43
44/// Rejects `WebSocket` upgrades from origins outside the configured allowlist.
45#[derive(Clone, Debug)]
46pub(crate) struct WsOriginValidator {
47    origins: Option<Vec<HeaderValue>>,
48}
49
50impl WsOriginValidator {
51    pub(crate) fn new(domains: Option<&str>) -> Result<Self, CorsDomainError> {
52        Ok(Self { origins: domains.map(parse_origins).transpose()?.flatten() })
53    }
54}
55
56impl<B> ValidateRequest<B> for WsOriginValidator {
57    type ResponseBody = HttpBody;
58
59    fn validate(&mut self, request: &mut Request<B>) -> Result<(), Response<HttpBody>> {
60        if let Some(allowed) = &self.origins &&
61            ws::is_upgrade_request(request)
62        {
63            let mut origins = request.headers().get_all(ORIGIN).iter();
64            // Originless clients are allowed; an explicit Origin must match exactly once.
65            if let Some(origin) = origins.next() &&
66                (origins.next().is_some() || !allowed.contains(origin))
67            {
68                let mut response = Response::new(HttpBody::empty());
69                *response.status_mut() = StatusCode::FORBIDDEN;
70                return Err(response)
71            }
72        }
73        Ok(())
74    }
75}
76
77fn parse_origins(domains: &str) -> Result<Option<Vec<HeaderValue>>, CorsDomainError> {
78    if domains.trim() == "*" {
79        return Ok(None)
80    }
81    let iter = domains.split(',').map(str::trim);
82    if iter.clone().any(|origin| origin == "*") {
83        return Err(CorsDomainError::WildCardNotAllowed { input: domains.to_string() })
84    }
85    iter.map(|domain| {
86        domain.parse().map_err(|_| CorsDomainError::InvalidHeader { domain: domain.to_string() })
87    })
88    .collect::<Result<Vec<_>, _>>()
89    .map(Some)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_wildcard_with_spaces_rejected() {
98        // Wildcard with leading space should still be rejected
99        let result = create_cors_layer("http://example.com, *");
100        assert!(matches!(result, Err(CorsDomainError::WildCardNotAllowed { .. })));
101    }
102}