better redirect and jsonrpc handling

This commit is contained in:
Bryan Stitt 2022-08-11 01:53:27 +00:00
parent 7cf82ce156
commit 661a7ad244
7 changed files with 107 additions and 76 deletions

View File

@ -69,6 +69,8 @@
- [x] Got warning: "WARN subscribe_new_heads:send_block: web3_proxy::connection: unable to get block from https://rpc.ethermine.org: Deserialization Error: expected value at line 1 column 1. Response: error code: 1015". this is cloudflare rate limiting on fetching a block, but this is a private rpc. why is there a block subscription?
- [x] im seeing ethspam occasionally try to query a future block. something must be setting the head block too early
- [x] we were sorting best block the wrong direction. i flipped a.cmp(b) to b.cmp(a) so that the largest would be first, but then i used 'max_by' which looks at the end of the list
- [x] HTTP GET to the websocket endpoints should redirect instead of giving an ugly error
- [ ] load the redirected page from config
- [ ] basic request method stats
- [ ] use siwe messages and signatures for sign up and login
- [ ] active requests on /status is always 0 even when i'm running requests through
@ -76,7 +78,6 @@
- [ ] i think the server isn't following the spec. we need a context attached to this error so we know which one
- [ ] maybe make jsonrpc an Option
- [ ] "chain is forked" message is wrong. it includes nodes just being on different heights of the same chain. need a smarter check
- [ ] disable redis persistence in dev
## V1
@ -242,4 +243,5 @@ in another repo: event subscriber
eth_1 | 2022-08-10T23:26:06.377129Z WARN web3_proxy::connections: chain is forked! 261 possible heads. 1/2/5/5 rpcs have 0xd403…3c5d
eth_1 | 2022-08-10T23:26:08.917603Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 1/2/5/5 rpcs have 0x0538…bfff
eth_1 | 2022-08-10T23:26:10.195014Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 1/2/5/5 rpcs have 0x0538…bfff
eth_1 | 2022-08-10T23:26:10.195658Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 2/3/5/5 rpcs have 0x0538…bfff
eth_1 | 2022-08-10T23:26:10.195658Z WARN web3_proxy::connections: chain is forked! 262 possible heads. 2/3/5/5 rpcs have 0x0538…bfff
- [ ] disable redis persistence in dev

View File

@ -10,13 +10,13 @@ use crate::jsonrpc::JsonRpcForwardedResponse;
pub async fn handler_404() -> Response {
let err = anyhow::anyhow!("nothing to see here");
handle_anyhow_error(Some(StatusCode::NOT_FOUND), None, err)
anyhow_error_into_response(Some(StatusCode::NOT_FOUND), None, err)
}
/// handle errors by converting them into something that implements `IntoResponse`
/// TODO: use this. i can't get <https://docs.rs/axum/latest/axum/error_handling/index.html> to work
/// TODO: i think we want a custom result type instead. put the anyhow result inside. then `impl IntoResponse for CustomResult`
pub fn handle_anyhow_error(
pub fn anyhow_error_into_response(
http_code: Option<StatusCode>,
id: Option<Box<RawValue>>,
err: anyhow::Error,

View File

@ -5,8 +5,8 @@ use axum_client_ip::ClientIp;
use std::sync::Arc;
use uuid::Uuid;
use super::errors::handle_anyhow_error;
use super::rate_limit::handle_rate_limit_error_response;
use super::errors::anyhow_error_into_response;
use super::rate_limit::RateLimitResult;
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
pub async fn public_proxy_web3_rpc(
@ -14,15 +14,18 @@ pub async fn public_proxy_web3_rpc(
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
) -> Response {
if let Some(err_response) =
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
{
return err_response.into_response();
}
let _ip = match app.rate_limit_by_ip(ip).await {
Ok(x) => match x.try_into_response().await {
Ok(RateLimitResult::AllowedIp(x)) => x,
Err(err_response) => return err_response,
_ => unimplemented!(),
},
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
};
match app.proxy_web3_rpc(payload).await {
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
Err(err) => handle_anyhow_error(None, None, err).into_response(),
Err(err) => anyhow_error_into_response(None, None, err).into_response(),
}
}
@ -31,15 +34,17 @@ pub async fn user_proxy_web3_rpc(
Extension(app): Extension<Arc<Web3ProxyApp>>,
Path(user_key): Path<Uuid>,
) -> Response {
// TODO: add a helper on this that turns RateLimitResult into error if its not allowed
if let Some(err_response) =
handle_rate_limit_error_response(app.rate_limit_by_key(user_key).await).await
{
return err_response.into_response();
}
let _user_id = match app.rate_limit_by_key(user_key).await {
Ok(x) => match x.try_into_response().await {
Ok(RateLimitResult::AllowedUser(x)) => x,
Err(err_response) => return err_response,
_ => unimplemented!(),
},
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
};
match app.proxy_web3_rpc(payload).await {
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
Err(err) => handle_anyhow_error(None, None, err),
Err(err) => anyhow_error_into_response(None, None, err),
}
}

View File

@ -12,16 +12,46 @@ use uuid::Uuid;
use crate::app::{UserCacheValue, Web3ProxyApp};
use super::errors::handle_anyhow_error;
use super::errors::anyhow_error_into_response;
pub enum RateLimitResult {
Allowed,
RateLimitExceeded,
AllowedIp(IpAddr),
AllowedUser(i64),
IpRateLimitExceeded(IpAddr),
UserRateLimitExceeded(i64),
UnknownKey,
}
impl RateLimitResult {
// TODO: i think this should be a function on RateLimitResult
pub async fn try_into_response(self) -> Result<RateLimitResult, Response> {
match self {
RateLimitResult::AllowedIp(_) => Ok(self),
RateLimitResult::AllowedUser(_) => Ok(self),
RateLimitResult::IpRateLimitExceeded(ip) => Err(anyhow_error_into_response(
Some(StatusCode::TOO_MANY_REQUESTS),
None,
// TODO: how can we attach context here? maybe add a request id tracing field?
anyhow::anyhow!(format!("rate limit exceeded for {}", ip)),
)),
RateLimitResult::UserRateLimitExceeded(user) => Err(anyhow_error_into_response(
Some(StatusCode::TOO_MANY_REQUESTS),
None,
// TODO: don't expose numeric ids. show the address instead
// TODO: how can we attach context here? maybe add a request id tracing field?
anyhow::anyhow!(format!("rate limit exceeded for user {}", user)),
)),
RateLimitResult::UnknownKey => Err(anyhow_error_into_response(
Some(StatusCode::FORBIDDEN),
None,
anyhow::anyhow!("unknown key"),
)),
}
}
}
impl Web3ProxyApp {
pub async fn rate_limit_by_ip(&self, ip: &IpAddr) -> anyhow::Result<RateLimitResult> {
pub async fn rate_limit_by_ip(&self, ip: IpAddr) -> anyhow::Result<RateLimitResult> {
let rate_limiter_key = format!("ip-{}", ip);
// TODO: dry this up with rate_limit_by_key
@ -35,7 +65,7 @@ impl Web3ProxyApp {
// TODO: set headers so they know when they can retry
debug!(?rate_limiter_key, "rate limit exceeded"); // this is too verbose, but a stat might be good
// TODO: use their id if possible
return Ok(RateLimitResult::RateLimitExceeded);
return Ok(RateLimitResult::IpRateLimitExceeded(ip));
}
Err(err) => {
// internal error, not rate limit being hit
@ -48,7 +78,7 @@ impl Web3ProxyApp {
warn!("no rate limiter!");
}
Ok(RateLimitResult::Allowed)
Ok(RateLimitResult::AllowedIp(ip))
}
pub async fn rate_limit_by_key(&self, user_key: Uuid) -> anyhow::Result<RateLimitResult> {
@ -142,26 +172,6 @@ impl Web3ProxyApp {
unimplemented!("no redis. cannot rate limit")
}
Ok(RateLimitResult::Allowed)
}
}
pub async fn handle_rate_limit_error_response(
r: anyhow::Result<RateLimitResult>,
) -> Option<Response> {
match r {
Ok(RateLimitResult::Allowed) => None,
Ok(RateLimitResult::RateLimitExceeded) => Some(handle_anyhow_error(
Some(StatusCode::TOO_MANY_REQUESTS),
None,
// TODO: how can we attach context here? maybe add a request id tracing field?
anyhow::anyhow!("rate limit exceeded"),
)),
Ok(RateLimitResult::UnknownKey) => Some(handle_anyhow_error(
Some(StatusCode::FORBIDDEN),
None,
anyhow::anyhow!("unknown key"),
)),
Err(err) => Some(handle_anyhow_error(None, None, err)),
Ok(RateLimitResult::AllowedUser(user_data.user_id))
}
}

View File

@ -7,17 +7,21 @@
// I wonder how we handle payment
// probably have to do manual withdrawals
use axum::{response::IntoResponse, Extension, Json};
use axum::{
response::{IntoResponse, Response},
Extension, Json,
};
use axum_client_ip::ClientIp;
use entities::user;
use ethers::{prelude::Address, types::Bytes};
use reqwest::StatusCode;
use sea_orm::ActiveModelTrait;
use serde::Deserialize;
use std::sync::Arc;
use crate::app::Web3ProxyApp;
use super::rate_limit::handle_rate_limit_error_response;
use super::{rate_limit::RateLimitResult, errors::anyhow_error_into_response};
pub async fn create_user(
// this argument tells axum to parse the request body
@ -25,12 +29,15 @@ pub async fn create_user(
Json(payload): Json<CreateUser>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
) -> impl IntoResponse {
if let Some(err_response) =
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
{
return err_response.into_response();
}
) -> Response {
let _ip = match app.rate_limit_by_ip(ip).await {
Ok(x) => match x.try_into_response().await {
Ok(RateLimitResult::AllowedIp(x)) => x,
Err(err_response) => return err_response,
_ => unimplemented!(),
},
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
};
// TODO: check invite_code against the app's config or database
if payload.invite_code != "llam4n0des!" {
@ -58,8 +65,8 @@ pub async fn create_user(
// TODO: proper error message
let user = user.insert(db).await.unwrap();
todo!("serialize and return the user: {:?}", user)
// (StatusCode::CREATED, Json(user))
// TODO: do not expose user ids
(StatusCode::CREATED, Json(user)).into_response()
}
// the input to our `create_user` handler

View File

@ -1,10 +1,11 @@
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
extract::Path,
response::{IntoResponse, Response},
response::{IntoResponse, Redirect, Response},
Extension,
};
use axum_client_ip::ClientIp;
use fstrings::{format_args_f, format_f};
use futures::SinkExt;
use futures::{
future::AbortHandle,
@ -22,18 +23,21 @@ use crate::{
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
};
use super::rate_limit::handle_rate_limit_error_response;
use super::{errors::anyhow_error_into_response, rate_limit::RateLimitResult};
pub async fn public_websocket_handler(
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
ws_upgrade: Option<WebSocketUpgrade>,
) -> Response {
if let Some(err_response) =
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
{
return err_response.into_response();
}
let ip = match app.rate_limit_by_ip(ip).await {
Ok(x) => match x.try_into_response().await {
Ok(RateLimitResult::AllowedIp(x)) => x,
Err(err_response) => return err_response,
_ => unimplemented!(),
},
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
};
match ws_upgrade {
Some(ws) => ws
@ -41,9 +45,8 @@ pub async fn public_websocket_handler(
.into_response(),
None => {
// this is not a websocket. give a friendly page. maybe redirect to the llama nodes home
// TODO: make a friendly page
// TODO: rate limit this?
"hello, world".into_response()
// TODO: redirect to a configurable url
Redirect::to(&format_f!("https://www.etherscan.io/#userip={ip}")).into_response()
}
}
}
@ -53,19 +56,23 @@ pub async fn user_websocket_handler(
Path(user_key): Path<Uuid>,
ws_upgrade: Option<WebSocketUpgrade>,
) -> Response {
if let Some(err_response) =
handle_rate_limit_error_response(app.rate_limit_by_key(user_key).await).await
{
return err_response;
}
// TODO: dry this up. maybe a rate_limit_by_key_response function?
let user_id = match app.rate_limit_by_key(user_key).await {
Ok(x) => match x.try_into_response().await {
Ok(RateLimitResult::AllowedUser(x)) => x,
Err(err_response) => return err_response,
_ => unimplemented!(),
},
Err(err) => return anyhow_error_into_response(None, None, err).into_response(),
};
match ws_upgrade {
Some(ws_upgrade) => ws_upgrade.on_upgrade(|socket| proxy_web3_socket(app, socket)),
None => {
// this is not a websocket. give a friendly page with stats for this user
// TODO: make a friendly page
// TODO: rate limit this?
"hello, world".into_response()
// TODO: redirect to a configurable url
// TODO: should this use user_key instead? or user's address?
Redirect::to(&format_f!("https://www.etherscan.io/#userid={user_id}")).into_response()
}
}
}

View File

@ -8,8 +8,8 @@ use tracing::warn;
#[derive(Clone, serde::Deserialize)]
pub struct JsonRpcRequest {
// TODO: skip jsonrpc entireley?
pub jsonrpc: Box<RawValue>,
// TODO: skip jsonrpc entirely? its against spec to drop it, but some servers bad
pub jsonrpc: Option<Box<RawValue>>,
/// id could be a stricter type, but many rpcs do things against the spec
pub id: Box<RawValue>,
pub method: String,