Merge remote-tracking branch 'rorytrent/Web3ProxyError' into devel

This commit is contained in:
Bryan Stitt 2023-04-05 15:08:57 -07:00
commit 1a160a8c7d
18 changed files with 918 additions and 360 deletions

View File

@ -1,8 +1,8 @@
use crate::app::Web3ProxyApp; use crate::app::Web3ProxyApp;
use crate::frontend::errors::FrontendErrorResponse; use crate::frontend::errors::{Web3ProxyError, Web3ProxyResponse};
use crate::http_params::get_user_id_from_params; use crate::http_params::get_user_id_from_params;
use anyhow::Context; use anyhow::Context;
use axum::response::{IntoResponse, Response}; use axum::response::IntoResponse;
use axum::{ use axum::{
headers::{authorization::Bearer, Authorization}, headers::{authorization::Bearer, Authorization},
Json, TypedHeader, Json, TypedHeader,
@ -24,25 +24,21 @@ pub async fn query_admin_modify_usertier<'a>(
app: &'a Web3ProxyApp, app: &'a Web3ProxyApp,
bearer: Option<TypedHeader<Authorization<Bearer>>>, bearer: Option<TypedHeader<Authorization<Bearer>>>,
params: &'a HashMap<String, String>, params: &'a HashMap<String, String>,
) -> Result<Response, FrontendErrorResponse> { ) -> Web3ProxyResponse {
// Quickly return if any of the input tokens are bad // Quickly return if any of the input tokens are bad
let user_address: Vec<u8> = params let user_address: Vec<u8> = params
.get("user_address") .get("user_address")
.ok_or_else(|| { .ok_or_else(|| {
FrontendErrorResponse::BadRequest( Web3ProxyError::BadRequest("Unable to find user_address key in request".to_string())
"Unable to find user_address key in request".to_string(),
)
})? })?
.parse::<Address>() .parse::<Address>()
.map_err(|_| { .map_err(|_| {
FrontendErrorResponse::BadRequest( Web3ProxyError::BadRequest("Unable to parse user_address as an Address".to_string())
"Unable to parse user_address as an Address".to_string(),
)
})? })?
.to_fixed_bytes() .to_fixed_bytes()
.into(); .into();
let user_tier_title = params.get("user_tier_title").ok_or_else(|| { let user_tier_title = params.get("user_tier_title").ok_or_else(|| {
FrontendErrorResponse::BadRequest( Web3ProxyError::BadRequest(
"Unable to get the user_tier_title key from the request".to_string(), "Unable to get the user_tier_title key from the request".to_string(),
) )
})?; })?;
@ -78,7 +74,7 @@ pub async fn query_admin_modify_usertier<'a>(
.filter(admin::Column::UserId.eq(caller_id)) .filter(admin::Column::UserId.eq(caller_id))
.one(&db_conn) .one(&db_conn)
.await? .await?
.ok_or(FrontendErrorResponse::AccessDenied)?; .ok_or(Web3ProxyError::AccessDenied)?;
// If we are here, that means an admin was found, and we can safely proceed // If we are here, that means an admin was found, and we can safely proceed
@ -87,7 +83,7 @@ pub async fn query_admin_modify_usertier<'a>(
.filter(user::Column::Address.eq(user_address)) .filter(user::Column::Address.eq(user_address))
.one(&db_conn) .one(&db_conn)
.await? .await?
.ok_or(FrontendErrorResponse::BadRequest( .ok_or(Web3ProxyError::BadRequest(
"No user with this id found".to_string(), "No user with this id found".to_string(),
))?; ))?;
// Return early if the target user_tier_id is the same as the original user_tier_id // Return early if the target user_tier_id is the same as the original user_tier_id
@ -101,7 +97,7 @@ pub async fn query_admin_modify_usertier<'a>(
.filter(user_tier::Column::Title.eq(user_tier_title.clone())) .filter(user_tier::Column::Title.eq(user_tier_title.clone()))
.one(&db_conn) .one(&db_conn)
.await? .await?
.ok_or(FrontendErrorResponse::BadRequest( .ok_or(Web3ProxyError::BadRequest(
"User Tier name was not found".to_string(), "User Tier name was not found".to_string(),
))?; ))?;

View File

@ -4,12 +4,12 @@ mod ws;
use crate::block_number::{block_needed, BlockNeeded}; use crate::block_number::{block_needed, BlockNeeded};
use crate::config::{AppConfig, TopConfig}; use crate::config::{AppConfig, TopConfig};
use crate::frontend::authorization::{Authorization, RequestMetadata, RpcSecretKey}; use crate::frontend::authorization::{Authorization, RequestMetadata, RpcSecretKey};
use crate::frontend::errors::FrontendErrorResponse; use crate::frontend::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResult};
use crate::frontend::rpc_proxy_ws::ProxyMode; use crate::frontend::rpc_proxy_ws::ProxyMode;
use crate::jsonrpc::{ use crate::jsonrpc::{
JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest, JsonRpcRequestEnum, JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest, JsonRpcRequestEnum,
}; };
use crate::rpcs::blockchain::{BlocksByHashCache, Web3ProxyBlock}; use crate::rpcs::blockchain::Web3ProxyBlock;
use crate::rpcs::consensus::ConsensusWeb3Rpcs; use crate::rpcs::consensus::ConsensusWeb3Rpcs;
use crate::rpcs::many::Web3Rpcs; use crate::rpcs::many::Web3Rpcs;
use crate::rpcs::one::Web3Rpc; use crate::rpcs::one::Web3Rpc;
@ -18,6 +18,7 @@ use crate::stats::{AppStat, RpcQueryStats, StatBuffer};
use crate::user_token::UserBearerToken; use crate::user_token::UserBearerToken;
use anyhow::Context; use anyhow::Context;
use axum::headers::{Origin, Referer, UserAgent}; use axum::headers::{Origin, Referer, UserAgent};
use axum::http::StatusCode;
use chrono::Utc; use chrono::Utc;
use deferred_rate_limiter::DeferredRateLimiter; use deferred_rate_limiter::DeferredRateLimiter;
use derive_more::From; use derive_more::From;
@ -1051,7 +1052,7 @@ impl Web3ProxyApp {
self: &Arc<Self>, self: &Arc<Self>,
authorization: Arc<Authorization>, authorization: Arc<Authorization>,
request: JsonRpcRequestEnum, request: JsonRpcRequestEnum,
) -> Result<(JsonRpcForwardedResponseEnum, Vec<Arc<Web3Rpc>>), FrontendErrorResponse> { ) -> Web3ProxyResult<(JsonRpcForwardedResponseEnum, Vec<Arc<Web3Rpc>>)> {
// trace!(?request, "proxy_web3_rpc"); // trace!(?request, "proxy_web3_rpc");
// even though we have timeouts on the requests to our backend providers, // even though we have timeouts on the requests to our backend providers,
@ -1089,7 +1090,7 @@ impl Web3ProxyApp {
self: &Arc<Self>, self: &Arc<Self>,
authorization: &Arc<Authorization>, authorization: &Arc<Authorization>,
requests: Vec<JsonRpcRequest>, requests: Vec<JsonRpcRequest>,
) -> Result<(Vec<JsonRpcForwardedResponse>, Vec<Arc<Web3Rpc>>), FrontendErrorResponse> { ) -> Web3ProxyResult<(Vec<JsonRpcForwardedResponse>, Vec<Arc<Web3Rpc>>)> {
// TODO: we should probably change ethers-rs to support this directly. they pushed this off to v2 though // TODO: we should probably change ethers-rs to support this directly. they pushed this off to v2 though
let num_requests = requests.len(); let num_requests = requests.len();
@ -1097,12 +1098,11 @@ impl Web3ProxyApp {
// TODO: improve flattening // TODO: improve flattening
// get the head block now so that any requests that need it all use the same block // get the head block now so that any requests that need it all use the same block
// TODO: FrontendErrorResponse that handles "no servers synced" in a consistent way
// TODO: this still has an edge condition if there is a reorg in the middle of the request!!! // TODO: this still has an edge condition if there is a reorg in the middle of the request!!!
let head_block_num = self let head_block_num = self
.balanced_rpcs .balanced_rpcs
.head_block_num() .head_block_num()
.context(anyhow::anyhow!("no servers synced"))?; .ok_or(Web3ProxyError::NoServersSynced)?;
let responses = join_all( let responses = join_all(
requests requests
@ -1163,10 +1163,10 @@ impl Web3ProxyApp {
authorization: &Arc<Authorization>, authorization: &Arc<Authorization>,
mut request: JsonRpcRequest, mut request: JsonRpcRequest,
head_block_num: Option<U64>, head_block_num: Option<U64>,
) -> Result<(JsonRpcForwardedResponse, Vec<Arc<Web3Rpc>>), FrontendErrorResponse> { ) -> Web3ProxyResult<(JsonRpcForwardedResponse, Vec<Arc<Web3Rpc>>)> {
// trace!("Received request: {:?}", request); // trace!("Received request: {:?}", request);
let request_metadata = Arc::new(RequestMetadata::new(request.num_bytes())?); let request_metadata = Arc::new(RequestMetadata::new(request.num_bytes()));
let mut kafka_stuff = None; let mut kafka_stuff = None;
@ -1347,10 +1347,7 @@ impl Web3ProxyApp {
} }
None => { None => {
// TODO: what does geth do if this happens? // TODO: what does geth do if this happens?
// TODO: i think we want a 502 so that haproxy retries on another server return Err(Web3ProxyError::UnknownBlockNumber);
return Err(
anyhow::anyhow!("no servers synced. unknown eth_blockNumber").into(),
);
} }
} }
} }
@ -1377,7 +1374,7 @@ impl Web3ProxyApp {
let mut gas_estimate: U256 = if let Some(gas_estimate) = response.result.take() { let mut gas_estimate: U256 = if let Some(gas_estimate) = response.result.take() {
serde_json::from_str(gas_estimate.get()) serde_json::from_str(gas_estimate.get())
.context("gas estimate result is not an U256")? .or(Err(Web3ProxyError::GasEstimateNotU256))?
} else { } else {
// i think this is always an error response // i think this is always an error response
let rpcs = request_metadata.backend_requests.lock().clone(); let rpcs = request_metadata.backend_requests.lock().clone();
@ -1438,7 +1435,7 @@ impl Web3ProxyApp {
let head_block_num = head_block_num let head_block_num = head_block_num
.or(self.balanced_rpcs.head_block_num()) .or(self.balanced_rpcs.head_block_num())
.ok_or_else(|| anyhow::anyhow!("no servers synced"))?; .ok_or_else(|| Web3ProxyError::NoServersSynced)?;
// TODO: error/wait if no head block! // TODO: error/wait if no head block!
@ -1467,15 +1464,15 @@ impl Web3ProxyApp {
{ {
let params = request let params = request
.params .params
.context("there must be params if we got this far")?; .web3_context("there must be params if we got this far")?;
let params = params let params = params
.as_array() .as_array()
.context("there must be an array if we got this far")? .web3_context("there must be an array if we got this far")?
.get(0) .get(0)
.context("there must be an item if we got this far")? .web3_context("there must be an item if we got this far")?
.as_str() .as_str()
.context("there must be a string if we got this far")?; .web3_context("there must be a string if we got this far")?;
let params = Bytes::from_str(params) let params = Bytes::from_str(params)
.expect("there must be Bytes if we got this far"); .expect("there must be Bytes if we got this far");
@ -1599,11 +1596,12 @@ impl Web3ProxyApp {
let param = Bytes::from_str( let param = Bytes::from_str(
params[0] params[0]
.as_str() .as_str()
.context("parsing params 0 into str then bytes")?, .ok_or(Web3ProxyError::ParseBytesError(None))
.web3_context("parsing params 0 into str then bytes")?,
) )
.map_err(|x| { .map_err(|x| {
trace!("bad request: {:?}", x); trace!("bad request: {:?}", x);
FrontendErrorResponse::BadRequest( Web3ProxyError::BadRequest(
"param 0 could not be read as H256".to_string(), "param 0 could not be read as H256".to_string(),
) )
})?; })?;
@ -1618,7 +1616,7 @@ impl Web3ProxyApp {
return Ok(( return Ok((
JsonRpcForwardedResponse::from_str( JsonRpcForwardedResponse::from_str(
"invalid request", "invalid request",
None, Some(StatusCode::BAD_REQUEST.as_u16().into()),
Some(request_id), Some(request_id),
), ),
vec![], vec![],
@ -1640,7 +1638,7 @@ impl Web3ProxyApp {
method => { method => {
if method.starts_with("admin_") { if method.starts_with("admin_") {
// TODO: emit a stat? will probably just be noise // TODO: emit a stat? will probably just be noise
return Err(FrontendErrorResponse::AccessDenied); return Err(Web3ProxyError::AccessDenied);
} }
// emit stats // emit stats
@ -1648,7 +1646,7 @@ impl Web3ProxyApp {
// TODO: if no servers synced, wait for them to be synced? probably better to error and let haproxy retry another server // TODO: if no servers synced, wait for them to be synced? probably better to error and let haproxy retry another server
let head_block_num = head_block_num let head_block_num = head_block_num
.or(self.balanced_rpcs.head_block_num()) .or(self.balanced_rpcs.head_block_num())
.context("no servers synced")?; .ok_or(Web3ProxyError::NoServersSynced)?;
// we do this check before checking caches because it might modify the request params // we do this check before checking caches because it might modify the request params
// TODO: add a stat for archive vs full since they should probably cost different // TODO: add a stat for archive vs full since they should probably cost different
@ -1771,17 +1769,10 @@ impl Web3ProxyApp {
// TODO: only cache the inner response // TODO: only cache the inner response
// TODO: how are we going to stream this? // TODO: how are we going to stream this?
// TODO: check response size. if its very large, return it in a custom Error type that bypasses caching? or will moka do that for us? // TODO: check response size. if its very large, return it in a custom Error type that bypasses caching? or will moka do that for us?
Ok::<_, anyhow::Error>(response) Ok::<_, Web3ProxyError>(response)
}) })
.await // TODO: add context (error while caching and forwarding response {})
// TODO: what is the best way to handle an Arc here? .await?
.map_err(|err| {
// TODO: emit a stat for an error
anyhow::anyhow!(
"error while caching and forwarding response: {}",
err
)
})?
} else { } else {
self.balanced_rpcs self.balanced_rpcs
.try_proxy_connection( .try_proxy_connection(
@ -1813,7 +1804,7 @@ impl Web3ProxyApp {
stat_sender stat_sender
.send_async(response_stat.into()) .send_async(response_stat.into())
.await .await
.context("stat_sender sending response_stat")?; .map_err(Web3ProxyError::SendAppStatError)?;
} }
return Ok((response, rpcs)); return Ok((response, rpcs));
@ -1836,7 +1827,7 @@ impl Web3ProxyApp {
stat_sender stat_sender
.send_async(response_stat.into()) .send_async(response_stat.into())
.await .await
.context("stat_sender sending response stat")?; .map_err(Web3ProxyError::SendAppStatError)?;
} }
if let Some((kafka_topic, kafka_key, kafka_headers)) = kafka_stuff { if let Some((kafka_topic, kafka_key, kafka_headers)) = kafka_stuff {
@ -1846,7 +1837,7 @@ impl Web3ProxyApp {
.expect("if headers are set, producer must exist"); .expect("if headers are set, producer must exist");
let response_bytes = let response_bytes =
rmp_serde::to_vec(&response).context("failed msgpack serialize response")?; rmp_serde::to_vec(&response).web3_context("failed msgpack serialize response")?;
let f = async move { let f = async move {
let produce_future = kafka_producer.send( let produce_future = kafka_producer.send(

View File

@ -2,11 +2,11 @@
use super::Web3ProxyApp; use super::Web3ProxyApp;
use crate::frontend::authorization::{Authorization, RequestMetadata}; use crate::frontend::authorization::{Authorization, RequestMetadata};
use crate::frontend::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResult};
use crate::jsonrpc::JsonRpcForwardedResponse; use crate::jsonrpc::JsonRpcForwardedResponse;
use crate::jsonrpc::JsonRpcRequest; use crate::jsonrpc::JsonRpcRequest;
use crate::rpcs::transactions::TxStatus; use crate::rpcs::transactions::TxStatus;
use crate::stats::RpcQueryStats; use crate::stats::RpcQueryStats;
use anyhow::Context;
use axum::extract::ws::Message; use axum::extract::ws::Message;
use ethers::prelude::U64; use ethers::prelude::U64;
use futures::future::AbortHandle; use futures::future::AbortHandle;
@ -27,13 +27,13 @@ impl Web3ProxyApp {
subscription_count: &'a AtomicUsize, subscription_count: &'a AtomicUsize,
// TODO: taking a sender for Message instead of the exact json we are planning to send feels wrong, but its easier for now // TODO: taking a sender for Message instead of the exact json we are planning to send feels wrong, but its easier for now
response_sender: flume::Sender<Message>, response_sender: flume::Sender<Message>,
) -> anyhow::Result<(AbortHandle, JsonRpcForwardedResponse)> { ) -> Web3ProxyResult<(AbortHandle, JsonRpcForwardedResponse)> {
// TODO: this is not efficient // TODO: this is not efficient
let request_bytes = serde_json::to_string(&request_json) let request_bytes = serde_json::to_string(&request_json)
.context("finding request size")? .web3_context("finding request size")?
.len(); .len();
let request_metadata = Arc::new(RequestMetadata::new(request_bytes).unwrap()); let request_metadata = Arc::new(RequestMetadata::new(request_bytes));
let (subscription_abort_handle, subscription_registration) = AbortHandle::new_pair(); let (subscription_abort_handle, subscription_registration) = AbortHandle::new_pair();
@ -67,7 +67,7 @@ impl Web3ProxyApp {
}; };
// TODO: what should the payload for RequestMetadata be? // TODO: what should the payload for RequestMetadata be?
let request_metadata = Arc::new(RequestMetadata::new(0).unwrap()); let request_metadata = Arc::new(RequestMetadata::new(0));
// TODO: make a struct for this? using our JsonRpcForwardedResponse won't work because it needs an id // TODO: make a struct for this? using our JsonRpcForwardedResponse won't work because it needs an id
let response_json = json!({ let response_json = json!({
@ -133,7 +133,7 @@ impl Web3ProxyApp {
// TODO: do something with this handle? // TODO: do something with this handle?
tokio::spawn(async move { tokio::spawn(async move {
while let Some(Ok(new_tx_state)) = pending_tx_receiver.next().await { while let Some(Ok(new_tx_state)) = pending_tx_receiver.next().await {
let request_metadata = Arc::new(RequestMetadata::new(0).unwrap()); let request_metadata = Arc::new(RequestMetadata::new(0));
let new_tx = match new_tx_state { let new_tx = match new_tx_state {
TxStatus::Pending(tx) => tx, TxStatus::Pending(tx) => tx,
@ -208,7 +208,7 @@ impl Web3ProxyApp {
// TODO: do something with this handle? // TODO: do something with this handle?
tokio::spawn(async move { tokio::spawn(async move {
while let Some(Ok(new_tx_state)) = pending_tx_receiver.next().await { while let Some(Ok(new_tx_state)) = pending_tx_receiver.next().await {
let request_metadata = Arc::new(RequestMetadata::new(0).unwrap()); let request_metadata = Arc::new(RequestMetadata::new(0));
let new_tx = match new_tx_state { let new_tx = match new_tx_state {
TxStatus::Pending(tx) => tx, TxStatus::Pending(tx) => tx,
@ -284,7 +284,7 @@ impl Web3ProxyApp {
// TODO: do something with this handle? // TODO: do something with this handle?
tokio::spawn(async move { tokio::spawn(async move {
while let Some(Ok(new_tx_state)) = pending_tx_receiver.next().await { while let Some(Ok(new_tx_state)) = pending_tx_receiver.next().await {
let request_metadata = Arc::new(RequestMetadata::new(0).unwrap()); let request_metadata = Arc::new(RequestMetadata::new(0));
let new_tx = match new_tx_state { let new_tx = match new_tx_state {
TxStatus::Pending(tx) => tx, TxStatus::Pending(tx) => tx,
@ -341,7 +341,7 @@ impl Web3ProxyApp {
); );
}); });
} }
_ => return Err(anyhow::anyhow!("unimplemented")), _ => return Err(Web3ProxyError::NotImplemented),
} }
// TODO: do something with subscription_join_handle? // TODO: do something with subscription_join_handle?

View File

@ -1,4 +1,5 @@
//! Helper functions for turning ether's BlockNumber into numbers and updating incoming queries to match. //! Helper functions for turning ether's BlockNumber into numbers and updating incoming queries to match.
use crate::frontend::errors::{Web3ProxyError, Web3ProxyResult};
use anyhow::Context; use anyhow::Context;
use ethers::{ use ethers::{
prelude::{BlockNumber, U64}, prelude::{BlockNumber, U64},
@ -126,7 +127,7 @@ pub async fn block_needed(
params: Option<&mut serde_json::Value>, params: Option<&mut serde_json::Value>,
head_block_num: U64, head_block_num: U64,
rpcs: &Web3Rpcs, rpcs: &Web3Rpcs,
) -> anyhow::Result<BlockNeeded> { ) -> Web3ProxyResult<BlockNeeded> {
// some requests have potentially very large responses // some requests have potentially very large responses
// TODO: only skip caching if the response actually is large // TODO: only skip caching if the response actually is large
if method.starts_with("trace_") || method == "debug_traceTransaction" { if method.starts_with("trace_") || method == "debug_traceTransaction" {
@ -181,7 +182,7 @@ pub async fn block_needed(
.get_mut(0) .get_mut(0)
.ok_or_else(|| anyhow::anyhow!("invalid format. no params"))? .ok_or_else(|| anyhow::anyhow!("invalid format. no params"))?
.as_object_mut() .as_object_mut()
.ok_or_else(|| anyhow::anyhow!("invalid format"))?; .ok_or_else(|| Web3ProxyError::BadRequest("invalid format".to_string()))?;
if obj.contains_key("blockHash") { if obj.contains_key("blockHash") {
return Ok(BlockNeeded::CacheSuccessForever); return Ok(BlockNeeded::CacheSuccessForever);

View File

@ -1,13 +1,12 @@
//! Handle admin helper logic //! Handle admin helper logic
use super::authorization::login_is_authorized; use super::authorization::login_is_authorized;
use super::errors::FrontendResult; use super::errors::Web3ProxyResponse;
use crate::admin_queries::query_admin_modify_usertier; use crate::admin_queries::query_admin_modify_usertier;
use crate::app::Web3ProxyApp; use crate::app::Web3ProxyApp;
use crate::frontend::errors::FrontendErrorResponse; use crate::frontend::errors::{Web3ProxyError, Web3ProxyErrorContext};
use crate::user_token::UserBearerToken; use crate::user_token::UserBearerToken;
use crate::PostLogin; use crate::PostLogin;
use anyhow::Context;
use axum::{ use axum::{
extract::{Path, Query}, extract::{Path, Query},
headers::{authorization::Bearer, Authorization}, headers::{authorization::Bearer, Authorization},
@ -43,7 +42,7 @@ pub async fn admin_change_user_roles(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
bearer: Option<TypedHeader<Authorization<Bearer>>>, bearer: Option<TypedHeader<Authorization<Bearer>>>,
Query(params): Query<HashMap<String, String>>, Query(params): Query<HashMap<String, String>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let response = query_admin_modify_usertier(&app, bearer, &params).await?; let response = query_admin_modify_usertier(&app, bearer, &params).await?;
Ok(response) Ok(response)
@ -58,7 +57,7 @@ pub async fn admin_login_get(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
InsecureClientIp(ip): InsecureClientIp, InsecureClientIp(ip): InsecureClientIp,
Path(mut params): Path<HashMap<String, String>>, Path(mut params): Path<HashMap<String, String>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
// First check if the login is authorized // First check if the login is authorized
login_is_authorized(&app, ip).await?; login_is_authorized(&app, ip).await?;
@ -85,30 +84,22 @@ pub async fn admin_login_get(
let admin_address: Address = params let admin_address: Address = params
.get("admin_address") .get("admin_address")
.ok_or_else(|| { .ok_or_else(|| {
FrontendErrorResponse::BadRequest( Web3ProxyError::BadRequest("Unable to find admin_address key in request".to_string())
"Unable to find admin_address key in request".to_string(),
)
})? })?
.parse::<Address>() .parse::<Address>()
.map_err(|_err| { .map_err(|_err| {
FrontendErrorResponse::BadRequest( Web3ProxyError::BadRequest("Unable to parse admin_address as an Address".to_string())
"Unable to parse admin_address as an Address".to_string(),
)
})?; })?;
// Fetch the user_address parameter from the login string ... (as who we want to be logging in ...) // Fetch the user_address parameter from the login string ... (as who we want to be logging in ...)
let user_address: Vec<u8> = params let user_address: Vec<u8> = params
.get("user_address") .get("user_address")
.ok_or_else(|| { .ok_or_else(|| {
FrontendErrorResponse::BadRequest( Web3ProxyError::BadRequest("Unable to find user_address key in request".to_string())
"Unable to find user_address key in request".to_string(),
)
})? })?
.parse::<Address>() .parse::<Address>()
.map_err(|_err| { .map_err(|_err| {
FrontendErrorResponse::BadRequest( Web3ProxyError::BadRequest("Unable to parse user_address as an Address".to_string())
"Unable to parse user_address as an Address".to_string(),
)
})? })?
.to_fixed_bytes() .to_fixed_bytes()
.into(); .into();
@ -147,10 +138,10 @@ pub async fn admin_login_get(
let admin_address: Vec<u8> = admin_address.to_fixed_bytes().into(); let admin_address: Vec<u8> = admin_address.to_fixed_bytes().into();
let db_conn = app.db_conn().context("login requires a database")?; let db_conn = app.db_conn().web3_context("login requires a database")?;
let db_replica = app let db_replica = app
.db_replica() .db_replica()
.context("login requires a replica database")?; .web3_context("login requires a replica database")?;
// Get the user that we want to imitate from the read-only database (their id ...) // Get the user that we want to imitate from the read-only database (their id ...)
// TODO: Only get the id, not the whole user object ... // TODO: Only get the id, not the whole user object ...
@ -158,7 +149,7 @@ pub async fn admin_login_get(
.filter(user::Column::Address.eq(user_address)) .filter(user::Column::Address.eq(user_address))
.one(db_replica.conn()) .one(db_replica.conn())
.await? .await?
.ok_or(FrontendErrorResponse::BadRequest( .ok_or(Web3ProxyError::BadRequest(
"Could not find user in db".to_string(), "Could not find user in db".to_string(),
))?; ))?;
@ -169,7 +160,7 @@ pub async fn admin_login_get(
.filter(user::Column::Address.eq(admin_address)) .filter(user::Column::Address.eq(admin_address))
.one(db_replica.conn()) .one(db_replica.conn())
.await? .await?
.ok_or(FrontendErrorResponse::BadRequest( .ok_or(Web3ProxyError::BadRequest(
"Could not find admin in db".to_string(), "Could not find admin in db".to_string(),
))?; ))?;
@ -184,7 +175,7 @@ pub async fn admin_login_get(
trail trail
.save(&db_conn) .save(&db_conn)
.await .await
.context("saving user's pending_login")?; .web3_context("saving user's pending_login")?;
// Can there be two login-sessions at the same time? // Can there be two login-sessions at the same time?
// I supposed if the user logs in, the admin would be logged out and vice versa // I supposed if the user logs in, the admin would be logged out and vice versa
@ -209,7 +200,7 @@ pub async fn admin_login_get(
user_pending_login user_pending_login
.save(&db_conn) .save(&db_conn)
.await .await
.context("saving an admin trail pre login")?; .web3_context("saving an admin trail pre login")?;
// there are multiple ways to sign messages and not all wallets support them // there are multiple ways to sign messages and not all wallets support them
// TODO: default message eip from config? // TODO: default message eip from config?
@ -223,7 +214,7 @@ pub async fn admin_login_get(
"eip4361" => message.to_string(), "eip4361" => message.to_string(),
_ => { _ => {
// TODO: custom error that is handled a 401 // TODO: custom error that is handled a 401
return Err(anyhow::anyhow!("invalid message eip given").into()); return Err(Web3ProxyError::InvalidEip);
} }
}; };
@ -238,14 +229,14 @@ pub async fn admin_login_post(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
InsecureClientIp(ip): InsecureClientIp, InsecureClientIp(ip): InsecureClientIp,
Json(payload): Json<PostLogin>, Json(payload): Json<PostLogin>,
) -> FrontendResult { ) -> Web3ProxyResponse {
login_is_authorized(&app, ip).await?; login_is_authorized(&app, ip).await?;
// Check for the signed bytes .. // Check for the signed bytes ..
// TODO: this seems too verbose. how can we simply convert a String into a [u8; 65] // TODO: this seems too verbose. how can we simply convert a String into a [u8; 65]
let their_sig_bytes = Bytes::from_str(&payload.sig).context("parsing sig")?; let their_sig_bytes = Bytes::from_str(&payload.sig).web3_context("parsing sig")?;
if their_sig_bytes.len() != 65 { if their_sig_bytes.len() != 65 {
return Err(anyhow::anyhow!("checking signature length").into()); return Err(Web3ProxyError::InvalidSignatureLength);
} }
let mut their_sig: [u8; 65] = [0; 65]; let mut their_sig: [u8; 65] = [0; 65];
for x in 0..65 { for x in 0..65 {
@ -255,17 +246,18 @@ pub async fn admin_login_post(
// we can't trust that they didn't tamper with the message in some way. like some clients return it hex encoded // we can't trust that they didn't tamper with the message in some way. like some clients return it hex encoded
// TODO: checking 0x seems fragile, but I think it will be fine. siwe message text shouldn't ever start with 0x // TODO: checking 0x seems fragile, but I think it will be fine. siwe message text shouldn't ever start with 0x
let their_msg: Message = if payload.msg.starts_with("0x") { let their_msg: Message = if payload.msg.starts_with("0x") {
let their_msg_bytes = Bytes::from_str(&payload.msg).context("parsing payload message")?; let their_msg_bytes =
Bytes::from_str(&payload.msg).web3_context("parsing payload message")?;
// TODO: lossy or no? // TODO: lossy or no?
String::from_utf8_lossy(their_msg_bytes.as_ref()) String::from_utf8_lossy(their_msg_bytes.as_ref())
.parse::<siwe::Message>() .parse::<siwe::Message>()
.context("parsing hex string message")? .web3_context("parsing hex string message")?
} else { } else {
payload payload
.msg .msg
.parse::<siwe::Message>() .parse::<siwe::Message>()
.context("parsing string message")? .web3_context("parsing string message")?
}; };
// the only part of the message we will trust is their nonce // the only part of the message we will trust is their nonce
@ -273,7 +265,9 @@ pub async fn admin_login_post(
let login_nonce = UserBearerToken::from_str(&their_msg.nonce)?; let login_nonce = UserBearerToken::from_str(&their_msg.nonce)?;
// fetch the message we gave them from our database // fetch the message we gave them from our database
let db_replica = app.db_replica().context("Getting database connection")?; let db_replica = app
.db_replica()
.web3_context("Getting database connection")?;
// massage type for the db // massage type for the db
let login_nonce_uuid: Uuid = login_nonce.clone().into(); let login_nonce_uuid: Uuid = login_nonce.clone().into();
@ -283,30 +277,30 @@ pub async fn admin_login_post(
.filter(pending_login::Column::Nonce.eq(login_nonce_uuid)) .filter(pending_login::Column::Nonce.eq(login_nonce_uuid))
.one(db_replica.conn()) .one(db_replica.conn())
.await .await
.context("database error while finding pending_login")? .web3_context("database error while finding pending_login")?
.context("login nonce not found")?; .web3_context("login nonce not found")?;
let our_msg: siwe::Message = user_pending_login let our_msg: siwe::Message = user_pending_login
.message .message
.parse() .parse()
.context("parsing siwe message")?; .web3_context("parsing siwe message")?;
// default options are fine. the message includes timestamp and domain and nonce // default options are fine. the message includes timestamp and domain and nonce
let verify_config = VerificationOpts::default(); let verify_config = VerificationOpts::default();
let db_conn = app let db_conn = app
.db_conn() .db_conn()
.context("deleting expired pending logins requires a db")?; .web3_context("deleting expired pending logins requires a db")?;
if let Err(err_1) = our_msg if let Err(err_1) = our_msg
.verify(&their_sig, &verify_config) .verify(&their_sig, &verify_config)
.await .await
.context("verifying signature against our local message") .web3_context("verifying signature against our local message")
{ {
// verification method 1 failed. try eip191 // verification method 1 failed. try eip191
if let Err(err_191) = our_msg if let Err(err_191) = our_msg
.verify_eip191(&their_sig) .verify_eip191(&their_sig)
.context("verifying eip191 signature against our local message") .web3_context("verifying eip191 signature against our local message")
{ {
// delete ALL expired rows. // delete ALL expired rows.
let now = Utc::now(); let now = Utc::now();
@ -318,18 +312,16 @@ pub async fn admin_login_post(
// TODO: emit a stat? if this is high something weird might be happening // TODO: emit a stat? if this is high something weird might be happening
debug!("cleared expired pending_logins: {:?}", delete_result); debug!("cleared expired pending_logins: {:?}", delete_result);
return Err(anyhow::anyhow!( return Err(Web3ProxyError::EipVerificationFailed(
"both the primary and eip191 verification failed: {:#?}; {:#?}", Box::new(err_1),
err_1, Box::new(err_191),
err_191 ));
)
.into());
} }
} }
let imitating_user_id = user_pending_login let imitating_user_id = user_pending_login
.imitating_user .imitating_user
.context("getting address of the imitating user")?; .web3_context("getting address of the imitating user")?;
// TODO: limit columns or load whole user? // TODO: limit columns or load whole user?
// TODO: Right now this loads the whole admin. I assume we might want to load the user though (?) figure this out as we go along... // TODO: Right now this loads the whole admin. I assume we might want to load the user though (?) figure this out as we go along...
@ -337,13 +329,13 @@ pub async fn admin_login_post(
.filter(user::Column::Address.eq(our_msg.address.as_ref())) .filter(user::Column::Address.eq(our_msg.address.as_ref()))
.one(db_replica.conn()) .one(db_replica.conn())
.await? .await?
.context("getting admin address")?; .web3_context("getting admin address")?;
let imitating_user = user::Entity::find() let imitating_user = user::Entity::find()
.filter(user::Column::Id.eq(imitating_user_id)) .filter(user::Column::Id.eq(imitating_user_id))
.one(db_replica.conn()) .one(db_replica.conn())
.await? .await?
.context("admin address was not found!")?; .web3_context("admin address was not found!")?;
// Add a message that the admin has logged in // Add a message that the admin has logged in
// Note that the admin is trying to log in as this user // Note that the admin is trying to log in as this user
@ -357,7 +349,7 @@ pub async fn admin_login_post(
trail trail
.save(&db_conn) .save(&db_conn)
.await .await
.context("saving an admin trail post login")?; .web3_context("saving an admin trail post login")?;
// I supposed we also get the rpc_key, whatever this is used for (?). // I supposed we also get the rpc_key, whatever this is used for (?).
// I think the RPC key should still belong to the admin though in this case ... // I think the RPC key should still belong to the admin though in this case ...
@ -367,7 +359,7 @@ pub async fn admin_login_post(
.filter(rpc_key::Column::UserId.eq(admin.id)) .filter(rpc_key::Column::UserId.eq(admin.id))
.all(db_replica.conn()) .all(db_replica.conn())
.await .await
.context("failed loading user's key")?; .web3_context("failed loading user's key")?;
// create a bearer token for the user. // create a bearer token for the user.
let user_bearer_token = UserBearerToken::default(); let user_bearer_token = UserBearerToken::default();
@ -408,7 +400,7 @@ pub async fn admin_login_post(
user_login user_login
.save(&db_conn) .save(&db_conn)
.await .await
.context("saving user login")?; .web3_context("saving user login")?;
if let Err(err) = user_pending_login if let Err(err) = user_pending_login
.into_active_model() .into_active_model()
@ -427,10 +419,12 @@ pub async fn admin_login_post(
pub async fn admin_logout_post( pub async fn admin_logout_post(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let user_bearer = UserBearerToken::try_from(bearer)?; let user_bearer = UserBearerToken::try_from(bearer)?;
let db_conn = app.db_conn().context("database needed for user logout")?; let db_conn = app
.db_conn()
.web3_context("database needed for user logout")?;
if let Err(err) = login::Entity::delete_many() if let Err(err) = login::Entity::delete_many()
.filter(login::Column::BearerToken.eq(user_bearer.uuid())) .filter(login::Column::BearerToken.eq(user_bearer.uuid()))

View File

@ -1,11 +1,10 @@
//! Utilities for authorization of logged in and anonymous users. //! Utilities for authorization of logged in and anonymous users.
use super::errors::FrontendErrorResponse; use super::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResult};
use super::rpc_proxy_ws::ProxyMode; use super::rpc_proxy_ws::ProxyMode;
use crate::app::{AuthorizationChecks, Web3ProxyApp, APP_USER_AGENT}; use crate::app::{AuthorizationChecks, Web3ProxyApp, APP_USER_AGENT};
use crate::rpcs::one::Web3Rpc; use crate::rpcs::one::Web3Rpc;
use crate::user_token::UserBearerToken; use crate::user_token::UserBearerToken;
use anyhow::Context;
use axum::headers::authorization::Bearer; use axum::headers::authorization::Bearer;
use axum::headers::{Header, Origin, Referer, UserAgent}; use axum::headers::{Header, Origin, Referer, UserAgent};
use chrono::Utc; use chrono::Utc;
@ -88,11 +87,11 @@ pub struct RequestMetadata {
} }
impl RequestMetadata { impl RequestMetadata {
pub fn new(request_bytes: usize) -> anyhow::Result<Self> { pub fn new(request_bytes: usize) -> Self {
// TODO: how can we do this without turning it into a string first. this is going to slow us down! // TODO: how can we do this without turning it into a string first. this is going to slow us down!
let request_bytes = request_bytes as u64; let request_bytes = request_bytes as u64;
let new = Self { Self {
start_instant: Instant::now(), start_instant: Instant::now(),
request_bytes, request_bytes,
archive_request: false.into(), archive_request: false.into(),
@ -102,9 +101,7 @@ impl RequestMetadata {
response_bytes: 0.into(), response_bytes: 0.into(),
response_millis: 0.into(), response_millis: 0.into(),
response_from_backup_rpc: false.into(), response_from_backup_rpc: false.into(),
}; }
Ok(new)
} }
} }
@ -130,7 +127,7 @@ impl Display for RpcSecretKey {
} }
impl FromStr for RpcSecretKey { impl FromStr for RpcSecretKey {
type Err = anyhow::Error; type Err = Web3ProxyError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(ulid) = s.parse::<Ulid>() { if let Ok(ulid) = s.parse::<Ulid>() {
@ -139,7 +136,7 @@ impl FromStr for RpcSecretKey {
Ok(uuid.into()) Ok(uuid.into())
} else { } else {
// TODO: custom error type so that this shows as a 400 // TODO: custom error type so that this shows as a 400
Err(anyhow::anyhow!("UserKey was not a ULID or UUID")) Err(Web3ProxyError::InvalidUserKey)
} }
} }
} }
@ -175,7 +172,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 +203,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 +241,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 +256,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 +268,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 +280,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
));
} }
} }
} }
@ -308,14 +302,11 @@ impl Authorization {
/// rate limit logins only by ip. /// rate limit logins only by ip.
/// we want all origins and referers and user agents to count together /// we want all origins and referers and user agents to count together
pub async fn login_is_authorized( pub async fn login_is_authorized(app: &Web3ProxyApp, ip: IpAddr) -> Web3ProxyResult<Authorization> {
app: &Web3ProxyApp,
ip: IpAddr,
) -> Result<Authorization, FrontendErrorResponse> {
let authorization = match app.rate_limit_login(ip, ProxyMode::Best).await? { let authorization = match app.rate_limit_login(ip, ProxyMode::Best).await? {
RateLimitResult::Allowed(authorization, None) => authorization, RateLimitResult::Allowed(authorization, None) => authorization,
RateLimitResult::RateLimited(authorization, retry_at) => { RateLimitResult::RateLimited(authorization, retry_at) => {
return Err(FrontendErrorResponse::RateLimited(authorization, retry_at)); return Err(Web3ProxyError::RateLimited(authorization, retry_at));
} }
// TODO: don't panic. give the user an error // TODO: don't panic. give the user an error
x => unimplemented!("rate_limit_login shouldn't ever see these: {:?}", x), x => unimplemented!("rate_limit_login shouldn't ever see these: {:?}", x),
@ -330,7 +321,7 @@ pub async fn ip_is_authorized(
ip: IpAddr, ip: IpAddr,
origin: Option<Origin>, origin: Option<Origin>,
proxy_mode: ProxyMode, proxy_mode: ProxyMode,
) -> Result<(Authorization, Option<OwnedSemaphorePermit>), FrontendErrorResponse> { ) -> Web3ProxyResult<(Authorization, Option<OwnedSemaphorePermit>)> {
// TODO: i think we could write an `impl From` for this // TODO: i think we could write an `impl From` for this
// TODO: move this to an AuthorizedUser extrator // TODO: move this to an AuthorizedUser extrator
let (authorization, semaphore) = match app let (authorization, semaphore) = match app
@ -345,7 +336,7 @@ pub async fn ip_is_authorized(
RateLimitResult::Allowed(authorization, semaphore) => (authorization, semaphore), RateLimitResult::Allowed(authorization, semaphore) => (authorization, semaphore),
RateLimitResult::RateLimited(authorization, retry_at) => { RateLimitResult::RateLimited(authorization, retry_at) => {
// TODO: in the background, emit a stat (maybe simplest to use a channel?) // TODO: in the background, emit a stat (maybe simplest to use a channel?)
return Err(FrontendErrorResponse::RateLimited(authorization, retry_at)); return Err(Web3ProxyError::RateLimited(authorization, retry_at));
} }
// TODO: don't panic. give the user an error // TODO: don't panic. give the user an error
x => unimplemented!("rate_limit_by_ip shouldn't ever see these: {:?}", x), x => unimplemented!("rate_limit_by_ip shouldn't ever see these: {:?}", x),
@ -375,7 +366,7 @@ pub async fn ip_is_authorized(
.await?; .await?;
}; };
Ok::<_, anyhow::Error>(()) Ok::<_, Web3ProxyError>(())
} }
.map_err(|err| { .map_err(|err| {
warn!("background update of recent_users:ip failed: {}", err); warn!("background update of recent_users:ip failed: {}", err);
@ -389,7 +380,7 @@ pub async fn ip_is_authorized(
Ok((authorization, semaphore)) Ok((authorization, semaphore))
} }
/// like app.rate_limit_by_rpc_key but converts to a FrontendErrorResponse; /// like app.rate_limit_by_rpc_key but converts to a Web3ProxyError;
pub async fn key_is_authorized( pub async fn key_is_authorized(
app: &Arc<Web3ProxyApp>, app: &Arc<Web3ProxyApp>,
rpc_key: RpcSecretKey, rpc_key: RpcSecretKey,
@ -398,7 +389,7 @@ pub async fn key_is_authorized(
proxy_mode: ProxyMode, proxy_mode: ProxyMode,
referer: Option<Referer>, referer: Option<Referer>,
user_agent: Option<UserAgent>, user_agent: Option<UserAgent>,
) -> Result<(Authorization, Option<OwnedSemaphorePermit>), FrontendErrorResponse> { ) -> Web3ProxyResult<(Authorization, Option<OwnedSemaphorePermit>)> {
// check the rate limits. error if over the limit // check the rate limits. error if over the limit
// TODO: i think this should be in an "impl From" or "impl Into" // TODO: i think this should be in an "impl From" or "impl Into"
let (authorization, semaphore) = match app let (authorization, semaphore) = match app
@ -407,9 +398,9 @@ pub async fn key_is_authorized(
{ {
RateLimitResult::Allowed(authorization, semaphore) => (authorization, semaphore), RateLimitResult::Allowed(authorization, semaphore) => (authorization, semaphore),
RateLimitResult::RateLimited(authorization, retry_at) => { RateLimitResult::RateLimited(authorization, retry_at) => {
return Err(FrontendErrorResponse::RateLimited(authorization, retry_at)); return Err(Web3ProxyError::RateLimited(authorization, retry_at));
} }
RateLimitResult::UnknownKey => return Err(FrontendErrorResponse::UnknownKey), RateLimitResult::UnknownKey => return Err(Web3ProxyError::UnknownKey),
}; };
// TODO: DRY and maybe optimize the hashing // TODO: DRY and maybe optimize the hashing
@ -438,7 +429,7 @@ pub async fn key_is_authorized(
.await?; .await?;
} }
Ok::<_, anyhow::Error>(()) Ok::<_, Web3ProxyError>(())
} }
.map_err(|err| { .map_err(|err| {
warn!("background update of recent_users:id failed: {}", err); warn!("background update of recent_users:id failed: {}", err);
@ -454,7 +445,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
@ -482,12 +473,13 @@ impl Web3ProxyApp {
pub async fn registered_user_semaphore( pub async fn registered_user_semaphore(
&self, &self,
authorization_checks: &AuthorizationChecks, authorization_checks: &AuthorizationChecks,
) -> anyhow::Result<Option<OwnedSemaphorePermit>> { ) -> Web3ProxyResult<Option<OwnedSemaphorePermit>> {
if let Some(max_concurrent_requests) = authorization_checks.max_concurrent_requests { if let Some(max_concurrent_requests) = authorization_checks.max_concurrent_requests {
let user_id = authorization_checks let user_id = authorization_checks
.user_id .user_id
.try_into() .try_into()
.context("user ids should always be non-zero")?; .or(Err(Web3ProxyError::UserIdZero))
.web3_context("user ids should always be non-zero")?;
let semaphore = self let semaphore = self
.registered_user_semaphores .registered_user_semaphores
@ -517,7 +509,7 @@ impl Web3ProxyApp {
pub async fn bearer_is_authorized( pub async fn bearer_is_authorized(
&self, &self,
bearer: Bearer, bearer: Bearer,
) -> Result<(user::Model, OwnedSemaphorePermit), FrontendErrorResponse> { ) -> Web3ProxyResult<(user::Model, OwnedSemaphorePermit)> {
// get the user id for this bearer token // get the user id for this bearer token
let user_bearer_token = UserBearerToken::try_from(bearer)?; let user_bearer_token = UserBearerToken::try_from(bearer)?;
@ -535,7 +527,7 @@ impl Web3ProxyApp {
// get the attached address from the database for the given auth_token. // get the attached address from the database for the given auth_token.
let db_replica = self let db_replica = self
.db_replica() .db_replica()
.context("checking if bearer token is authorized")?; .web3_context("checking if bearer token is authorized")?;
let user_bearer_uuid: Uuid = user_bearer_token.into(); let user_bearer_uuid: Uuid = user_bearer_token.into();
@ -544,8 +536,8 @@ impl Web3ProxyApp {
.filter(login::Column::BearerToken.eq(user_bearer_uuid)) .filter(login::Column::BearerToken.eq(user_bearer_uuid))
.one(db_replica.conn()) .one(db_replica.conn())
.await .await
.context("fetching user from db by bearer token")? .web3_context("fetching user from db by bearer token")?
.context("unknown bearer token")?; .web3_context("unknown bearer token")?;
Ok((user, semaphore_permit)) Ok((user, semaphore_permit))
} }
@ -554,7 +546,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
@ -611,7 +603,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
// they do check origin because we can override rate limits for some origins // they do check origin because we can override rate limits for some origins
let authorization = Authorization::external( let authorization = Authorization::external(
@ -670,13 +662,15 @@ impl Web3ProxyApp {
&self, &self,
proxy_mode: ProxyMode, proxy_mode: ProxyMode,
rpc_secret_key: RpcSecretKey, rpc_secret_key: RpcSecretKey,
) -> anyhow::Result<AuthorizationChecks> { ) -> Web3ProxyResult<AuthorizationChecks> {
let authorization_checks: Result<_, Arc<anyhow::Error>> = self let authorization_checks: Result<_, Arc<Web3ProxyError>> = self
.rpc_secret_key_cache .rpc_secret_key_cache
.try_get_with(rpc_secret_key.into(), async move { .try_get_with(rpc_secret_key.into(), async move {
// trace!(?rpc_secret_key, "user cache miss"); // trace!(?rpc_secret_key, "user cache miss");
let db_replica = self.db_replica().context("Getting database connection")?; let db_replica = self
.db_replica()
.web3_context("Getting database connection")?;
// TODO: join the user table to this to return the User? we don't always need it // TODO: join the user table to this to return the User? we don't always need it
// TODO: join on secondary users // TODO: join on secondary users
@ -732,7 +726,11 @@ impl Web3ProxyApp {
if let Some(allowed_referers) = rpc_key_model.allowed_referers { if let Some(allowed_referers) = rpc_key_model.allowed_referers {
let x = allowed_referers let x = allowed_referers
.split(',') .split(',')
.map(|x| x.trim().parse::<Referer>()) .map(|x| {
x.trim()
.parse::<Referer>()
.or(Err(Web3ProxyError::InvalidReferer))
})
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
Some(x) Some(x)
@ -744,7 +742,11 @@ impl Web3ProxyApp {
if let Some(allowed_user_agents) = rpc_key_model.allowed_user_agents { if let Some(allowed_user_agents) = rpc_key_model.allowed_user_agents {
let x: Result<Vec<_>, _> = allowed_user_agents let x: Result<Vec<_>, _> = allowed_user_agents
.split(',') .split(',')
.map(|x| x.trim().parse::<UserAgent>()) .map(|x| {
x.trim()
.parse::<UserAgent>()
.or(Err(Web3ProxyError::InvalidUserAgent))
})
.collect(); .collect();
Some(x?) Some(x?)
@ -776,8 +778,7 @@ impl Web3ProxyApp {
}) })
.await; .await;
// TODO: what's the best way to handle this arc? try_unwrap will not work authorization_checks.map_err(Web3ProxyError::Arc)
authorization_checks.map_err(|err| anyhow::anyhow!(err))
} }
/// Authorized the ip/origin/referer/useragent and rate limit and concurrency /// Authorized the ip/origin/referer/useragent and rate limit and concurrency
@ -789,7 +790,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
@ -869,7 +870,7 @@ impl Authorization {
pub async fn check_again( pub async fn check_again(
&self, &self,
app: &Arc<Web3ProxyApp>, app: &Arc<Web3ProxyApp>,
) -> Result<(Arc<Self>, Option<OwnedSemaphorePermit>), FrontendErrorResponse> { ) -> Web3ProxyResult<(Arc<Self>, Option<OwnedSemaphorePermit>)> {
// TODO: we could probably do this without clones. but this is easy // TODO: we could probably do this without clones. but this is easy
let (a, s) = if let Some(rpc_secret_key) = self.checks.rpc_secret_key { let (a, s) = if let Some(rpc_secret_key) = self.checks.rpc_secret_key {
key_is_authorized( key_is_authorized(

View File

@ -2,51 +2,130 @@
use super::authorization::Authorization; use super::authorization::Authorization;
use crate::jsonrpc::JsonRpcForwardedResponse; use crate::jsonrpc::JsonRpcForwardedResponse;
use std::net::IpAddr;
use std::sync::Arc;
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, info, trace, warn};
use migration::sea_orm::DbErr; use migration::sea_orm::DbErr;
use redis_rate_limiter::redis::RedisError; use redis_rate_limiter::redis::RedisError;
use reqwest::header::ToStrError; use reqwest::header::ToStrError;
use tokio::{sync::AcquireError, task::JoinError, time::Instant}; use tokio::{sync::AcquireError, task::JoinError, time::Instant};
pub type Web3ProxyResult<T> = Result<T, Web3ProxyError>;
// TODO: take "IntoResponse" instead of Response? // TODO: take "IntoResponse" instead of Response?
pub type FrontendResult = Result<Response, FrontendErrorResponse>; pub type Web3ProxyResponse = Web3ProxyResult<Response>;
// TODO: // TODO:
#[derive(Debug, From)] #[derive(Debug, Display, Error, From)]
pub enum FrontendErrorResponse { pub enum Web3ProxyError {
AccessDenied, AccessDenied,
#[error(ignore)]
Anyhow(anyhow::Error), Anyhow(anyhow::Error),
Arc(Arc<Web3ProxyError>),
#[error(ignore)]
#[from(ignore)]
BadRequest(String), BadRequest(String),
SemaphoreAcquireError(AcquireError), BadRouting,
Database(DbErr), Database(DbErr),
#[display(fmt = "{:#?}, {:#?}", _0, _1)]
EipVerificationFailed(Box<Web3ProxyError>, Box<Web3ProxyError>),
EthersHttpClientError(ethers::prelude::HttpClientError),
EthersProviderError(ethers::prelude::ProviderError),
EthersWsClientError(ethers::prelude::WsClientError),
FlumeRecvError(flume::RecvError),
GasEstimateNotU256,
Headers(headers::Error), Headers(headers::Error),
HeaderToString(ToStrError), HeaderToString(ToStrError),
InfluxDb2RequestError(influxdb2::RequestError), InfluxDb2RequestError(influxdb2::RequestError),
#[display(fmt = "{} > {}", min, max)]
#[from(ignore)]
InvalidBlockBounds {
min: u64,
max: u64,
},
InvalidHeaderValue(InvalidHeaderValue), InvalidHeaderValue(InvalidHeaderValue),
InvalidEip,
InvalidInviteCode,
InvalidReferer,
InvalidSignatureLength,
InvalidUserAgent,
InvalidUserKey,
IpAddrParse(AddrParseError), IpAddrParse(AddrParseError),
#[error(ignore)]
#[from(ignore)]
IpNotAllowed(IpAddr),
JoinError(JoinError), JoinError(JoinError),
#[display(fmt = "{:?}", _0)]
#[error(ignore)]
JsonRpc(crate::jsonrpc::JsonRpcErrorData),
MsgPackEncode(rmp_serde::encode::Error), MsgPackEncode(rmp_serde::encode::Error),
NoBlockNumberOrHash,
NoBlocksKnown,
NoConsensusHeadBlock,
NoHandleReady,
NoServersSynced,
#[display(fmt = "{}/{}", num_known, min_head_rpcs)]
#[from(ignore)]
NotEnoughRpcs {
num_known: usize,
min_head_rpcs: usize,
},
NotFound, NotFound,
NotImplemented,
OriginRequired,
#[error(ignore)]
#[from(ignore)]
OriginNotAllowed(headers::Origin),
#[display(fmt = "{:?}", _0)]
#[error(ignore)]
ParseBytesError(Option<ethers::types::ParseBytesError>),
ParseMsgError(siwe::ParseError),
ParseAddressError,
#[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),
SemaphoreAcquireError(AcquireError),
SendAppStatError(flume::SendError<crate::stats::AppStat>),
SerdeJson(serde_json::Error),
/// 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), #[display(fmt = "{:?}", _0)]
#[error(ignore)]
Timeout(Option<tokio::time::error::Elapsed>),
UlidDecode(ulid::DecodeError), UlidDecode(ulid::DecodeError),
UnknownBlockNumber,
UnknownKey, UnknownKey,
UserAgentRequired,
#[error(ignore)]
UserAgentNotAllowed(headers::UserAgent),
UserIdZero,
VerificationError(siwe::VerificationError),
WatchRecvError(tokio::sync::watch::error::RecvError),
WatchSendError,
WebsocketOnly,
#[display(fmt = "{:?}, {}", _0, _1)]
#[error(ignore)]
WithContext(Option<Box<Web3ProxyError>>, String),
} }
impl FrontendErrorResponse { impl Web3ProxyError {
pub fn into_response_parts(self) -> (StatusCode, JsonRpcForwardedResponse) { pub fn into_response_parts(self) -> (StatusCode, JsonRpcForwardedResponse) {
match self { match self {
Self::AccessDenied => { Self::AccessDenied => {
@ -85,6 +164,17 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::BadRouting => {
error!("BadRouting");
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"bad routing",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::Database(err) => { Self::Database(err) => {
error!("database err={:?}", err); error!("database err={:?}", err);
( (
@ -96,6 +186,78 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::EipVerificationFailed(err_1, err_191) => {
info!(
"EipVerificationFailed err_1={:#?} err2={:#?}",
err_1, err_191
);
(
StatusCode::UNAUTHORIZED,
JsonRpcForwardedResponse::from_string(
format!(
"both the primary and eip191 verification failed: {:#?}; {:#?}",
err_1, err_191
),
Some(StatusCode::UNAUTHORIZED.as_u16().into()),
None,
),
)
}
Self::EthersHttpClientError(err) => {
warn!("EthersHttpClientError err={:?}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"ether http client error",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::EthersProviderError(err) => {
warn!("EthersProviderError err={:?}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"ether provider error",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::EthersWsClientError(err) => {
warn!("EthersWsClientError err={:?}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"ether ws client error",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::FlumeRecvError(err) => {
warn!("FlumeRecvError err={:#?}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"flume recv error!",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::GasEstimateNotU256 => {
warn!("GasEstimateNotU256");
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"gas estimate result is not an U256",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::Headers(err) => { Self::Headers(err) => {
warn!("HeadersError {:?}", err); warn!("HeadersError {:?}", err);
( (
@ -119,6 +281,20 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::InvalidBlockBounds { min, max } => {
debug!("InvalidBlockBounds min={} max={}", min, max);
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_string(
format!(
"Invalid blocks bounds requested. min ({}) > max ({})",
min, max
),
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::IpAddrParse(err) => { Self::IpAddrParse(err) => {
warn!("IpAddrParse err={:?}", err); warn!("IpAddrParse err={:?}", err);
( (
@ -130,6 +306,17 @@ impl FrontendErrorResponse {
), ),
) )
} }
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);
( (
@ -141,6 +328,72 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::InvalidEip => {
warn!("InvalidEip");
(
StatusCode::UNAUTHORIZED,
JsonRpcForwardedResponse::from_str(
"invalid message eip given",
Some(StatusCode::UNAUTHORIZED.as_u16().into()),
None,
),
)
}
Self::InvalidInviteCode => {
warn!("InvalidInviteCode");
(
StatusCode::UNAUTHORIZED,
JsonRpcForwardedResponse::from_str(
"invalid invite code",
Some(StatusCode::UNAUTHORIZED.as_u16().into()),
None,
),
)
}
Self::InvalidReferer => {
warn!("InvalidReferer");
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"invalid referer!",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::InvalidSignatureLength => {
warn!("InvalidSignatureLength");
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"invalid signature length",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::InvalidUserAgent => {
warn!("InvalidUserAgent");
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"invalid user agent!",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::InvalidUserKey => {
warn!("InvalidUserKey");
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"UserKey was not a ULID or UUID",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::JoinError(err) => { Self::JoinError(err) => {
let code = if err.is_cancelled() { let code = if err.is_cancelled() {
trace!("JoinError. likely shutting down. err={:?}", err); trace!("JoinError. likely shutting down. err={:?}", err);
@ -160,6 +413,17 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::JsonRpc(err) => {
debug!("JsonRpc err={:?}", err);
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"json rpc error!",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::MsgPackEncode(err) => { Self::MsgPackEncode(err) => {
debug!("MsgPackEncode Error: {}", err); debug!("MsgPackEncode Error: {}", err);
( (
@ -171,6 +435,75 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::NoBlockNumberOrHash => {
warn!("NoBlockNumberOrHash");
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"Blocks here must have a number or hash",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::NoBlocksKnown => {
error!("NoBlocksKnown");
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"no blocks known",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::NoConsensusHeadBlock => {
error!("NoConsensusHeadBlock");
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"no consensus head block",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::NoHandleReady => {
error!("NoHandleReady");
(
StatusCode::BAD_GATEWAY,
JsonRpcForwardedResponse::from_str(
"unable to retry for request handle",
Some(StatusCode::BAD_GATEWAY.as_u16().into()),
None,
),
)
}
Self::NoServersSynced => {
warn!("NoServersSynced");
(
StatusCode::BAD_GATEWAY,
JsonRpcForwardedResponse::from_str(
"no servers synced",
Some(StatusCode::BAD_GATEWAY.as_u16().into()),
None,
),
)
}
Self::NotEnoughRpcs {
num_known,
min_head_rpcs,
} => {
error!("NotEnoughRpcs {}/{}", num_known, min_head_rpcs);
(
StatusCode::BAD_GATEWAY,
JsonRpcForwardedResponse::from_string(
format!("not enough rpcs connected {}/{}", num_known, min_head_rpcs),
Some(StatusCode::BAD_GATEWAY.as_u16().into()),
None,
),
)
}
Self::NotFound => { Self::NotFound => {
// TODO: emit a stat? // TODO: emit a stat?
// TODO: instead of an error, show a normal html page for 404 // TODO: instead of an error, show a normal html page for 404
@ -183,6 +516,72 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::NotImplemented => {
trace!("NotImplemented");
(
StatusCode::NOT_IMPLEMENTED,
JsonRpcForwardedResponse::from_str(
"work in progress",
Some(StatusCode::NOT_IMPLEMENTED.as_u16().into()),
None,
),
)
}
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,
),
)
}
Self::ParseBytesError(err) => {
warn!("ParseBytesError err={:?}", err);
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"parse bytes error!",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::ParseMsgError(err) => {
warn!("ParseMsgError err={:?}", err);
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"parse message error!",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::ParseAddressError => {
warn!("ParseAddressError");
(
StatusCode::UNAUTHORIZED,
JsonRpcForwardedResponse::from_str(
"unable to parse address",
Some(StatusCode::BAD_REQUEST.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
@ -226,6 +625,33 @@ impl FrontendErrorResponse {
), ),
) )
} }
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::Arc(err) => match Arc::try_unwrap(err) {
Ok(err) => err,
Err(err) => Self::Anyhow(anyhow::anyhow!("{}", err)),
}
.into_response_parts(),
Self::SemaphoreAcquireError(err) => { Self::SemaphoreAcquireError(err) => {
warn!("semaphore acquire err={:?}", err); warn!("semaphore acquire err={:?}", err);
( (
@ -238,6 +664,28 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::SendAppStatError(err) => {
error!("SendAppStatError err={:?}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"error stat_sender sending response_stat",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::SerdeJson(err) => {
warn!("serde json err={:?}", err);
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"de/serialization error!",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::StatusCode(status_code, err_msg, err) => { Self::StatusCode(status_code, err_msg, err) => {
// different status codes should get different error levels. 500s should warn. 400s should stat // different status codes should get different error levels. 500s should warn. 400s should stat
let code = status_code.as_u16(); let code = status_code.as_u16();
@ -283,6 +731,17 @@ impl FrontendErrorResponse {
), ),
) )
} }
Self::UnknownBlockNumber => {
error!("UnknownBlockNumber");
(
StatusCode::BAD_GATEWAY,
JsonRpcForwardedResponse::from_str(
"no servers synced. unknown eth_blockNumber",
Some(StatusCode::BAD_GATEWAY.as_u16().into()),
None,
),
)
}
// TODO: stat? // TODO: stat?
Self::UnknownKey => ( Self::UnknownKey => (
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
@ -292,11 +751,114 @@ impl FrontendErrorResponse {
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,
),
)
}
Self::UserIdZero => {
warn!("UserIdZero");
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"user ids should always be non-zero",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::VerificationError(err) => {
warn!("VerificationError err={:?}", err);
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"verification error!",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::WatchRecvError(err) => {
error!("WatchRecvError err={:?}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"watch recv error!",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::WatchSendError => {
error!("WatchSendError");
(
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_str(
"watch send error!",
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
)
}
Self::WebsocketOnly => {
warn!("WebsocketOnly");
(
StatusCode::BAD_REQUEST,
JsonRpcForwardedResponse::from_str(
"redirect_public_url not set. only websockets work here",
Some(StatusCode::BAD_REQUEST.as_u16().into()),
None,
),
)
}
Self::WithContext(err, msg) => {
info!("in context: {}", msg);
match err {
Some(err) => err.into_response_parts(),
None => (
StatusCode::INTERNAL_SERVER_ERROR,
JsonRpcForwardedResponse::from_string(
msg,
Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16().into()),
None,
),
),
}
}
} }
} }
} }
impl IntoResponse for FrontendErrorResponse { impl From<ethers::types::ParseBytesError> for Web3ProxyError {
fn from(err: ethers::types::ParseBytesError) -> Self {
Self::ParseBytesError(Some(err))
}
}
impl From<tokio::time::error::Elapsed> for Web3ProxyError {
fn from(err: tokio::time::error::Elapsed) -> Self {
Self::Timeout(Some(err))
}
}
impl IntoResponse for Web3ProxyError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
// TODO: include the request id in these so that users can give us something that will point to logs // TODO: include the request id in these so that users can give us something that will point to logs
// TODO: status code is in the jsonrpc response and is also the first item in the tuple. DRY // TODO: status code is in the jsonrpc response and is also the first item in the tuple. DRY
@ -307,5 +869,24 @@ impl IntoResponse for FrontendErrorResponse {
} }
pub async fn handler_404() -> Response { pub async fn handler_404() -> Response {
FrontendErrorResponse::NotFound.into_response() Web3ProxyError::NotFound.into_response()
}
pub trait Web3ProxyErrorContext<T> {
fn web3_context<S: Into<String>>(self, msg: S) -> Result<T, Web3ProxyError>;
}
impl<T> Web3ProxyErrorContext<T> for Option<T> {
fn web3_context<S: Into<String>>(self, msg: S) -> Result<T, Web3ProxyError> {
self.ok_or(Web3ProxyError::WithContext(None, msg.into()))
}
}
impl<T, E> Web3ProxyErrorContext<T> for Result<T, E>
where
E: Into<Web3ProxyError>,
{
fn web3_context<S: Into<String>>(self, msg: S) -> Result<T, Web3ProxyError> {
self.map_err(|err| Web3ProxyError::WithContext(Some(Box::new(err.into())), msg.into()))
}
} }

View File

@ -1,7 +1,7 @@
//! Take a user's HTTP JSON-RPC requests and either respond from local data or proxy the request to a backend rpc server. //! Take a user's HTTP JSON-RPC requests and either respond from local data or proxy the request to a backend rpc server.
use super::authorization::{ip_is_authorized, key_is_authorized}; use super::authorization::{ip_is_authorized, key_is_authorized};
use super::errors::FrontendResult; use super::errors::Web3ProxyResponse;
use super::rpc_proxy_ws::ProxyMode; use super::rpc_proxy_ws::ProxyMode;
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum}; use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
use axum::extract::Path; use axum::extract::Path;
@ -22,7 +22,7 @@ pub async fn proxy_web3_rpc(
ip: InsecureClientIp, ip: InsecureClientIp,
origin: Option<TypedHeader<Origin>>, origin: Option<TypedHeader<Origin>>,
Json(payload): Json<JsonRpcRequestEnum>, Json(payload): Json<JsonRpcRequestEnum>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_proxy_web3_rpc(app, ip, origin, payload, ProxyMode::Best).await _proxy_web3_rpc(app, ip, origin, payload, ProxyMode::Best).await
} }
@ -32,7 +32,7 @@ pub async fn fastest_proxy_web3_rpc(
ip: InsecureClientIp, ip: InsecureClientIp,
origin: Option<TypedHeader<Origin>>, origin: Option<TypedHeader<Origin>>,
Json(payload): Json<JsonRpcRequestEnum>, Json(payload): Json<JsonRpcRequestEnum>,
) -> FrontendResult { ) -> Web3ProxyResponse {
// TODO: read the fastest number from params // TODO: read the fastest number from params
// TODO: check that the app allows this without authentication // TODO: check that the app allows this without authentication
_proxy_web3_rpc(app, ip, origin, payload, ProxyMode::Fastest(0)).await _proxy_web3_rpc(app, ip, origin, payload, ProxyMode::Fastest(0)).await
@ -44,7 +44,7 @@ pub async fn versus_proxy_web3_rpc(
ip: InsecureClientIp, ip: InsecureClientIp,
origin: Option<TypedHeader<Origin>>, origin: Option<TypedHeader<Origin>>,
Json(payload): Json<JsonRpcRequestEnum>, Json(payload): Json<JsonRpcRequestEnum>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_proxy_web3_rpc(app, ip, origin, payload, ProxyMode::Versus).await _proxy_web3_rpc(app, ip, origin, payload, ProxyMode::Versus).await
} }
@ -54,7 +54,7 @@ async fn _proxy_web3_rpc(
origin: Option<TypedHeader<Origin>>, origin: Option<TypedHeader<Origin>>,
payload: JsonRpcRequestEnum, payload: JsonRpcRequestEnum,
proxy_mode: ProxyMode, proxy_mode: ProxyMode,
) -> FrontendResult { ) -> Web3ProxyResponse {
// TODO: benchmark spawning this // TODO: benchmark spawning this
// TODO: do we care about keeping the TypedHeader wrapper? // TODO: do we care about keeping the TypedHeader wrapper?
let origin = origin.map(|x| x.0); let origin = origin.map(|x| x.0);
@ -115,7 +115,7 @@ pub async fn proxy_web3_rpc_with_key(
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
Path(rpc_key): Path<String>, Path(rpc_key): Path<String>,
Json(payload): Json<JsonRpcRequestEnum>, Json(payload): Json<JsonRpcRequestEnum>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_proxy_web3_rpc_with_key( _proxy_web3_rpc_with_key(
app, app,
ip, ip,
@ -138,7 +138,7 @@ pub async fn debug_proxy_web3_rpc_with_key(
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
Path(rpc_key): Path<String>, Path(rpc_key): Path<String>,
Json(payload): Json<JsonRpcRequestEnum>, Json(payload): Json<JsonRpcRequestEnum>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_proxy_web3_rpc_with_key( _proxy_web3_rpc_with_key(
app, app,
ip, ip,
@ -161,7 +161,7 @@ pub async fn fastest_proxy_web3_rpc_with_key(
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
Path(rpc_key): Path<String>, Path(rpc_key): Path<String>,
Json(payload): Json<JsonRpcRequestEnum>, Json(payload): Json<JsonRpcRequestEnum>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_proxy_web3_rpc_with_key( _proxy_web3_rpc_with_key(
app, app,
ip, ip,
@ -184,7 +184,7 @@ pub async fn versus_proxy_web3_rpc_with_key(
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
Path(rpc_key): Path<String>, Path(rpc_key): Path<String>,
Json(payload): Json<JsonRpcRequestEnum>, Json(payload): Json<JsonRpcRequestEnum>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_proxy_web3_rpc_with_key( _proxy_web3_rpc_with_key(
app, app,
ip, ip,
@ -208,7 +208,7 @@ async fn _proxy_web3_rpc_with_key(
rpc_key: String, rpc_key: String,
payload: JsonRpcRequestEnum, payload: JsonRpcRequestEnum,
proxy_mode: ProxyMode, proxy_mode: ProxyMode,
) -> FrontendResult { ) -> Web3ProxyResponse {
// TODO: DRY w/ proxy_web3_rpc // TODO: DRY w/ proxy_web3_rpc
// the request can take a while, so we spawn so that we can start serving another request // the request can take a while, so we spawn so that we can start serving another request
let rpc_key = rpc_key.parse()?; let rpc_key = rpc_key.parse()?;

View File

@ -3,10 +3,11 @@
//! WebSockets are the preferred method of receiving requests, but not all clients have good support. //! WebSockets are the preferred method of receiving requests, but not all clients have good support.
use super::authorization::{ip_is_authorized, key_is_authorized, Authorization, RequestMetadata}; use super::authorization::{ip_is_authorized, key_is_authorized, Authorization, RequestMetadata};
use super::errors::{FrontendErrorResponse, FrontendResult}; use super::errors::{Web3ProxyError, Web3ProxyResponse};
use crate::stats::RpcQueryStats; use crate::stats::RpcQueryStats;
use crate::{ use crate::{
app::Web3ProxyApp, app::Web3ProxyApp,
frontend::errors::Web3ProxyResult,
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest}, jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
}; };
use axum::headers::{Origin, Referer, UserAgent}; use axum::headers::{Origin, Referer, UserAgent};
@ -26,7 +27,7 @@ use futures::{
use handlebars::Handlebars; use handlebars::Handlebars;
use hashbrown::HashMap; use hashbrown::HashMap;
use http::StatusCode; use http::StatusCode;
use log::{error, info, trace, warn}; use log::{info, trace, warn};
use serde_json::json; use serde_json::json;
use serde_json::value::to_raw_value; use serde_json::value::to_raw_value;
use std::sync::Arc; use std::sync::Arc;
@ -59,7 +60,7 @@ pub async fn websocket_handler(
ip: InsecureClientIp, ip: InsecureClientIp,
origin: Option<TypedHeader<Origin>>, origin: Option<TypedHeader<Origin>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_websocket_handler(ProxyMode::Best, app, ip, origin, ws_upgrade).await _websocket_handler(ProxyMode::Best, app, ip, origin, ws_upgrade).await
} }
@ -71,7 +72,7 @@ pub async fn fastest_websocket_handler(
ip: InsecureClientIp, ip: InsecureClientIp,
origin: Option<TypedHeader<Origin>>, origin: Option<TypedHeader<Origin>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
// TODO: get the fastest number from the url params (default to 0/all) // TODO: get the fastest number from the url params (default to 0/all)
// TODO: config to disable this // TODO: config to disable this
_websocket_handler(ProxyMode::Fastest(0), app, ip, origin, ws_upgrade).await _websocket_handler(ProxyMode::Fastest(0), app, ip, origin, ws_upgrade).await
@ -85,7 +86,7 @@ pub async fn versus_websocket_handler(
ip: InsecureClientIp, ip: InsecureClientIp,
origin: Option<TypedHeader<Origin>>, origin: Option<TypedHeader<Origin>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
// TODO: config to disable this // TODO: config to disable this
_websocket_handler(ProxyMode::Versus, app, ip, origin, ws_upgrade).await _websocket_handler(ProxyMode::Versus, app, ip, origin, ws_upgrade).await
} }
@ -96,7 +97,7 @@ async fn _websocket_handler(
InsecureClientIp(ip): InsecureClientIp, InsecureClientIp(ip): InsecureClientIp,
origin: Option<TypedHeader<Origin>>, origin: Option<TypedHeader<Origin>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let origin = origin.map(|x| x.0); let origin = origin.map(|x| x.0);
let (authorization, _semaphore) = ip_is_authorized(&app, ip, origin, proxy_mode).await?; let (authorization, _semaphore) = ip_is_authorized(&app, ip, origin, proxy_mode).await?;
@ -112,11 +113,7 @@ async fn _websocket_handler(
// this is not a websocket. redirect to a friendly page // this is not a websocket. redirect to a friendly page
Ok(Redirect::permanent(redirect).into_response()) Ok(Redirect::permanent(redirect).into_response())
} else { } else {
// TODO: do not use an anyhow error. send the user a 400 Err(Web3ProxyError::WebsocketOnly)
Err(
anyhow::anyhow!("redirect_public_url not set. only websockets work here")
.into(),
)
} }
} }
} }
@ -134,7 +131,7 @@ pub async fn websocket_handler_with_key(
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_websocket_handler_with_key( _websocket_handler_with_key(
ProxyMode::Best, ProxyMode::Best,
app, app,
@ -157,7 +154,7 @@ pub async fn debug_websocket_handler_with_key(
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_websocket_handler_with_key( _websocket_handler_with_key(
ProxyMode::Debug, ProxyMode::Debug,
app, app,
@ -180,7 +177,7 @@ pub async fn fastest_websocket_handler_with_key(
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
// TODO: get the fastest number from the url params (default to 0/all) // TODO: get the fastest number from the url params (default to 0/all)
_websocket_handler_with_key( _websocket_handler_with_key(
ProxyMode::Fastest(0), ProxyMode::Fastest(0),
@ -204,7 +201,7 @@ pub async fn versus_websocket_handler_with_key(
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
_websocket_handler_with_key( _websocket_handler_with_key(
ProxyMode::Versus, ProxyMode::Versus,
app, app,
@ -228,7 +225,7 @@ async fn _websocket_handler_with_key(
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
user_agent: Option<TypedHeader<UserAgent>>, user_agent: Option<TypedHeader<UserAgent>>,
ws_upgrade: Option<WebSocketUpgrade>, ws_upgrade: Option<WebSocketUpgrade>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let rpc_key = rpc_key.parse()?; let rpc_key = rpc_key.parse()?;
let (authorization, _semaphore) = key_is_authorized( let (authorization, _semaphore) = key_is_authorized(
@ -260,7 +257,7 @@ async fn _websocket_handler_with_key(
&app.config.redirect_rpc_key_url, &app.config.redirect_rpc_key_url,
authorization.checks.rpc_secret_key_id, authorization.checks.rpc_secret_key_id,
) { ) {
(None, None, _) => Err(FrontendErrorResponse::StatusCode( (None, None, _) => Err(Web3ProxyError::StatusCode(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"this page is for rpcs".to_string(), "this page is for rpcs".to_string(),
None, None,
@ -273,7 +270,7 @@ async fn _websocket_handler_with_key(
if authorization.checks.rpc_secret_key_id.is_none() { if authorization.checks.rpc_secret_key_id.is_none() {
// i don't think this is possible // i don't think this is possible
Err(FrontendErrorResponse::StatusCode( Err(Web3ProxyError::StatusCode(
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
"AUTHORIZATION header required".to_string(), "AUTHORIZATION header required".to_string(),
None, None,
@ -291,7 +288,7 @@ async fn _websocket_handler_with_key(
} }
} }
// any other combinations get a simple error // any other combinations get a simple error
_ => Err(FrontendErrorResponse::StatusCode( _ => Err(Web3ProxyError::StatusCode(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"this page is for rpcs".to_string(), "this page is for rpcs".to_string(),
None, None,
@ -341,7 +338,7 @@ async fn handle_socket_payload(
Ok(json_request) => { Ok(json_request) => {
let id = json_request.id.clone(); let id = json_request.id.clone();
let response: anyhow::Result<JsonRpcForwardedResponseEnum> = match &json_request.method let response: Web3ProxyResult<JsonRpcForwardedResponseEnum> = match &json_request.method
[..] [..]
{ {
"eth_subscribe" => { "eth_subscribe" => {
@ -378,7 +375,7 @@ async fn handle_socket_payload(
// TODO: move this logic into the app? // TODO: move this logic into the app?
let request_bytes = json_request.num_bytes(); let request_bytes = json_request.num_bytes();
let request_metadata = Arc::new(RequestMetadata::new(request_bytes).unwrap()); let request_metadata = Arc::new(RequestMetadata::new(request_bytes));
let subscription_id = json_request.params.unwrap().to_string(); let subscription_id = json_request.params.unwrap().to_string();
@ -417,16 +414,7 @@ async fn handle_socket_payload(
_ => app _ => app
.proxy_web3_rpc(authorization.clone(), json_request.into()) .proxy_web3_rpc(authorization.clone(), json_request.into())
.await .await
.map_or_else( .map(|(response, _)| response),
|err| match err {
FrontendErrorResponse::Anyhow(err) => Err(err),
_ => {
error!("handle this better! {:?}", err);
Err(anyhow::anyhow!("unexpected error! {:?}", err))
}
},
|(response, _)| Ok(response),
),
}; };
(id, response) (id, response)
@ -442,9 +430,8 @@ async fn handle_socket_payload(
let response_str = match response { let response_str = match response {
Ok(x) => serde_json::to_string(&x).expect("to_string should always work here"), Ok(x) => serde_json::to_string(&x).expect("to_string should always work here"),
Err(err) => { Err(err) => {
// we have an anyhow error. turn it into a response let (_, mut response) = err.into_response_parts();
let response = JsonRpcForwardedResponse::from_anyhow_error(err, None, Some(id)); response.id = id;
serde_json::to_string(&response).expect("to_string should always work here") serde_json::to_string(&response).expect("to_string should always work here")
} }
}; };

View File

@ -1,6 +1,6 @@
//! Handle registration, logins, and managing account data. //! Handle registration, logins, and managing account data.
use super::authorization::{login_is_authorized, RpcSecretKey}; use super::authorization::{login_is_authorized, RpcSecretKey};
use super::errors::FrontendResult; use super::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResponse};
use crate::app::Web3ProxyApp; use crate::app::Web3ProxyApp;
use crate::http_params::{ use crate::http_params::{
get_chain_id_from_params, get_page_from_params, get_query_start_from_params, get_chain_id_from_params, get_page_from_params, get_query_start_from_params,
@ -9,7 +9,6 @@ use crate::stats::influxdb_queries::query_user_stats;
use crate::stats::StatType; use crate::stats::StatType;
use crate::user_token::UserBearerToken; use crate::user_token::UserBearerToken;
use crate::{PostLogin, PostLoginQuery}; use crate::{PostLogin, PostLoginQuery};
use anyhow::Context;
use axum::headers::{Header, Origin, Referer, UserAgent}; use axum::headers::{Header, Origin, Referer, UserAgent};
use axum::{ use axum::{
extract::{Path, Query}, extract::{Path, Query},
@ -65,7 +64,7 @@ pub async fn user_login_get(
InsecureClientIp(ip): InsecureClientIp, InsecureClientIp(ip): InsecureClientIp,
// TODO: what does axum's error handling look like if the path fails to parse? // TODO: what does axum's error handling look like if the path fails to parse?
Path(mut params): Path<HashMap<String, String>>, Path(mut params): Path<HashMap<String, String>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
login_is_authorized(&app, ip).await?; login_is_authorized(&app, ip).await?;
// create a message and save it in redis // create a message and save it in redis
@ -81,11 +80,9 @@ pub async fn user_login_get(
// TODO: allow ENS names here? // TODO: allow ENS names here?
let user_address: Address = params let user_address: Address = params
.remove("user_address") .remove("user_address")
// TODO: map_err so this becomes a 500. routing must be bad .ok_or(Web3ProxyError::BadRouting)?
.context("impossible")?
.parse() .parse()
// TODO: map_err so this becomes a 401 .or(Err(Web3ProxyError::ParseAddressError))?;
.context("unable to parse address")?;
let login_domain = app let login_domain = app
.config .config
@ -113,7 +110,7 @@ pub async fn user_login_get(
resources: vec![], resources: vec![],
}; };
let db_conn = app.db_conn().context("login requires a database")?; let db_conn = app.db_conn().web3_context("login requires a database")?;
// massage types to fit in the database. sea-orm does not make this very elegant // massage types to fit in the database. sea-orm does not make this very elegant
let uuid = Uuid::from_u128(nonce.into()); let uuid = Uuid::from_u128(nonce.into());
@ -135,7 +132,7 @@ pub async fn user_login_get(
user_pending_login user_pending_login
.save(&db_conn) .save(&db_conn)
.await .await
.context("saving user's pending_login")?; .web3_context("saving user's pending_login")?;
// there are multiple ways to sign messages and not all wallets support them // there are multiple ways to sign messages and not all wallets support them
// TODO: default message eip from config? // TODO: default message eip from config?
@ -148,8 +145,7 @@ pub async fn user_login_get(
"eip191_hash" => Bytes::from(&message.eip191_hash().unwrap()).to_string(), "eip191_hash" => Bytes::from(&message.eip191_hash().unwrap()).to_string(),
"eip4361" => message.to_string(), "eip4361" => message.to_string(),
_ => { _ => {
// TODO: custom error that is handled a 401 return Err(Web3ProxyError::InvalidEip);
return Err(anyhow::anyhow!("invalid message eip given").into());
} }
}; };
@ -165,13 +161,13 @@ pub async fn user_login_post(
InsecureClientIp(ip): InsecureClientIp, InsecureClientIp(ip): InsecureClientIp,
Query(query): Query<PostLoginQuery>, Query(query): Query<PostLoginQuery>,
Json(payload): Json<PostLogin>, Json(payload): Json<PostLogin>,
) -> FrontendResult { ) -> Web3ProxyResponse {
login_is_authorized(&app, ip).await?; login_is_authorized(&app, ip).await?;
// TODO: this seems too verbose. how can we simply convert a String into a [u8; 65] // TODO: this seems too verbose. how can we simply convert a String into a [u8; 65]
let their_sig_bytes = Bytes::from_str(&payload.sig).context("parsing sig")?; let their_sig_bytes = Bytes::from_str(&payload.sig).web3_context("parsing sig")?;
if their_sig_bytes.len() != 65 { if their_sig_bytes.len() != 65 {
return Err(anyhow::anyhow!("checking signature length").into()); return Err(Web3ProxyError::InvalidSignatureLength);
} }
let mut their_sig: [u8; 65] = [0; 65]; let mut their_sig: [u8; 65] = [0; 65];
for x in 0..65 { for x in 0..65 {
@ -181,17 +177,18 @@ pub async fn user_login_post(
// we can't trust that they didn't tamper with the message in some way. like some clients return it hex encoded // we can't trust that they didn't tamper with the message in some way. like some clients return it hex encoded
// TODO: checking 0x seems fragile, but I think it will be fine. siwe message text shouldn't ever start with 0x // TODO: checking 0x seems fragile, but I think it will be fine. siwe message text shouldn't ever start with 0x
let their_msg: Message = if payload.msg.starts_with("0x") { let their_msg: Message = if payload.msg.starts_with("0x") {
let their_msg_bytes = Bytes::from_str(&payload.msg).context("parsing payload message")?; let their_msg_bytes =
Bytes::from_str(&payload.msg).web3_context("parsing payload message")?;
// TODO: lossy or no? // TODO: lossy or no?
String::from_utf8_lossy(their_msg_bytes.as_ref()) String::from_utf8_lossy(their_msg_bytes.as_ref())
.parse::<siwe::Message>() .parse::<siwe::Message>()
.context("parsing hex string message")? .web3_context("parsing hex string message")?
} else { } else {
payload payload
.msg .msg
.parse::<siwe::Message>() .parse::<siwe::Message>()
.context("parsing string message")? .web3_context("parsing string message")?
}; };
// the only part of the message we will trust is their nonce // the only part of the message we will trust is their nonce
@ -199,7 +196,9 @@ pub async fn user_login_post(
let login_nonce = UserBearerToken::from_str(&their_msg.nonce)?; let login_nonce = UserBearerToken::from_str(&their_msg.nonce)?;
// fetch the message we gave them from our database // fetch the message we gave them from our database
let db_replica = app.db_replica().context("Getting database connection")?; let db_replica = app
.db_replica()
.web3_context("Getting database connection")?;
// massage type for the db // massage type for the db
let login_nonce_uuid: Uuid = login_nonce.clone().into(); let login_nonce_uuid: Uuid = login_nonce.clone().into();
@ -208,13 +207,13 @@ pub async fn user_login_post(
.filter(pending_login::Column::Nonce.eq(login_nonce_uuid)) .filter(pending_login::Column::Nonce.eq(login_nonce_uuid))
.one(db_replica.conn()) .one(db_replica.conn())
.await .await
.context("database error while finding pending_login")? .web3_context("database error while finding pending_login")?
.context("login nonce not found")?; .web3_context("login nonce not found")?;
let our_msg: siwe::Message = user_pending_login let our_msg: siwe::Message = user_pending_login
.message .message
.parse() .parse()
.context("parsing siwe message")?; .web3_context("parsing siwe message")?;
// default options are fine. the message includes timestamp and domain and nonce // default options are fine. the message includes timestamp and domain and nonce
let verify_config = VerificationOpts::default(); let verify_config = VerificationOpts::default();
@ -223,16 +222,16 @@ pub async fn user_login_post(
if let Err(err_1) = our_msg if let Err(err_1) = our_msg
.verify(&their_sig, &verify_config) .verify(&their_sig, &verify_config)
.await .await
.context("verifying signature against our local message") .web3_context("verifying signature against our local message")
{ {
// verification method 1 failed. try eip191 // verification method 1 failed. try eip191
if let Err(err_191) = our_msg if let Err(err_191) = our_msg
.verify_eip191(&their_sig) .verify_eip191(&their_sig)
.context("verifying eip191 signature against our local message") .web3_context("verifying eip191 signature against our local message")
{ {
let db_conn = app let db_conn = app
.db_conn() .db_conn()
.context("deleting expired pending logins requires a db")?; .web3_context("deleting expired pending logins requires a db")?;
// delete ALL expired rows. // delete ALL expired rows.
let now = Utc::now(); let now = Utc::now();
@ -244,12 +243,10 @@ pub async fn user_login_post(
// TODO: emit a stat? if this is high something weird might be happening // TODO: emit a stat? if this is high something weird might be happening
debug!("cleared expired pending_logins: {:?}", delete_result); debug!("cleared expired pending_logins: {:?}", delete_result);
return Err(anyhow::anyhow!( return Err(Web3ProxyError::EipVerificationFailed(
"both the primary and eip191 verification failed: {:#?}; {:#?}", Box::new(err_1),
err_1, Box::new(err_191),
err_191 ));
)
.into());
} }
} }
@ -260,7 +257,7 @@ pub async fn user_login_post(
.await .await
.unwrap(); .unwrap();
let db_conn = app.db_conn().context("login requires a db")?; let db_conn = app.db_conn().web3_context("login requires a db")?;
let (u, uks, status_code) = match u { let (u, uks, status_code) = match u {
None => { None => {
@ -270,7 +267,7 @@ pub async fn user_login_post(
// TODO: more advanced invite codes that set different request/minute and concurrency limits // TODO: more advanced invite codes that set different request/minute and concurrency limits
if let Some(invite_code) = &app.config.invite_code { if let Some(invite_code) = &app.config.invite_code {
if query.invite_code.as_ref() != Some(invite_code) { if query.invite_code.as_ref() != Some(invite_code) {
return Err(anyhow::anyhow!("checking invite_code").into()); return Err(Web3ProxyError::InvalidInviteCode);
} }
} }
@ -300,7 +297,7 @@ pub async fn user_login_post(
let uk = uk let uk = uk
.insert(&txn) .insert(&txn)
.await .await
.context("Failed saving new user key")?; .web3_context("Failed saving new user key")?;
let uks = vec![uk]; let uks = vec![uk];
@ -315,7 +312,7 @@ pub async fn user_login_post(
.filter(rpc_key::Column::UserId.eq(u.id)) .filter(rpc_key::Column::UserId.eq(u.id))
.all(db_replica.conn()) .all(db_replica.conn())
.await .await
.context("failed loading user's key")?; .web3_context("failed loading user's key")?;
(u, uks, StatusCode::OK) (u, uks, StatusCode::OK)
} }
@ -355,7 +352,7 @@ pub async fn user_login_post(
user_login user_login
.save(&db_conn) .save(&db_conn)
.await .await
.context("saving user login")?; .web3_context("saving user login")?;
if let Err(err) = user_pending_login if let Err(err) = user_pending_login
.into_active_model() .into_active_model()
@ -373,10 +370,12 @@ pub async fn user_login_post(
pub async fn user_logout_post( pub async fn user_logout_post(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let user_bearer = UserBearerToken::try_from(bearer)?; let user_bearer = UserBearerToken::try_from(bearer)?;
let db_conn = app.db_conn().context("database needed for user logout")?; let db_conn = app
.db_conn()
.web3_context("database needed for user logout")?;
if let Err(err) = login::Entity::delete_many() if let Err(err) = login::Entity::delete_many()
.filter(login::Column::BearerToken.eq(user_bearer.uuid())) .filter(login::Column::BearerToken.eq(user_bearer.uuid()))
@ -417,7 +416,7 @@ pub async fn user_logout_post(
pub async fn user_get( pub async fn user_get(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let (user, _semaphore) = app.bearer_is_authorized(bearer_token).await?; let (user, _semaphore) = app.bearer_is_authorized(bearer_token).await?;
Ok(Json(user).into_response()) Ok(Json(user).into_response())
@ -435,7 +434,7 @@ pub async fn user_post(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
Json(payload): Json<UserPost>, Json(payload): Json<UserPost>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let (user, _semaphore) = app.bearer_is_authorized(bearer_token).await?; let (user, _semaphore) = app.bearer_is_authorized(bearer_token).await?;
let mut user: user::ActiveModel = user.into(); let mut user: user::ActiveModel = user.into();
@ -456,7 +455,7 @@ pub async fn user_post(
// TODO: what else can we update here? password hash? subscription to newsletter? // TODO: what else can we update here? password hash? subscription to newsletter?
let user = if user.is_changed() { let user = if user.is_changed() {
let db_conn = app.db_conn().context("Getting database connection")?; let db_conn = app.db_conn().web3_context("Getting database connection")?;
user.save(&db_conn).await? user.save(&db_conn).await?
} else { } else {
@ -464,7 +463,7 @@ pub async fn user_post(
user user
}; };
let user: user::Model = user.try_into().context("Returning updated user")?; let user: user::Model = user.try_into().web3_context("Returning updated user")?;
Ok(Json(user).into_response()) Ok(Json(user).into_response())
} }
@ -480,8 +479,8 @@ pub async fn user_post(
pub async fn user_balance_get( pub async fn user_balance_get(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?; let (_user, _semaphore) = app.bearer_is_authorized(bearer).await?;
todo!("user_balance_get"); todo!("user_balance_get");
} }
@ -495,8 +494,8 @@ pub async fn user_balance_get(
pub async fn user_balance_post( pub async fn user_balance_post(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?; let (_user, _semaphore) = app.bearer_is_authorized(bearer).await?;
todo!("user_balance_post"); todo!("user_balance_post");
} }
@ -506,18 +505,18 @@ pub async fn user_balance_post(
pub async fn rpc_keys_get( pub async fn rpc_keys_get(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?; let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
let db_replica = app let db_replica = app
.db_replica() .db_replica()
.context("db_replica is required to fetch a user's keys")?; .web3_context("db_replica is required to fetch a user's keys")?;
let uks = rpc_key::Entity::find() let uks = rpc_key::Entity::find()
.filter(rpc_key::Column::UserId.eq(user.id)) .filter(rpc_key::Column::UserId.eq(user.id))
.all(db_replica.conn()) .all(db_replica.conn())
.await .await
.context("failed loading user's key")?; .web3_context("failed loading user's key")?;
let response_json = json!({ let response_json = json!({
"user_id": user.id, "user_id": user.id,
@ -535,11 +534,11 @@ pub async fn rpc_keys_get(
pub async fn rpc_keys_delete( pub async fn rpc_keys_delete(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?; let (_user, _semaphore) = app.bearer_is_authorized(bearer).await?;
// TODO: think about how cascading deletes and billing should work // TODO: think about how cascading deletes and billing should work
Err(anyhow::anyhow!("work in progress").into()) Err(Web3ProxyError::NotImplemented)
} }
/// the JSON input to the `rpc_keys_management` handler. /// the JSON input to the `rpc_keys_management` handler.
@ -567,12 +566,14 @@ pub async fn rpc_keys_management(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
Json(payload): Json<UserKeyManagement>, Json(payload): Json<UserKeyManagement>,
) -> FrontendResult { ) -> Web3ProxyResponse {
// TODO: is there a way we can know if this is a PUT or POST? right now we can modify or create keys with either. though that probably doesn't matter // TODO: is there a way we can know if this is a PUT or POST? right now we can modify or create keys with either. though that probably doesn't matter
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?; let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
let db_replica = app.db_replica().context("getting db for user's keys")?; let db_replica = app
.db_replica()
.web3_context("getting db for user's keys")?;
let mut uk = if let Some(existing_key_id) = payload.key_id { let mut uk = if let Some(existing_key_id) = payload.key_id {
// get the key and make sure it belongs to the user // get the key and make sure it belongs to the user
@ -581,8 +582,8 @@ pub async fn rpc_keys_management(
.filter(rpc_key::Column::Id.eq(existing_key_id)) .filter(rpc_key::Column::Id.eq(existing_key_id))
.one(db_replica.conn()) .one(db_replica.conn())
.await .await
.context("failed loading user's key")? .web3_context("failed loading user's key")?
.context("key does not exist or is not controlled by this bearer token")? .web3_context("key does not exist or is not controlled by this bearer token")?
.into_active_model() .into_active_model()
} else { } else {
// make a new key // make a new key
@ -591,7 +592,7 @@ pub async fn rpc_keys_management(
let log_level = payload let log_level = payload
.log_level .log_level
.context("log level must be 'none', 'detailed', or 'aggregated'")?; .web3_context("log level must be 'none', 'detailed', or 'aggregated'")?;
rpc_key::ActiveModel { rpc_key::ActiveModel {
user_id: sea_orm::Set(user.id), user_id: sea_orm::Set(user.id),
@ -720,9 +721,11 @@ pub async fn rpc_keys_management(
} }
let uk = if uk.is_changed() { let uk = if uk.is_changed() {
let db_conn = app.db_conn().context("login requires a db")?; let db_conn = app.db_conn().web3_context("login requires a db")?;
uk.save(&db_conn).await.context("Failed saving user key")? uk.save(&db_conn)
.await
.web3_context("Failed saving user key")?
} else { } else {
uk uk
}; };
@ -738,7 +741,7 @@ pub async fn user_revert_logs_get(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
Query(params): Query<HashMap<String, String>>, Query(params): Query<HashMap<String, String>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?; let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
let chain_id = get_chain_id_from_params(app.as_ref(), &params)?; let chain_id = get_chain_id_from_params(app.as_ref(), &params)?;
@ -757,13 +760,13 @@ pub async fn user_revert_logs_get(
let db_replica = app let db_replica = app
.db_replica() .db_replica()
.context("getting replica db for user's revert logs")?; .web3_context("getting replica db for user's revert logs")?;
let uks = rpc_key::Entity::find() let uks = rpc_key::Entity::find()
.filter(rpc_key::Column::UserId.eq(user.id)) .filter(rpc_key::Column::UserId.eq(user.id))
.all(db_replica.conn()) .all(db_replica.conn())
.await .await
.context("failed loading user's key")?; .web3_context("failed loading user's key")?;
// TODO: only select the ids // TODO: only select the ids
let uks: Vec<_> = uks.into_iter().map(|x| x.id).collect(); let uks: Vec<_> = uks.into_iter().map(|x| x.id).collect();
@ -808,7 +811,7 @@ pub async fn user_stats_aggregated_get(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
bearer: Option<TypedHeader<Authorization<Bearer>>>, bearer: Option<TypedHeader<Authorization<Bearer>>>,
Query(params): Query<HashMap<String, String>>, Query(params): Query<HashMap<String, String>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let response = query_user_stats(&app, bearer, &params, StatType::Aggregated).await?; let response = query_user_stats(&app, bearer, &params, StatType::Aggregated).await?;
Ok(response) Ok(response)
@ -828,7 +831,7 @@ pub async fn user_stats_detailed_get(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
bearer: Option<TypedHeader<Authorization<Bearer>>>, bearer: Option<TypedHeader<Authorization<Bearer>>>,
Query(params): Query<HashMap<String, String>>, Query(params): Query<HashMap<String, String>>,
) -> FrontendResult { ) -> Web3ProxyResponse {
let response = query_user_stats(&app, bearer, &params, StatType::Detailed).await?; let response = query_user_stats(&app, bearer, &params, StatType::Detailed).await?;
Ok(response) Ok(response)

View File

@ -1,5 +1,5 @@
use crate::app::DatabaseReplica; use crate::app::DatabaseReplica;
use crate::frontend::errors::FrontendErrorResponse; use crate::frontend::errors::{Web3ProxyError, Web3ProxyResult};
use crate::{app::Web3ProxyApp, user_token::UserBearerToken}; use crate::{app::Web3ProxyApp, user_token::UserBearerToken};
use anyhow::Context; use anyhow::Context;
use axum::{ use axum::{
@ -24,7 +24,7 @@ pub async fn get_user_id_from_params(
// this is a long type. should we strip it down? // this is a long type. should we strip it down?
bearer: Option<TypedHeader<Authorization<Bearer>>>, bearer: Option<TypedHeader<Authorization<Bearer>>>,
params: &HashMap<String, String>, params: &HashMap<String, String>,
) -> Result<u64, FrontendErrorResponse> { ) -> Web3ProxyResult<u64> {
match (bearer, params.get("user_id")) { match (bearer, params.get("user_id")) {
(Some(TypedHeader(Authorization(bearer))), Some(user_id)) => { (Some(TypedHeader(Authorization(bearer))), Some(user_id)) => {
// check for the bearer cache key // check for the bearer cache key
@ -45,7 +45,7 @@ pub async fn get_user_id_from_params(
.one(db_replica.conn()) .one(db_replica.conn())
.await .await
.context("database error while querying for user")? .context("database error while querying for user")?
.ok_or(FrontendErrorResponse::AccessDenied)?; .ok_or(Web3ProxyError::AccessDenied)?;
// if expired, delete ALL expired logins // if expired, delete ALL expired logins
let now = Utc::now(); let now = Utc::now();
@ -60,7 +60,7 @@ pub async fn get_user_id_from_params(
// TODO: emit a stat? if this is high something weird might be happening // TODO: emit a stat? if this is high something weird might be happening
debug!("cleared expired logins: {:?}", delete_result); debug!("cleared expired logins: {:?}", delete_result);
return Err(FrontendErrorResponse::AccessDenied); return Err(Web3ProxyError::AccessDenied);
} }
save_to_redis = true; save_to_redis = true;
@ -76,7 +76,7 @@ pub async fn get_user_id_from_params(
let user_id: u64 = user_id.parse().context("Parsing user_id param")?; let user_id: u64 = user_id.parse().context("Parsing user_id param")?;
if bearer_user_id != user_id { if bearer_user_id != user_id {
return Err(FrontendErrorResponse::AccessDenied); return Err(Web3ProxyError::AccessDenied);
} }
if save_to_redis { if save_to_redis {
@ -103,7 +103,7 @@ pub async fn get_user_id_from_params(
// TODO: proper error code from a useful error code // TODO: proper error code from a useful error code
// TODO: maybe instead of this sharp edged warn, we have a config value? // TODO: maybe instead of this sharp edged warn, we have a config value?
// TODO: check config for if we should deny or allow this // TODO: check config for if we should deny or allow this
Err(FrontendErrorResponse::AccessDenied) Err(Web3ProxyError::AccessDenied)
// // TODO: make this a flag // // TODO: make this a flag
// warn!("allowing without auth during development!"); // warn!("allowing without auth during development!");
// Ok(x.parse()?) // Ok(x.parse()?)
@ -215,7 +215,7 @@ pub fn get_query_stop_from_params(
pub fn get_query_window_seconds_from_params( pub fn get_query_window_seconds_from_params(
params: &HashMap<String, String>, params: &HashMap<String, String>,
) -> Result<u64, FrontendErrorResponse> { ) -> Web3ProxyResult<u64> {
params.get("query_window_seconds").map_or_else( params.get("query_window_seconds").map_or_else(
|| { || {
// no page in params. set default // no page in params. set default
@ -225,15 +225,13 @@ pub fn get_query_window_seconds_from_params(
// parse the given timestamp // parse the given timestamp
query_window_seconds.parse::<u64>().map_err(|err| { query_window_seconds.parse::<u64>().map_err(|err| {
trace!("Unable to parse rpc_key_id: {:#?}", err); trace!("Unable to parse rpc_key_id: {:#?}", err);
FrontendErrorResponse::BadRequest("Unable to parse rpc_key_id".to_string()) Web3ProxyError::BadRequest("Unable to parse rpc_key_id".to_string())
}) })
}, },
) )
} }
pub fn get_stats_column_from_params( pub fn get_stats_column_from_params(params: &HashMap<String, String>) -> Web3ProxyResult<&str> {
params: &HashMap<String, String>,
) -> Result<&str, FrontendErrorResponse> {
params.get("query_stats_column").map_or_else( params.get("query_stats_column").map_or_else(
|| Ok("frontend_requests"), || Ok("frontend_requests"),
|query_stats_column: &String| { |query_stats_column: &String| {
@ -247,7 +245,7 @@ pub fn get_stats_column_from_params(
| "sum_request_bytes" | "sum_request_bytes"
| "sum_response_bytes" | "sum_response_bytes"
| "sum_response_millis" => Ok(query_stats_column), | "sum_response_millis" => Ok(query_stats_column),
_ => Err(FrontendErrorResponse::BadRequest( _ => Err(Web3ProxyError::BadRequest(
"Unable to parse query_stats_column. It must be one of: \ "Unable to parse query_stats_column. It must be one of: \
frontend_requests, \ frontend_requests, \
backend_requests, \ backend_requests, \

View File

@ -1,3 +1,4 @@
use crate::frontend::errors::Web3ProxyResult;
use derive_more::From; use derive_more::From;
use ethers::prelude::{HttpClientError, ProviderError, WsClientError}; use ethers::prelude::{HttpClientError, ProviderError, WsClientError};
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor}; use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
@ -240,7 +241,7 @@ impl JsonRpcForwardedResponse {
} }
} }
pub fn from_ethers_error(e: ProviderError, id: Box<RawValue>) -> anyhow::Result<Self> { pub fn from_ethers_error(e: ProviderError, id: Box<RawValue>) -> Web3ProxyResult<Self> {
// TODO: move turning ClientError into json to a helper function? // TODO: move turning ClientError into json to a helper function?
let code; let code;
let message: String; let message: String;
@ -280,7 +281,7 @@ impl JsonRpcForwardedResponse {
} }
} }
} else { } else {
return Err(anyhow::anyhow!("unexpected ethers error!")); return Err(anyhow::anyhow!("unexpected ethers error!").into());
} }
} }
} }
@ -302,7 +303,7 @@ impl JsonRpcForwardedResponse {
pub fn try_from_response_result( pub fn try_from_response_result(
result: Result<Box<RawValue>, ProviderError>, result: Result<Box<RawValue>, ProviderError>,
id: Box<RawValue>, id: Box<RawValue>,
) -> anyhow::Result<Self> { ) -> Web3ProxyResult<Self> {
match result { match result {
Ok(response) => Ok(Self::from_response(response, id)), Ok(response) => Ok(Self::from_response(response, id)),
Err(e) => Self::from_ethers_error(e, id), Err(e) => Self::from_ethers_error(e, id),

View File

@ -4,8 +4,8 @@ use super::many::Web3Rpcs;
use super::one::Web3Rpc; use super::one::Web3Rpc;
use super::transactions::TxStatus; use super::transactions::TxStatus;
use crate::frontend::authorization::Authorization; use crate::frontend::authorization::Authorization;
use crate::frontend::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResult};
use crate::{config::BlockAndRpc, jsonrpc::JsonRpcRequest}; use crate::{config::BlockAndRpc, jsonrpc::JsonRpcRequest};
use anyhow::{anyhow, Context};
use derive_more::From; use derive_more::From;
use ethers::prelude::{Block, TxHash, H256, U64}; use ethers::prelude::{Block, TxHash, H256, U64};
use log::{debug, trace, warn, Level}; use log::{debug, trace, warn, Level};
@ -132,11 +132,11 @@ impl Web3ProxyBlock {
} }
impl TryFrom<ArcBlock> for Web3ProxyBlock { impl TryFrom<ArcBlock> for Web3ProxyBlock {
type Error = anyhow::Error; type Error = Web3ProxyError;
fn try_from(x: ArcBlock) -> Result<Self, Self::Error> { fn try_from(x: ArcBlock) -> Result<Self, Self::Error> {
if x.number.is_none() || x.hash.is_none() { if x.number.is_none() || x.hash.is_none() {
return Err(anyhow!("Blocks here must have a number of hash")); return Err(Web3ProxyError::NoBlockNumberOrHash);
} }
let b = Web3ProxyBlock { let b = Web3ProxyBlock {
@ -166,7 +166,7 @@ impl Web3Rpcs {
&self, &self,
block: Web3ProxyBlock, block: Web3ProxyBlock,
heaviest_chain: bool, heaviest_chain: bool,
) -> anyhow::Result<Web3ProxyBlock> { ) -> Web3ProxyResult<Web3ProxyBlock> {
// TODO: i think we can rearrange this function to make it faster on the hot path // TODO: i think we can rearrange this function to make it faster on the hot path
let block_hash = block.hash(); let block_hash = block.hash();
@ -198,13 +198,13 @@ impl Web3Rpcs {
/// Get a block from caches with fallback. /// Get a block from caches with fallback.
/// Will query a specific node or the best available. /// Will query a specific node or the best available.
/// TODO: return anyhow::Result<Option<ArcBlock>>? /// TODO: return Web3ProxyResult<Option<ArcBlock>>?
pub async fn block( pub async fn block(
&self, &self,
authorization: &Arc<Authorization>, authorization: &Arc<Authorization>,
hash: &H256, hash: &H256,
rpc: Option<&Arc<Web3Rpc>>, rpc: Option<&Arc<Web3Rpc>>,
) -> anyhow::Result<Web3ProxyBlock> { ) -> Web3ProxyResult<Web3ProxyBlock> {
// first, try to get the hash from our cache // first, try to get the hash from our cache
// the cache is set last, so if its here, its everywhere // the cache is set last, so if its here, its everywhere
// TODO: use try_get_with // TODO: use try_get_with
@ -233,7 +233,7 @@ impl Web3Rpcs {
x.try_into().ok() x.try_into().ok()
} }
}) })
.context("no block!")?, .web3_context("no block!")?,
None => { None => {
// TODO: helper for method+params => JsonRpcRequest // TODO: helper for method+params => JsonRpcRequest
// TODO: does this id matter? // TODO: does this id matter?
@ -253,16 +253,16 @@ impl Web3Rpcs {
.await?; .await?;
if let Some(err) = response.error { if let Some(err) = response.error {
let err = anyhow::anyhow!("{:#?}", err); return Err(err).web3_context("failed fetching block");
return Err(err.context("failed fetching block"));
} }
let block = response.result.context("no error, but also no block")?; let block = response
.result
.web3_context("no error, but also no block")?;
let block: Option<ArcBlock> = serde_json::from_str(block.get())?; let block: Option<ArcBlock> = serde_json::from_str(block.get())?;
let block: ArcBlock = block.context("no block in the response")?; let block: ArcBlock = block.web3_context("no block in the response")?;
// TODO: received time is going to be weird // TODO: received time is going to be weird
Web3ProxyBlock::try_from(block)? Web3ProxyBlock::try_from(block)?
@ -281,7 +281,7 @@ impl Web3Rpcs {
&self, &self,
authorization: &Arc<Authorization>, authorization: &Arc<Authorization>,
num: &U64, num: &U64,
) -> anyhow::Result<(H256, u64)> { ) -> Web3ProxyResult<(H256, u64)> {
let (block, block_depth) = self.cannonical_block(authorization, num).await?; let (block, block_depth) = self.cannonical_block(authorization, num).await?;
let hash = *block.hash(); let hash = *block.hash();
@ -295,7 +295,7 @@ impl Web3Rpcs {
&self, &self,
authorization: &Arc<Authorization>, authorization: &Arc<Authorization>,
num: &U64, num: &U64,
) -> anyhow::Result<(Web3ProxyBlock, u64)> { ) -> Web3ProxyResult<(Web3ProxyBlock, u64)> {
// we only have blocks by hash now // we only have blocks by hash now
// maybe save them during save_block in a blocks_by_number Cache<U64, Vec<ArcBlock>> // maybe save them during save_block in a blocks_by_number Cache<U64, Vec<ArcBlock>>
// if theres multiple, use petgraph to find the one on the main chain (and remove the others if they have enough confirmations) // if theres multiple, use petgraph to find the one on the main chain (and remove the others if they have enough confirmations)
@ -303,7 +303,7 @@ impl Web3Rpcs {
let mut consensus_head_receiver = self let mut consensus_head_receiver = self
.watch_consensus_head_sender .watch_consensus_head_sender
.as_ref() .as_ref()
.context("need new head subscriptions to fetch cannonical_block")? .web3_context("need new head subscriptions to fetch cannonical_block")?
.subscribe(); .subscribe();
// be sure the requested block num exists // be sure the requested block num exists
@ -311,7 +311,7 @@ impl Web3Rpcs {
let mut head_block_num = *consensus_head_receiver let mut head_block_num = *consensus_head_receiver
.borrow_and_update() .borrow_and_update()
.as_ref() .as_ref()
.context("no consensus head block")? .web3_context("no consensus head block")?
.number(); .number();
loop { loop {
@ -352,7 +352,7 @@ impl Web3Rpcs {
debug!("could not find canonical block {}: {:?}", num, err); debug!("could not find canonical block {}: {:?}", num, err);
} }
let raw_block = response.result.context("no cannonical block result")?; let raw_block = response.result.web3_context("no cannonical block result")?;
let block: ArcBlock = serde_json::from_str(raw_block.get())?; let block: ArcBlock = serde_json::from_str(raw_block.get())?;
@ -412,13 +412,13 @@ impl Web3Rpcs {
consensus_finder: &mut ConsensusFinder, consensus_finder: &mut ConsensusFinder,
new_block: Option<Web3ProxyBlock>, new_block: Option<Web3ProxyBlock>,
rpc: Arc<Web3Rpc>, rpc: Arc<Web3Rpc>,
pending_tx_sender: &Option<broadcast::Sender<TxStatus>>, _pending_tx_sender: &Option<broadcast::Sender<TxStatus>>,
) -> anyhow::Result<()> { ) -> Web3ProxyResult<()> {
// TODO: how should we handle an error here? // TODO: how should we handle an error here?
if !consensus_finder if !consensus_finder
.update_rpc(new_block.clone(), rpc.clone(), self) .update_rpc(new_block.clone(), rpc.clone(), self)
.await .await
.context("failed to update rpc")? .web3_context("failed to update rpc")?
{ {
// nothing changed. no need to scan for a new consensus head // nothing changed. no need to scan for a new consensus head
return Ok(()); return Ok(());
@ -429,12 +429,10 @@ impl Web3Rpcs {
.await .await
{ {
Err(err) => { Err(err) => {
let err = err.context("error while finding consensus head block!"); return Err(err).web3_context("error while finding consensus head block!");
return Err(err);
} }
Ok(None) => { Ok(None) => {
return Err(anyhow!("no consensus head block!")); return Err(Web3ProxyError::NoConsensusHeadBlock);
} }
Ok(Some(x)) => x, Ok(Some(x)) => x,
}; };
@ -479,7 +477,8 @@ impl Web3Rpcs {
watch_consensus_head_sender watch_consensus_head_sender
.send(Some(consensus_head_block)) .send(Some(consensus_head_block))
.context( .or(Err(Web3ProxyError::WatchSendError))
.web3_context(
"watch_consensus_head_sender failed sending first consensus_head_block", "watch_consensus_head_sender failed sending first consensus_head_block",
)?; )?;
} }
@ -529,11 +528,12 @@ impl Web3Rpcs {
let consensus_head_block = self let consensus_head_block = self
.try_cache_block(consensus_head_block, true) .try_cache_block(consensus_head_block, true)
.await .await
.context("save consensus_head_block as heaviest chain")?; .web3_context("save consensus_head_block as heaviest chain")?;
watch_consensus_head_sender watch_consensus_head_sender
.send(Some(consensus_head_block)) .send(Some(consensus_head_block))
.context("watch_consensus_head_sender failed sending uncled consensus_head_block")?; .or(Err(Web3ProxyError::WatchSendError))
.web3_context("watch_consensus_head_sender failed sending uncled consensus_head_block")?;
} }
} }
Ordering::Less => { Ordering::Less => {
@ -562,11 +562,14 @@ impl Web3Rpcs {
let consensus_head_block = self let consensus_head_block = self
.try_cache_block(consensus_head_block, true) .try_cache_block(consensus_head_block, true)
.await .await
.context("save_block sending consensus_head_block as heaviest chain")?; .web3_context(
"save_block sending consensus_head_block as heaviest chain",
)?;
watch_consensus_head_sender watch_consensus_head_sender
.send(Some(consensus_head_block)) .send(Some(consensus_head_block))
.context("watch_consensus_head_sender failed sending rollback consensus_head_block")?; .or(Err(Web3ProxyError::WatchSendError))
.web3_context("watch_consensus_head_sender failed sending rollback consensus_head_block")?;
} }
Ordering::Greater => { Ordering::Greater => {
debug!( debug!(
@ -590,7 +593,9 @@ impl Web3Rpcs {
let consensus_head_block = let consensus_head_block =
self.try_cache_block(consensus_head_block, true).await?; self.try_cache_block(consensus_head_block, true).await?;
watch_consensus_head_sender.send(Some(consensus_head_block)).context("watch_consensus_head_sender failed sending new consensus_head_block")?; watch_consensus_head_sender.send(Some(consensus_head_block))
.or(Err(Web3ProxyError::WatchSendError))
.web3_context("watch_consensus_head_sender failed sending new consensus_head_block")?;
} }
} }
} }

View File

@ -2,7 +2,7 @@ use super::blockchain::Web3ProxyBlock;
use super::many::Web3Rpcs; use super::many::Web3Rpcs;
use super::one::Web3Rpc; use super::one::Web3Rpc;
use crate::frontend::authorization::Authorization; use crate::frontend::authorization::Authorization;
use anyhow::Context; use crate::frontend::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResult};
use ethers::prelude::{H256, U64}; use ethers::prelude::{H256, U64};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use itertools::{Itertools, MinMaxResult}; use itertools::{Itertools, MinMaxResult};
@ -156,7 +156,7 @@ impl ConsensusFinder {
rpc: Arc<Web3Rpc>, rpc: Arc<Web3Rpc>,
// we need this so we can save the block to caches. i don't like it though. maybe we should use a lazy_static Cache wrapper that has a "save_block" method?. i generally dislike globals but i also dislike all the types having to pass eachother around // we need this so we can save the block to caches. i don't like it though. maybe we should use a lazy_static Cache wrapper that has a "save_block" method?. i generally dislike globals but i also dislike all the types having to pass eachother around
web3_connections: &Web3Rpcs, web3_connections: &Web3Rpcs,
) -> anyhow::Result<bool> { ) -> Web3ProxyResult<bool> {
// add the rpc's block to connection_heads, or remove the rpc from connection_heads // add the rpc's block to connection_heads, or remove the rpc from connection_heads
let changed = match rpc_head_block { let changed = match rpc_head_block {
Some(mut rpc_head_block) => { Some(mut rpc_head_block) => {
@ -164,7 +164,7 @@ impl ConsensusFinder {
rpc_head_block = web3_connections rpc_head_block = web3_connections
.try_cache_block(rpc_head_block, false) .try_cache_block(rpc_head_block, false)
.await .await
.context("failed caching block")?; .web3_context("failed caching block")?;
// if let Some(max_block_lag) = max_block_lag { // if let Some(max_block_lag) = max_block_lag {
// if rpc_head_block.number() < ??? { // if rpc_head_block.number() < ??? {
@ -203,7 +203,7 @@ impl ConsensusFinder {
&mut self, &mut self,
authorization: &Arc<Authorization>, authorization: &Arc<Authorization>,
web3_rpcs: &Web3Rpcs, web3_rpcs: &Web3Rpcs,
) -> anyhow::Result<Option<ConsensusWeb3Rpcs>> { ) -> Web3ProxyResult<Option<ConsensusWeb3Rpcs>> {
let minmax_block = self.rpc_heads.values().minmax_by_key(|&x| x.number()); let minmax_block = self.rpc_heads.values().minmax_by_key(|&x| x.number());
let (lowest_block, highest_block) = match minmax_block { let (lowest_block, highest_block) = match minmax_block {

View File

@ -7,6 +7,7 @@ use crate::app::{flatten_handle, AnyhowJoinHandle, Web3ProxyApp};
///! Load balanced communication with a group of web3 providers ///! Load balanced communication with a group of web3 providers
use crate::config::{BlockAndRpc, TxHashAndRpc, Web3RpcConfig}; use crate::config::{BlockAndRpc, TxHashAndRpc, Web3RpcConfig};
use crate::frontend::authorization::{Authorization, RequestMetadata}; use crate::frontend::authorization::{Authorization, RequestMetadata};
use crate::frontend::errors::{Web3ProxyError, Web3ProxyResult};
use crate::frontend::rpc_proxy_ws::ProxyMode; use crate::frontend::rpc_proxy_ws::ProxyMode;
use crate::jsonrpc::{JsonRpcForwardedResponse, JsonRpcRequest}; use crate::jsonrpc::{JsonRpcForwardedResponse, JsonRpcRequest};
use crate::rpcs::transactions::TxStatus; use crate::rpcs::transactions::TxStatus;
@ -422,7 +423,7 @@ impl Web3Rpcs {
params: Option<&serde_json::Value>, params: Option<&serde_json::Value>,
error_level: Level, error_level: Level,
// TODO: remove this box once i figure out how to do the options // TODO: remove this box once i figure out how to do the options
) -> anyhow::Result<JsonRpcForwardedResponse> { ) -> Web3ProxyResult<JsonRpcForwardedResponse> {
// TODO: if only 1 active_request_handles, do self.try_send_request? // TODO: if only 1 active_request_handles, do self.try_send_request?
let responses = active_request_handles let responses = active_request_handles
@ -500,7 +501,7 @@ impl Web3Rpcs {
// TODO: if we are checking for the consensus head, i don' think we need min_block_needed/max_block_needed // TODO: if we are checking for the consensus head, i don' think we need min_block_needed/max_block_needed
min_block_needed: Option<&U64>, min_block_needed: Option<&U64>,
max_block_needed: Option<&U64>, max_block_needed: Option<&U64>,
) -> anyhow::Result<OpenRequestResult> { ) -> Web3ProxyResult<OpenRequestResult> {
let usable_rpcs_by_tier_and_head_number: BTreeMap<(u64, Option<U64>), Vec<Arc<Web3Rpc>>> = { let usable_rpcs_by_tier_and_head_number: BTreeMap<(u64, Option<U64>), Vec<Arc<Web3Rpc>>> = {
let synced_connections = self.watch_consensus_rpcs_sender.borrow().clone(); let synced_connections = self.watch_consensus_rpcs_sender.borrow().clone();
@ -529,11 +530,10 @@ impl Web3Rpcs {
cmp::Ordering::Greater => { cmp::Ordering::Greater => {
// TODO: force a debug log of the original request to see if our logic is wrong? // TODO: force a debug log of the original request to see if our logic is wrong?
// TODO: attach the rpc_key_id so we can find the user to ask if they need help // TODO: attach the rpc_key_id so we can find the user to ask if they need help
return Err(anyhow::anyhow!( return Err(Web3ProxyError::InvalidBlockBounds {
"Invalid blocks bounds requested. min ({}) > max ({})", min: min_block_needed.as_u64(),
min_block_needed, max: max_block_needed.as_u64(),
max_block_needed });
));
} }
} }
} }
@ -837,7 +837,7 @@ impl Web3Rpcs {
request_metadata: Option<&Arc<RequestMetadata>>, request_metadata: Option<&Arc<RequestMetadata>>,
min_block_needed: Option<&U64>, min_block_needed: Option<&U64>,
max_block_needed: Option<&U64>, max_block_needed: Option<&U64>,
) -> anyhow::Result<JsonRpcForwardedResponse> { ) -> Web3ProxyResult<JsonRpcForwardedResponse> {
let mut skip_rpcs = vec![]; let mut skip_rpcs = vec![];
let mut method_not_available_response = None; let mut method_not_available_response = None;
@ -1065,7 +1065,7 @@ impl Web3Rpcs {
error_level: Level, error_level: Level,
max_count: Option<usize>, max_count: Option<usize>,
always_include_backups: bool, always_include_backups: bool,
) -> anyhow::Result<JsonRpcForwardedResponse> { ) -> Web3ProxyResult<JsonRpcForwardedResponse> {
let mut watch_consensus_rpcs = self.watch_consensus_rpcs_sender.subscribe(); let mut watch_consensus_rpcs = self.watch_consensus_rpcs_sender.subscribe();
loop { loop {
@ -1161,7 +1161,7 @@ impl Web3Rpcs {
request_metadata: Option<&Arc<RequestMetadata>>, request_metadata: Option<&Arc<RequestMetadata>>,
min_block_needed: Option<&U64>, min_block_needed: Option<&U64>,
max_block_needed: Option<&U64>, max_block_needed: Option<&U64>,
) -> anyhow::Result<JsonRpcForwardedResponse> { ) -> Web3ProxyResult<JsonRpcForwardedResponse> {
match authorization.checks.proxy_mode { match authorization.checks.proxy_mode {
ProxyMode::Debug | ProxyMode::Best => { ProxyMode::Debug | ProxyMode::Best => {
self.try_send_best_consensus_head_connection( self.try_send_best_consensus_head_connection(
@ -1173,7 +1173,7 @@ impl Web3Rpcs {
) )
.await .await
} }
ProxyMode::Fastest(x) => todo!("Fastest"), ProxyMode::Fastest(_x) => todo!("Fastest"),
ProxyMode::Versus => todo!("Versus"), ProxyMode::Versus => todo!("Versus"),
} }
} }
@ -1676,7 +1676,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;

View File

@ -5,6 +5,7 @@ use super::request::{OpenRequestHandle, OpenRequestResult};
use crate::app::{flatten_handle, AnyhowJoinHandle}; use crate::app::{flatten_handle, AnyhowJoinHandle};
use crate::config::{BlockAndRpc, Web3RpcConfig}; use crate::config::{BlockAndRpc, Web3RpcConfig};
use crate::frontend::authorization::Authorization; use crate::frontend::authorization::Authorization;
use crate::frontend::errors::{Web3ProxyError, Web3ProxyResult};
use crate::rpcs::request::RequestRevertHandler; use crate::rpcs::request::RequestRevertHandler;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use ethers::prelude::{Bytes, Middleware, ProviderError, TxHash, H256, U64}; use ethers::prelude::{Bytes, Middleware, ProviderError, TxHash, H256, U64};
@ -1129,7 +1130,7 @@ impl Web3Rpc {
trace!("watching pending transactions on {}", self); trace!("watching pending transactions on {}", self);
// TODO: does this keep the lock open for too long? // TODO: does this keep the lock open for too long?
match provider.as_ref() { match provider.as_ref() {
Web3Provider::Http(provider) => { Web3Provider::Http(_provider) => {
// there is a "watch_pending_transactions" function, but a lot of public nodes do not support the necessary rpc endpoints // there is a "watch_pending_transactions" function, but a lot of public nodes do not support the necessary rpc endpoints
self.wait_for_disconnect().await?; self.wait_for_disconnect().await?;
} }
@ -1184,7 +1185,7 @@ impl Web3Rpc {
authorization: &'a Arc<Authorization>, authorization: &'a Arc<Authorization>,
max_wait: Option<Duration>, max_wait: Option<Duration>,
unlocked_provider: Option<Arc<Web3Provider>>, unlocked_provider: Option<Arc<Web3Provider>>,
) -> anyhow::Result<OpenRequestHandle> { ) -> Web3ProxyResult<OpenRequestHandle> {
let max_wait = max_wait.map(|x| Instant::now() + x); let max_wait = max_wait.map(|x| Instant::now() + x);
loop { loop {
@ -1206,8 +1207,7 @@ impl Web3Rpc {
if let Some(max_wait) = max_wait { if let Some(max_wait) = max_wait {
if retry_at > max_wait { if retry_at > max_wait {
// break now since we will wait past our maximum wait time // break now since we will wait past our maximum wait time
// TODO: don't use anyhow. use specific error type return Err(Web3ProxyError::Timeout(None));
return Err(anyhow::anyhow!("timeout waiting for request handle"));
} }
} }
@ -1221,7 +1221,7 @@ impl Web3Rpc {
let now = Instant::now(); let now = Instant::now();
if now > max_wait { if now > max_wait {
return Err(anyhow::anyhow!("unable to retry for request handle")); return Err(Web3ProxyError::NoHandleReady);
} }
} }
@ -1239,7 +1239,7 @@ impl Web3Rpc {
authorization: &Arc<Authorization>, authorization: &Arc<Authorization>,
// TODO: borrow on this instead of needing to clone the Arc? // TODO: borrow on this instead of needing to clone the Arc?
unlocked_provider: Option<Arc<Web3Provider>>, unlocked_provider: Option<Arc<Web3Provider>>,
) -> anyhow::Result<OpenRequestResult> { ) -> Web3ProxyResult<OpenRequestResult> {
// TODO: think more about this read block // TODO: think more about this read block
// TODO: this should *not* be new_head_client. this should be a separate object // TODO: this should *not* be new_head_client. this should be a separate object
if unlocked_provider.is_some() || self.provider.read().await.is_some() { if unlocked_provider.is_some() || self.provider.read().await.is_some() {

View File

@ -1,12 +1,12 @@
use super::StatType; use super::StatType;
use crate::app::Web3ProxyApp; use crate::app::Web3ProxyApp;
use crate::frontend::errors::FrontendErrorResponse; use crate::frontend::errors::{Web3ProxyError, Web3ProxyResponse, Web3ProxyResult};
use crate::http_params::{ use crate::http_params::{
get_chain_id_from_params, get_page_from_params, get_query_start_from_params, get_chain_id_from_params, get_page_from_params, get_query_start_from_params,
get_query_window_seconds_from_params, get_user_id_from_params, get_query_window_seconds_from_params, get_user_id_from_params,
}; };
use anyhow::Context; use anyhow::Context;
use axum::response::{IntoResponse, Response}; use axum::response::IntoResponse;
use axum::Json; use axum::Json;
use axum::{ use axum::{
headers::{authorization::Bearer, Authorization}, headers::{authorization::Bearer, Authorization},
@ -28,7 +28,7 @@ pub fn filter_query_window_seconds(
query_window_seconds: u64, query_window_seconds: u64,
response: &mut HashMap<&str, serde_json::Value>, response: &mut HashMap<&str, serde_json::Value>,
q: Select<rpc_accounting::Entity>, q: Select<rpc_accounting::Entity>,
) -> Result<Select<rpc_accounting::Entity>, FrontendErrorResponse> { ) -> Web3ProxyResult<Select<rpc_accounting::Entity>> {
if query_window_seconds == 0 { if query_window_seconds == 0 {
// TODO: order by more than this? // TODO: order by more than this?
// query_window_seconds is not set so we aggregate all records // query_window_seconds is not set so we aggregate all records
@ -62,7 +62,7 @@ pub async fn query_user_stats<'a>(
bearer: Option<TypedHeader<Authorization<Bearer>>>, bearer: Option<TypedHeader<Authorization<Bearer>>>,
params: &'a HashMap<String, String>, params: &'a HashMap<String, String>,
stat_response_type: StatType, stat_response_type: StatType,
) -> Result<Response, FrontendErrorResponse> { ) -> Web3ProxyResponse {
let db_conn = app.db_conn().context("query_user_stats needs a db")?; let db_conn = app.db_conn().context("query_user_stats needs a db")?;
let db_replica = app let db_replica = app
.db_replica() .db_replica()
@ -209,7 +209,7 @@ pub async fn query_user_stats<'a>(
// TODO: move getting the param and checking the bearer token into a helper function // TODO: move getting the param and checking the bearer token into a helper function
if let Some(rpc_key_id) = params.get("rpc_key_id") { if let Some(rpc_key_id) = params.get("rpc_key_id") {
let rpc_key_id = rpc_key_id.parse::<u64>().map_err(|e| { let rpc_key_id = rpc_key_id.parse::<u64>().map_err(|e| {
FrontendErrorResponse::StatusCode( Web3ProxyError::StatusCode(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"Unable to parse rpc_key_id".to_string(), "Unable to parse rpc_key_id".to_string(),
Some(e.into()), Some(e.into()),

View File

@ -2,7 +2,7 @@ use super::StatType;
use crate::http_params::get_stats_column_from_params; use crate::http_params::get_stats_column_from_params;
use crate::{ use crate::{
app::Web3ProxyApp, app::Web3ProxyApp,
frontend::errors::FrontendErrorResponse, frontend::errors::{Web3ProxyError, Web3ProxyResponse},
http_params::{ http_params::{
get_chain_id_from_params, get_query_start_from_params, get_query_stop_from_params, get_chain_id_from_params, get_query_start_from_params, get_query_stop_from_params,
get_query_window_seconds_from_params, get_user_id_from_params, get_query_window_seconds_from_params, get_user_id_from_params,
@ -11,7 +11,7 @@ use crate::{
use anyhow::Context; use anyhow::Context;
use axum::{ use axum::{
headers::{authorization::Bearer, Authorization}, headers::{authorization::Bearer, Authorization},
response::{IntoResponse, Response}, response::IntoResponse,
Json, TypedHeader, Json, TypedHeader,
}; };
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
@ -60,7 +60,7 @@ pub async fn query_user_stats<'a>(
bearer: Option<TypedHeader<Authorization<Bearer>>>, bearer: Option<TypedHeader<Authorization<Bearer>>>,
params: &'a HashMap<String, String>, params: &'a HashMap<String, String>,
stat_response_type: StatType, stat_response_type: StatType,
) -> Result<Response, FrontendErrorResponse> { ) -> Web3ProxyResponse {
info!("Got this far 1"); info!("Got this far 1");
let db_conn = app.db_conn().context("query_user_stats needs a db")?; let db_conn = app.db_conn().context("query_user_stats needs a db")?;
let db_replica = app let db_replica = app
@ -96,7 +96,7 @@ pub async fn query_user_stats<'a>(
// Return a bad request if query_start == query_stop, because then the query is empty basically // Return a bad request if query_start == query_stop, because then the query is empty basically
if query_start == query_stop { if query_start == query_stop {
return Err(FrontendErrorResponse::BadRequest( return Err(Web3ProxyError::BadRequest(
"Start and Stop date cannot be equal. Please specify a (different) start date." "Start and Stop date cannot be equal. Please specify a (different) start date."
.to_owned(), .to_owned(),
)); ));
@ -462,9 +462,9 @@ pub async fn query_user_stats<'a>(
// Also optionally add the rpc_key_id: // Also optionally add the rpc_key_id:
if let Some(rpc_key_id) = params.get("rpc_key_id") { if let Some(rpc_key_id) = params.get("rpc_key_id") {
let rpc_key_id = rpc_key_id.parse::<u64>().map_err(|e| { let rpc_key_id = rpc_key_id
FrontendErrorResponse::BadRequest("Unable to parse rpc_key_id".to_string()) .parse::<u64>()
})?; .map_err(|e| Web3ProxyError::BadRequest("Unable to parse rpc_key_id".to_string()))?;
response_body.insert("rpc_key_id", serde_json::Value::Number(rpc_key_id.into())); response_body.insert("rpc_key_id", serde_json::Value::Number(rpc_key_id.into()));
} }