better error handling for ip_is_authorized()

This commit is contained in:
Rory Neithinger 2023-03-19 15:50:25 -07:00
parent f3fc4924dc
commit c32d12b5e0
3 changed files with 117 additions and 20 deletions

@ -175,7 +175,7 @@ impl From<RpcSecretKey> for Uuid {
} }
impl Authorization { impl Authorization {
pub fn internal(db_conn: Option<DatabaseConnection>) -> anyhow::Result<Self> { pub fn internal(db_conn: Option<DatabaseConnection>) -> Web3ProxyResult<Self> {
let authorization_checks = AuthorizationChecks { let authorization_checks = AuthorizationChecks {
// any error logs on a local (internal) query are likely problems. log them all // any error logs on a local (internal) query are likely problems. log them all
log_revert_chance: 1.0, log_revert_chance: 1.0,
@ -206,7 +206,7 @@ impl Authorization {
proxy_mode: ProxyMode, proxy_mode: ProxyMode,
referer: Option<Referer>, referer: Option<Referer>,
user_agent: Option<UserAgent>, user_agent: Option<UserAgent>,
) -> anyhow::Result<Self> { ) -> Web3ProxyResult<Self> {
// some origins can override max_requests_per_period for anon users // some origins can override max_requests_per_period for anon users
let max_requests_per_period = origin let max_requests_per_period = origin
.as_ref() .as_ref()
@ -244,13 +244,13 @@ impl Authorization {
referer: Option<Referer>, referer: Option<Referer>,
user_agent: Option<UserAgent>, user_agent: Option<UserAgent>,
authorization_type: AuthorizationType, authorization_type: AuthorizationType,
) -> anyhow::Result<Self> { ) -> Web3ProxyResult<Self> {
// check ip // check ip
match &authorization_checks.allowed_ips { match &authorization_checks.allowed_ips {
None => {} None => {}
Some(allowed_ips) => { Some(allowed_ips) => {
if !allowed_ips.iter().any(|x| x.contains(&ip)) { 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) { match (&origin, &authorization_checks.allowed_origins) {
(None, None) => {} (None, None) => {}
(Some(_), None) => {} (Some(_), None) => {}
(None, Some(_)) => return Err(anyhow::anyhow!("Origin required")), (None, Some(_)) => return Err(Web3ProxyError::OriginRequired),
(Some(origin), Some(allowed_origins)) => { (Some(origin), Some(allowed_origins)) => {
if !allowed_origins.contains(origin) { 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) { match (&referer, &authorization_checks.allowed_referers) {
(None, None) => {} (None, None) => {}
(Some(_), None) => {} (Some(_), None) => {}
(None, Some(_)) => return Err(anyhow::anyhow!("Referer required")), (None, Some(_)) => return Err(Web3ProxyError::RefererRequired),
(Some(referer), Some(allowed_referers)) => { (Some(referer), Some(allowed_referers)) => {
if !allowed_referers.contains(referer) { 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) { match (&user_agent, &authorization_checks.allowed_user_agents) {
(None, None) => {} (None, None) => {}
(Some(_), 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)) => { (Some(user_agent), Some(allowed_user_agents)) => {
if !allowed_user_agents.contains(user_agent) { if !allowed_user_agents.contains(user_agent) {
return Err(anyhow::anyhow!( return Err(Web3ProxyError::UserAgentNotAllowed(user_agent.clone()));
"User agent ({}) is not allowed!",
user_agent
));
} }
} }
} }
@ -451,7 +448,7 @@ pub async fn key_is_authorized(
impl Web3ProxyApp { impl Web3ProxyApp {
/// Limit the number of concurrent requests from the given ip address. /// Limit the number of concurrent requests from the given ip address.
pub async fn ip_semaphore(&self, ip: IpAddr) -> anyhow::Result<Option<OwnedSemaphorePermit>> { pub async fn ip_semaphore(&self, ip: IpAddr) -> Web3ProxyResult<Option<OwnedSemaphorePermit>> {
if let Some(max_concurrent_requests) = self.config.public_max_concurrent_requests { if let Some(max_concurrent_requests) = self.config.public_max_concurrent_requests {
let semaphore = self let semaphore = self
.ip_semaphores .ip_semaphores
@ -551,7 +548,7 @@ impl Web3ProxyApp {
&self, &self,
ip: IpAddr, ip: IpAddr,
proxy_mode: ProxyMode, proxy_mode: ProxyMode,
) -> anyhow::Result<RateLimitResult> { ) -> Web3ProxyResult<RateLimitResult> {
// TODO: dry this up with rate_limit_by_rpc_key? // TODO: dry this up with rate_limit_by_rpc_key?
// we don't care about user agent or origin or referer // we don't care about user agent or origin or referer
@ -608,7 +605,7 @@ impl Web3ProxyApp {
ip: IpAddr, ip: IpAddr,
origin: Option<Origin>, origin: Option<Origin>,
proxy_mode: ProxyMode, proxy_mode: ProxyMode,
) -> anyhow::Result<RateLimitResult> { ) -> Web3ProxyResult<RateLimitResult> {
// ip rate limits don't check referer or user agent // ip rate limits don't check referer or user agent
// the do check origin because we can override rate limits for some origins // the do check origin because we can override rate limits for some origins
let authorization = Authorization::external( let authorization = Authorization::external(
@ -786,7 +783,7 @@ impl Web3ProxyApp {
referer: Option<Referer>, referer: Option<Referer>,
rpc_key: RpcSecretKey, rpc_key: RpcSecretKey,
user_agent: Option<UserAgent>, user_agent: Option<UserAgent>,
) -> anyhow::Result<RateLimitResult> { ) -> Web3ProxyResult<RateLimitResult> {
let authorization_checks = self.authorization_checks(proxy_mode, rpc_key).await?; 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 // if no rpc_key_id matching the given rpc was found, then we can't rate limit by key

@ -2,13 +2,16 @@
use super::authorization::Authorization; use super::authorization::Authorization;
use crate::jsonrpc::JsonRpcForwardedResponse; use crate::jsonrpc::JsonRpcForwardedResponse;
use std::net::IpAddr;
use axum::{ use axum::{
headers, headers,
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Json, Json,
}; };
use derive_more::From; use derive_more::{Display, Error, From};
use http::header::InvalidHeaderValue; use http::header::InvalidHeaderValue;
use ipnet::AddrParseError; use ipnet::AddrParseError;
use log::{debug, error, trace, warn}; use log::{debug, error, trace, warn};
@ -22,10 +25,13 @@ pub type Web3ProxyResult<T> = Result<T, Web3ProxyError>;
pub type Web3ProxyResponse = Web3ProxyResult<Response>; pub type Web3ProxyResponse = Web3ProxyResult<Response>;
// TODO: // TODO:
#[derive(Debug, From)] #[derive(Debug, Display, Error, From)]
pub enum Web3ProxyError { pub enum Web3ProxyError {
AccessDenied, AccessDenied,
#[error(ignore)]
Anyhow(anyhow::Error), Anyhow(anyhow::Error),
#[error(ignore)]
#[from(ignore)]
BadRequest(String), BadRequest(String),
SemaphoreAcquireError(AcquireError), SemaphoreAcquireError(AcquireError),
Database(DbErr), Database(DbErr),
@ -34,17 +40,34 @@ pub enum Web3ProxyError {
InfluxDb2RequestError(influxdb2::RequestError), InfluxDb2RequestError(influxdb2::RequestError),
InvalidHeaderValue(InvalidHeaderValue), InvalidHeaderValue(InvalidHeaderValue),
IpAddrParse(AddrParseError), IpAddrParse(AddrParseError),
#[error(ignore)]
#[from(ignore)]
IpNotAllowed(IpAddr),
JoinError(JoinError), JoinError(JoinError),
MsgPackEncode(rmp_serde::encode::Error), MsgPackEncode(rmp_serde::encode::Error),
NotFound, NotFound,
OriginRequired,
#[error(ignore)]
#[from(ignore)]
OriginNotAllowed(headers::Origin),
#[display(fmt = "{:?}, {:?}", _0, _1)]
RateLimited(Authorization, Option<Instant>), RateLimited(Authorization, Option<Instant>),
Redis(RedisError), 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 /// 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<anyhow::Error>), StatusCode(StatusCode, String, Option<anyhow::Error>),
/// TODO: what should be attached to the timout? /// TODO: what should be attached to the timout?
Timeout(tokio::time::error::Elapsed), Timeout(tokio::time::error::Elapsed),
UlidDecode(ulid::DecodeError), UlidDecode(ulid::DecodeError),
UnknownKey, UnknownKey,
UserAgentRequired,
#[error(ignore)]
UserAgentNotAllowed(headers::UserAgent),
} }
impl Web3ProxyError { 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) => { Self::InvalidHeaderValue(err) => {
warn!("InvalidHeaderValue err={:?}", 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 // TODO: this should actually by the id of the key. multiple users might control one key
Self::RateLimited(authorization, retry_at) => { Self::RateLimited(authorization, retry_at) => {
// TODO: emit a stat // 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) => { Self::SemaphoreAcquireError(err) => {
warn!("semaphore acquire err={:?}", err); warn!("semaphore acquire err={:?}", err);
( (
@ -293,6 +371,28 @@ impl Web3ProxyError {
None, 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,
),
)
}
} }
} }
} }

@ -1720,7 +1720,7 @@ mod tests {
OpenRequestResult::Handle(_) OpenRequestResult::Handle(_)
)); ));
let best_available_server_from_none = rpcs let _best_available_server_from_none = rpcs
.best_available_rpc(&authorization, None, &[], None, None) .best_available_rpc(&authorization, None, &[], None, None)
.await; .await;