From 115657e97cd298b64d6f938e88faf751a8d2315f Mon Sep 17 00:00:00 2001 From: Bryan Stitt Date: Tue, 16 Aug 2022 22:52:12 +0000 Subject: [PATCH] half the login page and better error handling --- Cargo.lock | 1 + web3_proxy/Cargo.toml | 4 +- web3_proxy/src/frontend/errors.rs | 25 +++++++++++- web3_proxy/src/frontend/http_proxy.rs | 1 + web3_proxy/src/frontend/mod.rs | 2 +- web3_proxy/src/frontend/users.rs | 59 +++++++++++++++++++-------- 6 files changed, 71 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7e0aa59..0ab5f4ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5525,6 +5525,7 @@ dependencies = [ "serde", "serde_json", "siwe", + "time 0.3.11", "tokio", "tokio-stream", "toml", diff --git a/web3_proxy/Cargo.toml b/web3_proxy/Cargo.toml index b7c16995..e0906859 100644 --- a/web3_proxy/Cargo.toml +++ b/web3_proxy/Cargo.toml @@ -50,8 +50,10 @@ siwe = "0.4.1" sea-orm = { version = "0.9.1", features = ["macros"] } serde = { version = "1.0.143", features = [] } serde_json = { version = "1.0.83", default-features = false, features = ["alloc", "raw_value"] } +# TODO: make sure this time version matches siwe. PR to put this in their prelude +time = "0.3.11" tokio = { version = "1.20.1", features = ["full", "tracing"] } -# TODO: make sure this uuid version matches what is in sea orm. PR on sea orm to put builder into prelude +# TODO: make sure this uuid version matches sea-orm. PR to put this in their prelude tokio-stream = { version = "0.1.9", features = ["sync"] } toml = "0.5.9" tower = "0.4.13" diff --git a/web3_proxy/src/frontend/errors.rs b/web3_proxy/src/frontend/errors.rs index 40c55df3..022aa5ff 100644 --- a/web3_proxy/src/frontend/errors.rs +++ b/web3_proxy/src/frontend/errors.rs @@ -5,6 +5,7 @@ use axum::{ Json, }; use derive_more::From; +use redis_rate_limit::{bb8::RunError, RedisError}; use serde_json::value::RawValue; use std::error::Error; @@ -14,12 +15,32 @@ pub type FrontendResult = Result; #[derive(From)] pub enum FrontendErrorResponse { Anyhow(anyhow::Error), - BoxError(Box), + Box(Box), + // TODO: should we box these instead? + Redis(RedisError), + RedisRunError(RunError), } impl IntoResponse for FrontendErrorResponse { fn into_response(self) -> Response { - todo!("into_response based on the error type") + let null_id = RawValue::from_string("null".to_string()).unwrap(); + + // TODO: think more about this. this match should probably give us http and jsonrpc codes + let err = match self { + Self::Anyhow(err) => err, + Self::Box(err) => anyhow::anyhow!("Boxed error: {:?}", err), + Self::Redis(err) => err.into(), + Self::RedisRunError(err) => err.into(), + }; + + let err = JsonRpcForwardedResponse::from_anyhow_error(err, null_id); + + let code = StatusCode::INTERNAL_SERVER_ERROR; + + // TODO: logs here are too verbose. emit a stat instead? or maybe only log internal errors? + // warn!("Responding with error: {:?}", err); + + (code, Json(err)).into_response() } } diff --git a/web3_proxy/src/frontend/http_proxy.rs b/web3_proxy/src/frontend/http_proxy.rs index 5b769481..bffc5441 100644 --- a/web3_proxy/src/frontend/http_proxy.rs +++ b/web3_proxy/src/frontend/http_proxy.rs @@ -15,6 +15,7 @@ pub async fn public_proxy_web3_rpc( Extension(app): Extension>, ClientIp(ip): ClientIp, ) -> Response { + // TODO: dry this up a lot let _ip = match app.rate_limit_by_ip(ip).await { Ok(x) => match x.try_into_response().await { Ok(RateLimitResult::AllowedIp(x)) => x, diff --git a/web3_proxy/src/frontend/mod.rs b/web3_proxy/src/frontend/mod.rs index 7ebb72d1..e0c233e9 100644 --- a/web3_proxy/src/frontend/mod.rs +++ b/web3_proxy/src/frontend/mod.rs @@ -68,7 +68,7 @@ pub async fn serve(port: u16, proxy_app: Arc) -> anyhow::Result<() .route("/u/:user_key", get(ws_proxy::user_websocket_handler)) .route("/health", get(http::health)) .route("/status", get(http::status)) - .route("/login", get(users::get_login)) + .route("/login/:user_address", get(users::get_login)) .route("/users", post(users::create_user)) .route( "/foo", diff --git a/web3_proxy/src/frontend/users.rs b/web3_proxy/src/frontend/users.rs index 5eaff6ca..85360f5c 100644 --- a/web3_proxy/src/frontend/users.rs +++ b/web3_proxy/src/frontend/users.rs @@ -21,11 +21,14 @@ use axum_client_ip::ClientIp; use axum_macros::debug_handler; use entities::user; use ethers::{prelude::Address, types::Bytes}; -use redis_rate_limit::redis::{pipe, AsyncCommands}; +use redis_rate_limit::redis::AsyncCommands; use reqwest::StatusCode; use sea_orm::ActiveModelTrait; use serde::Deserialize; +use siwe::Message; +use std::ops::Add; use std::sync::Arc; +use time::{Duration, OffsetDateTime}; use uuid::Uuid; // TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead @@ -33,7 +36,8 @@ use uuid::Uuid; pub async fn get_login( Extension(app): Extension>, ClientIp(ip): ClientIp, - // TODO: what does axum's error handling look like? + // TODO: what does axum's error handling look like if the path fails to parse? + // TODO: allow ENS names here? Path(user_address): Path
, ) -> FrontendResult { // TODO: refactor this to use the try operator @@ -51,29 +55,50 @@ pub async fn get_login( // its a better UX to just click "login with ethereum" and have the account created if it doesn't exist // we can prompt for an email and and payment after they log in - let session_id = uuid::Uuid::new_v4(); + // TODO: how many seconds? get from config? + let expire_seconds: usize = 300; - // TODO: if no redis, store in local cache? + // create a session id and save it in redis + let nonce = Uuid::new_v4(); + + let issued_at = OffsetDateTime::now_utc(); + + let expiration_time = issued_at.add(Duration::new(expire_seconds as i64, 0)); + + // TODO: get request_id out of the trace? do we need that when we have a none? + + // TODO: get most of these from the app config + let message = Message { + domain: "staging.llamanodes.com".parse().unwrap(), + address: user_address.to_fixed_bytes(), + statement: Some("🦙🦙🦙🦙🦙".to_string()), + uri: "https://staging.llamanodes.com/".parse().unwrap(), + version: siwe::Version::V1, + chain_id: 1, + expiration_time: Some(expiration_time.into()), + issued_at: issued_at.into(), + nonce: nonce.to_string(), + not_before: None, + request_id: None, + resources: vec![], + }; + + let session_key = format!("pending:{}", nonce); + + // TODO: if no redis server, store in local cache? let redis_pool = app .redis_pool .as_ref() .expect("login requires a redis server"); - let mut redis_conn = redis_pool.get().await.unwrap(); + let mut redis_conn = redis_pool.get().await?; - // TODO: how many seconds? get from config? - let session_expiration_seconds = 300; + // TODO: the address isn't enough. we need to save the actual message + redis_conn + .set_ex(session_key, message.to_string(), expire_seconds) + .await?; - let reply: String = redis_conn - .set_ex( - session_id.to_string(), - user_address.to_string(), - session_expiration_seconds, - ) - .await - .unwrap(); - - todo!("how should this work? probably keep stuff in redis") + Ok(message.to_string().into_response()) } #[debug_handler]