From c32d12b5e0c28162af0f9929f939c3edcf2a008a Mon Sep 17 00:00:00 2001 From: Rory Neithinger Date: Sun, 19 Mar 2023 15:50:25 -0700 Subject: [PATCH] better error handling for ip_is_authorized() --- web3_proxy/src/frontend/authorization.rs | 31 +++---- web3_proxy/src/frontend/errors.rs | 104 ++++++++++++++++++++++- web3_proxy/src/rpcs/many.rs | 2 +- 3 files changed, 117 insertions(+), 20 deletions(-) diff --git a/web3_proxy/src/frontend/authorization.rs b/web3_proxy/src/frontend/authorization.rs index fb0ad581..0920d9d3 100644 --- a/web3_proxy/src/frontend/authorization.rs +++ b/web3_proxy/src/frontend/authorization.rs @@ -175,7 +175,7 @@ impl From for Uuid { } impl Authorization { - pub fn internal(db_conn: Option) -> anyhow::Result { + pub fn internal(db_conn: Option) -> Web3ProxyResult { let authorization_checks = AuthorizationChecks { // any error logs on a local (internal) query are likely problems. log them all log_revert_chance: 1.0, @@ -206,7 +206,7 @@ impl Authorization { proxy_mode: ProxyMode, referer: Option, user_agent: Option, - ) -> anyhow::Result { + ) -> Web3ProxyResult { // some origins can override max_requests_per_period for anon users let max_requests_per_period = origin .as_ref() @@ -244,13 +244,13 @@ impl Authorization { referer: Option, user_agent: Option, authorization_type: AuthorizationType, - ) -> anyhow::Result { + ) -> Web3ProxyResult { // check ip match &authorization_checks.allowed_ips { None => {} Some(allowed_ips) => { if !allowed_ips.iter().any(|x| x.contains(&ip)) { - return Err(anyhow::anyhow!("IP ({}) is not allowed!", ip)); + return Err(Web3ProxyError::IpNotAllowed(ip)); } } } @@ -259,10 +259,10 @@ impl Authorization { match (&origin, &authorization_checks.allowed_origins) { (None, None) => {} (Some(_), None) => {} - (None, Some(_)) => return Err(anyhow::anyhow!("Origin required")), + (None, Some(_)) => return Err(Web3ProxyError::OriginRequired), (Some(origin), Some(allowed_origins)) => { if !allowed_origins.contains(origin) { - return Err(anyhow::anyhow!("Origin ({}) is not allowed!", origin)); + return Err(Web3ProxyError::OriginNotAllowed(origin.clone())); } } } @@ -271,10 +271,10 @@ impl Authorization { match (&referer, &authorization_checks.allowed_referers) { (None, None) => {} (Some(_), None) => {} - (None, Some(_)) => return Err(anyhow::anyhow!("Referer required")), + (None, Some(_)) => return Err(Web3ProxyError::RefererRequired), (Some(referer), Some(allowed_referers)) => { if !allowed_referers.contains(referer) { - return Err(anyhow::anyhow!("Referer ({:?}) is not allowed!", referer)); + return Err(Web3ProxyError::RefererNotAllowed(referer.clone())); } } } @@ -283,13 +283,10 @@ impl Authorization { match (&user_agent, &authorization_checks.allowed_user_agents) { (None, None) => {} (Some(_), None) => {} - (None, Some(_)) => return Err(anyhow::anyhow!("User agent required")), + (None, Some(_)) => return Err(Web3ProxyError::UserAgentRequired), (Some(user_agent), Some(allowed_user_agents)) => { if !allowed_user_agents.contains(user_agent) { - return Err(anyhow::anyhow!( - "User agent ({}) is not allowed!", - user_agent - )); + return Err(Web3ProxyError::UserAgentNotAllowed(user_agent.clone())); } } } @@ -451,7 +448,7 @@ pub async fn key_is_authorized( impl Web3ProxyApp { /// Limit the number of concurrent requests from the given ip address. - pub async fn ip_semaphore(&self, ip: IpAddr) -> anyhow::Result> { + pub async fn ip_semaphore(&self, ip: IpAddr) -> Web3ProxyResult> { if let Some(max_concurrent_requests) = self.config.public_max_concurrent_requests { let semaphore = self .ip_semaphores @@ -551,7 +548,7 @@ impl Web3ProxyApp { &self, ip: IpAddr, proxy_mode: ProxyMode, - ) -> anyhow::Result { + ) -> Web3ProxyResult { // TODO: dry this up with rate_limit_by_rpc_key? // we don't care about user agent or origin or referer @@ -608,7 +605,7 @@ impl Web3ProxyApp { ip: IpAddr, origin: Option, proxy_mode: ProxyMode, - ) -> anyhow::Result { + ) -> Web3ProxyResult { // ip rate limits don't check referer or user agent // the do check origin because we can override rate limits for some origins let authorization = Authorization::external( @@ -786,7 +783,7 @@ impl Web3ProxyApp { referer: Option, rpc_key: RpcSecretKey, user_agent: Option, - ) -> anyhow::Result { + ) -> Web3ProxyResult { let authorization_checks = self.authorization_checks(proxy_mode, rpc_key).await?; // if no rpc_key_id matching the given rpc was found, then we can't rate limit by key diff --git a/web3_proxy/src/frontend/errors.rs b/web3_proxy/src/frontend/errors.rs index 515f5c86..14bf0185 100644 --- a/web3_proxy/src/frontend/errors.rs +++ b/web3_proxy/src/frontend/errors.rs @@ -2,13 +2,16 @@ use super::authorization::Authorization; use crate::jsonrpc::JsonRpcForwardedResponse; + +use std::net::IpAddr; + use axum::{ headers, http::StatusCode, response::{IntoResponse, Response}, Json, }; -use derive_more::From; +use derive_more::{Display, Error, From}; use http::header::InvalidHeaderValue; use ipnet::AddrParseError; use log::{debug, error, trace, warn}; @@ -22,10 +25,13 @@ pub type Web3ProxyResult = Result; pub type Web3ProxyResponse = Web3ProxyResult; // TODO: -#[derive(Debug, From)] +#[derive(Debug, Display, Error, From)] pub enum Web3ProxyError { AccessDenied, + #[error(ignore)] Anyhow(anyhow::Error), + #[error(ignore)] + #[from(ignore)] BadRequest(String), SemaphoreAcquireError(AcquireError), Database(DbErr), @@ -34,17 +40,34 @@ pub enum Web3ProxyError { InfluxDb2RequestError(influxdb2::RequestError), InvalidHeaderValue(InvalidHeaderValue), IpAddrParse(AddrParseError), + #[error(ignore)] + #[from(ignore)] + IpNotAllowed(IpAddr), JoinError(JoinError), MsgPackEncode(rmp_serde::encode::Error), NotFound, + OriginRequired, + #[error(ignore)] + #[from(ignore)] + OriginNotAllowed(headers::Origin), + #[display(fmt = "{:?}, {:?}", _0, _1)] RateLimited(Authorization, Option), Redis(RedisError), + RefererRequired, + #[display(fmt = "{:?}", _0)] + #[error(ignore)] + #[from(ignore)] + RefererNotAllowed(headers::Referer), /// simple way to return an error message to the user and an anyhow to our logs + #[display(fmt = "{}, {}, {:?}", _0, _1, _2)] StatusCode(StatusCode, String, Option), /// TODO: what should be attached to the timout? Timeout(tokio::time::error::Elapsed), UlidDecode(ulid::DecodeError), UnknownKey, + UserAgentRequired, + #[error(ignore)] + UserAgentNotAllowed(headers::UserAgent), } impl Web3ProxyError { @@ -131,6 +154,17 @@ impl Web3ProxyError { ), ) } + Self::IpNotAllowed(ip) => { + warn!("IpNotAllowed ip={})", ip); + ( + StatusCode::FORBIDDEN, + JsonRpcForwardedResponse::from_string( + format!("IP ({}) is not allowed!", ip), + Some(StatusCode::FORBIDDEN.as_u16().into()), + None, + ), + ) + } Self::InvalidHeaderValue(err) => { warn!("InvalidHeaderValue err={:?}", err); ( @@ -184,6 +218,28 @@ impl Web3ProxyError { ), ) } + Self::OriginRequired => { + warn!("OriginRequired"); + ( + StatusCode::BAD_REQUEST, + JsonRpcForwardedResponse::from_str( + "Origin required", + Some(StatusCode::BAD_REQUEST.as_u16().into()), + None, + ), + ) + } + Self::OriginNotAllowed(origin) => { + warn!("OriginNotAllowed origin={}", origin); + ( + StatusCode::FORBIDDEN, + JsonRpcForwardedResponse::from_string( + format!("Origin ({}) is not allowed!", origin), + Some(StatusCode::FORBIDDEN.as_u16().into()), + None, + ), + ) + } // TODO: this should actually by the id of the key. multiple users might control one key Self::RateLimited(authorization, retry_at) => { // TODO: emit a stat @@ -227,6 +283,28 @@ impl Web3ProxyError { ), ) } + Self::RefererRequired => { + warn!("referer required"); + ( + StatusCode::BAD_REQUEST, + JsonRpcForwardedResponse::from_str( + "Referer required", + Some(StatusCode::BAD_REQUEST.as_u16().into()), + None, + ), + ) + } + Self::RefererNotAllowed(referer) => { + warn!("referer not allowed referer={:?}", referer); + ( + StatusCode::FORBIDDEN, + JsonRpcForwardedResponse::from_string( + format!("Referer ({:?}) is not allowed", referer), + Some(StatusCode::FORBIDDEN.as_u16().into()), + None, + ), + ) + } Self::SemaphoreAcquireError(err) => { warn!("semaphore acquire err={:?}", err); ( @@ -293,6 +371,28 @@ impl Web3ProxyError { None, ), ), + Self::UserAgentRequired => { + warn!("UserAgentRequired"); + ( + StatusCode::BAD_REQUEST, + JsonRpcForwardedResponse::from_str( + "User agent required", + Some(StatusCode::BAD_REQUEST.as_u16().into()), + None, + ), + ) + } + Self::UserAgentNotAllowed(ua) => { + warn!("UserAgentNotAllowed ua={}", ua); + ( + StatusCode::FORBIDDEN, + JsonRpcForwardedResponse::from_string( + format!("User agent ({}) is not allowed!", ua), + Some(StatusCode::FORBIDDEN.as_u16().into()), + None, + ), + ) + } } } } diff --git a/web3_proxy/src/rpcs/many.rs b/web3_proxy/src/rpcs/many.rs index ef704498..a309b2bd 100644 --- a/web3_proxy/src/rpcs/many.rs +++ b/web3_proxy/src/rpcs/many.rs @@ -1720,7 +1720,7 @@ mod tests { OpenRequestResult::Handle(_) )); - let best_available_server_from_none = rpcs + let _best_available_server_from_none = rpcs .best_available_rpc(&authorization, None, &[], None, None) .await;