diff --git a/web3_proxy/src/app.rs b/web3_proxy/src/app.rs index 27b85039..dbe5c506 100644 --- a/web3_proxy/src/app.rs +++ b/web3_proxy/src/app.rs @@ -689,6 +689,10 @@ impl Web3ProxyApp { Ok(collected) } + pub fn db_conn(&self) -> Option<&DatabaseConnection> { + self.db_conn.as_ref() + } + pub async fn redis_conn(&self) -> anyhow::Result { match self.redis_pool.as_ref() { None => Err(anyhow::anyhow!("no redis server configured")), diff --git a/web3_proxy/src/frontend/authorization.rs b/web3_proxy/src/frontend/authorization.rs index 4d446ac6..e1f75abf 100644 --- a/web3_proxy/src/frontend/authorization.rs +++ b/web3_proxy/src/frontend/authorization.rs @@ -1,10 +1,11 @@ use super::errors::FrontendErrorResponse; use crate::app::{UserKeyData, Web3ProxyApp}; use anyhow::Context; -use axum::headers::{Origin, Referer, UserAgent}; +use axum::headers::{authorization::Bearer, Origin, Referer, UserAgent}; use deferred_rate_limiter::DeferredRateLimitResult; use entities::user_keys; use ipnet::IpNet; +use redis_rate_limiter::redis::AsyncCommands; use redis_rate_limiter::RedisRateLimitResult; use sea_orm::{prelude::Decimal, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; use serde::Serialize; @@ -143,6 +144,37 @@ pub async fn login_is_authorized( Ok(AuthorizedRequest::Ip(db, ip)) } +pub async fn bearer_is_authorized( + app: &Web3ProxyApp, + bearer: Bearer, + ip: IpAddr, + origin: Option, + referer: Option, + user_agent: Option, +) -> Result { + let mut redis_conn = app.redis_conn().await.context("Getting redis connection")?; + + // TODO: verify that bearer.token() is a Ulid? + let bearer_cache_key = format!("bearer:{}", bearer.token()); + + // turn bearer into a user key id + let user_key_id: u64 = redis_conn + .get(bearer_cache_key) + .await + .context("unknown bearer token")?; + + let db_conn = app.db_conn().context("Getting database connection")?; + + // turn user key id into a user key + let user_key_data = user_keys::Entity::find_by_id(user_key_id) + .one(db_conn) + .await + .context("fetching user key by id")? + .context("unknown user id")?; + + key_is_authorized(app, user_key_data.api_key, ip, origin, referer, user_agent).await +} + pub async fn ip_is_authorized( app: &Web3ProxyApp, ip: IpAddr, @@ -261,7 +293,7 @@ impl Web3ProxyApp { .try_get_with(user_key, async move { trace!(?user_key, "user_cache miss"); - let db = self.db_conn.as_ref().context("no database")?; + let db = self.db_conn().context("Getting database connection")?; // TODO: join the user table to this to return the User? we don't always need it match user_keys::Entity::find() diff --git a/web3_proxy/src/frontend/mod.rs b/web3_proxy/src/frontend/mod.rs index 6f4b2213..f9feef5f 100644 --- a/web3_proxy/src/frontend/mod.rs +++ b/web3_proxy/src/frontend/mod.rs @@ -46,9 +46,12 @@ pub async fn serve(port: u16, proxy_app: Arc) -> anyhow::Result<() // TODO: these should probbably all start with /rpc. then / can be the static site let app = Router::new() // routes should be order most to least common - .route("/rpc", post(rpc_proxy_http::public_proxy_web3_rpc)) + .route("/rpc", post(rpc_proxy_http::proxy_web3_rpc)) .route("/rpc", get(rpc_proxy_ws::public_websocket_handler)) - .route("/rpc/:user_key", post(rpc_proxy_http::user_proxy_web3_rpc)) + .route( + "/rpc/:user_key", + post(rpc_proxy_http::proxy_web3_rpc_with_key), + ) .route("/rpc/:user_key", get(rpc_proxy_ws::user_websocket_handler)) .route("/rpc/health", get(http::health)) .route("/rpc/status", get(http::status)) diff --git a/web3_proxy/src/frontend/rpc_proxy_http.rs b/web3_proxy/src/frontend/rpc_proxy_http.rs index 2168bb70..6cc47ede 100644 --- a/web3_proxy/src/frontend/rpc_proxy_http.rs +++ b/web3_proxy/src/frontend/rpc_proxy_http.rs @@ -1,8 +1,9 @@ -use super::authorization::{ip_is_authorized, key_is_authorized}; +use super::authorization::{bearer_is_authorized, ip_is_authorized, key_is_authorized}; use super::errors::FrontendResult; use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum}; use axum::extract::Path; -use axum::headers::{Origin, Referer, UserAgent}; +use axum::headers::authorization::Bearer; +use axum::headers::{Authorization, Origin, Referer, UserAgent}; use axum::TypedHeader; use axum::{response::IntoResponse, Extension, Json}; use axum_client_ip::ClientIp; @@ -10,18 +11,30 @@ use std::sync::Arc; use tracing::{error_span, Instrument}; use uuid::Uuid; -pub async fn public_proxy_web3_rpc( +pub async fn proxy_web3_rpc( Extension(app): Extension>, + bearer: Option>>, ClientIp(ip): ClientIp, Json(payload): Json, + origin: Option>, referer: Option>, user_agent: Option>, ) -> FrontendResult { let request_span = error_span!("request", %ip, ?referer, ?user_agent); - let authorization = ip_is_authorized(&app, ip) - .instrument(request_span.clone()) - .await?; + let authorization = if let Some(TypedHeader(Authorization(bearer))) = bearer { + let origin = origin.map(|x| x.0); + let referer = referer.map(|x| x.0); + let user_agent = user_agent.map(|x| x.0); + + bearer_is_authorized(&app, bearer, ip, origin, referer, user_agent) + .instrument(request_span.clone()) + .await? + } else { + ip_is_authorized(&app, ip) + .instrument(request_span.clone()) + .await? + }; let request_span = error_span!("request", ?authorization); @@ -38,7 +51,7 @@ pub async fn public_proxy_web3_rpc( Ok(Json(&response).into_response()) } -pub async fn user_proxy_web3_rpc( +pub async fn proxy_web3_rpc_with_key( Extension(app): Extension>, ClientIp(ip): ClientIp, Json(payload): Json, diff --git a/web3_proxy/src/frontend/users.rs b/web3_proxy/src/frontend/users.rs index dad394b5..9725b3a1 100644 --- a/web3_proxy/src/frontend/users.rs +++ b/web3_proxy/src/frontend/users.rs @@ -175,7 +175,7 @@ pub async fn post_login( let bearer_token = Ulid::new(); - let db = app.db_conn.as_ref().unwrap(); + let db = app.db_conn().context("Getting database connection")?; // TODO: limit columns or load whole user? let u = user::Entity::find() @@ -263,11 +263,11 @@ pub async fn get_logout( Extension(app): Extension>, TypedHeader(Authorization(bearer)): TypedHeader>, ) -> FrontendResult { + let mut redis_conn = app.redis_conn().await?; + // TODO: i don't like this. move this to a helper function so it is less fragile let bearer_cache_key = format!("bearer:{}", bearer.token()); - let mut redis_conn = app.redis_conn().await?; - redis_conn.del(bearer_cache_key).await?; // TODO: what should the response be? probably json something @@ -310,7 +310,7 @@ pub async fn post_user( } } - let db = app.db_conn.as_ref().unwrap(); + let db = app.db_conn().context("Getting database connection")?; user.save(db).await?; @@ -332,16 +332,19 @@ impl ProtectedAction { async fn verify( self, app: &Web3ProxyApp, + // TODO: i don't think we want Bearer here. we want user_key and a helper for bearer -> user_key bearer: Bearer, primary_address: &Address, ) -> anyhow::Result { // get the attached address from redis for the given auth_token. - let bearer_cache_key = format!("bearer:{}", bearer.token()); - let mut redis_conn = app.redis_conn().await?; - // TODO: is this type correct? - let u_id: Option = redis_conn.get(bearer_cache_key).await?; + let bearer_cache_key = format!("bearer:{}", bearer.token()); + + let user_key_id: Option = redis_conn + .get(bearer_cache_key) + .await + .context("fetching bearer cache key from redis")?; // TODO: if auth_address == primary_address, allow // TODO: if auth_address != primary_address, only allow if they are a secondary user with the correct role