From dbd8ea2429c07b52a1b9b25cd68fd0d10075a08b Mon Sep 17 00:00:00 2001 From: Bryan Stitt Date: Fri, 23 Sep 2022 21:46:27 +0000 Subject: [PATCH] add logout endpoint and prefix with /rpc --- Cargo.lock | 84 ++++++++++++++++++++---- TODO.md | 14 ++-- web3_proxy/Cargo.toml | 5 +- web3_proxy/src/app.rs | 7 ++ web3_proxy/src/frontend/authorization.rs | 1 + web3_proxy/src/frontend/mod.rs | 30 +++++---- web3_proxy/src/frontend/users.rs | 40 +++++++++-- web3_proxy/src/rpcs/connections.rs | 2 - 8 files changed, 142 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0657e50..cb991d45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array 0.14.5", +] + [[package]] name = "aes" version = "0.7.5" @@ -39,6 +48,20 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.6" @@ -451,18 +474,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "axum-auth" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9770f9a9147b2324066609acb5495538cb25f973129663fba2658ba7ed69407" -dependencies = [ - "async-trait", - "axum-core", - "base64 0.13.0", - "http", -] - [[package]] name = "axum-client-ip" version = "0.2.0" @@ -1055,7 +1066,14 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ + "aes-gcm", + "base64 0.13.0", + "hkdf", + "hmac", "percent-encoding", + "rand 0.8.5", + "sha2 0.10.2", + "subtle", "time 0.3.14", "version_check", ] @@ -2162,6 +2180,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug 0.3.0", + "polyval", +] + [[package]] name = "gimli" version = "0.26.1" @@ -2330,6 +2358,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3434,6 +3471,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug 0.3.0", + "universal-hash", +] + [[package]] name = "postgres-protocol" version = "0.6.4" @@ -5361,6 +5410,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array 0.14.5", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -5564,7 +5623,6 @@ dependencies = [ "arc-swap", "argh", "axum", - "axum-auth", "axum-client-ip", "axum-macros", "counter", diff --git a/TODO.md b/TODO.md index 258081c0..818777d5 100644 --- a/TODO.md +++ b/TODO.md @@ -160,19 +160,23 @@ These are roughly in order of completition - [-] opt-in debug mode that inspects responses for reverts and saves the request to the database for the user. - [-] let them choose a % to log (or maybe x/second). someone like curve logging all reverts will be a BIG database very quickly - this must be opt-in or spawned since it will slow things down and will make their calls less private -- [-] Api keys need option to lock to IP, cors header, referer, user agent, etc +- [x] Api keys need option to lock to IP, cors header, referer, user agent, etc +- [ ] endpoint for creating/modifying api keys and their advanced security features - [ ] active requests per second per api key - [ ] distribution of methods per api key (eth_call, eth_getLogs, etc.) - [-] add configurable size limits to all the Caches +- [ ] /user/logout to clear bearer token and jwt +- [ ] BUG: i think if all backend servers stop, the server doesn't properly reconnect. It appears to stop listening on 8854, but not shut down. +- [ ] bearer tokens should expire +- [-] signed cookie jar +- [ ] user login should return both the bearer token and a jwt (jsonwebtoken rust crate should make it easy) +- [ ] revert logs should have a maximum age and a maximum count to keep the database from being huge - [ ] Ulid instead of Uuid for user keys - - since users are actively using our service, we will need to support both - [ ] Ulid instead of Uuid for database ids - might have to use Uuid in sea-orm and then convert to Ulid on display -- [ ] bearer tokens should expire -- [-] signed cookie jar -- [ ] user login should return both the bearer token and a jwt (jsonwebtoken rust crate should make it easy) -- [ ] /user/logout to clear bearer token and jwt +- [ ] option to rotate api key ## V1 diff --git a/web3_proxy/Cargo.toml b/web3_proxy/Cargo.toml index 6463a839..9c8f1962 100644 --- a/web3_proxy/Cargo.toml +++ b/web3_proxy/Cargo.toml @@ -23,7 +23,6 @@ anyhow = { version = "1.0.65", features = ["backtrace"] } arc-swap = "1.5.1" argh = "0.1.9" axum = { version = "0.5.16", features = ["headers", "serde_json", "tokio-tungstenite", "ws"] } -axum-auth = "0.3.0" axum-client-ip = "0.2.0" axum-macros = "0.2.3" counter = "0.5.6" @@ -36,7 +35,7 @@ flume = "0.10.14" futures = { version = "0.3.24", features = ["thread-pool"] } hashbrown = { version = "0.12.3", features = ["serde"] } http = "0.2.8" -ipnet = "*" +ipnet = "2.5.0" metered = { version = "0.9.0", features = ["serialize"] } moka = { version = "0.9.4", default-features = false, features = ["future"] } notify = "5.0.0" @@ -60,7 +59,7 @@ time = "0.3.14" tokio = { version = "1.21.1", features = ["full", "tracing"] } # TODO: make sure this uuid version matches sea-orm. PR to put this in their prelude tokio-stream = { version = "0.1.10", features = ["sync"] } -tower-cookies = "0.7.0" +tower-cookies = { version = "0.7.0", features = ["private"] } toml = "0.5.9" tower = "0.4.13" tower-request-id = "0.2.0" diff --git a/web3_proxy/src/app.rs b/web3_proxy/src/app.rs index 9a5d5a9b..85b4b54f 100644 --- a/web3_proxy/src/app.rs +++ b/web3_proxy/src/app.rs @@ -44,6 +44,7 @@ use tokio::sync::{broadcast, watch}; use tokio::task::JoinHandle; use tokio::time::timeout; use tokio_stream::wrappers::{BroadcastStream, WatchStream}; +use tower_cookies::Key; use tracing::{error, info, trace, warn}; use uuid::Uuid; @@ -87,6 +88,8 @@ pub struct Web3ProxyApp { pub balanced_rpcs: Arc, /// Send private requests (like eth_sendRawTransaction) to all these servers pub private_rpcs: Option>, + // TODO: this lifetime is definitely wrong + pub cookie_key: Key, response_cache: ResponseCache, // don't drop this or the sender will stop working // TODO: broadcast channel instead? @@ -368,8 +371,12 @@ impl Web3ProxyApp { .time_to_live(Duration::from_secs(60)) .build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new()); + // TODO: get this from the app's config + let cookie_key = Key::from(&[0; 64]); + let app = Self { config: top_config.app, + cookie_key, balanced_rpcs, private_rpcs, response_cache, diff --git a/web3_proxy/src/frontend/authorization.rs b/web3_proxy/src/frontend/authorization.rs index f53f4700..19c7c41e 100644 --- a/web3_proxy/src/frontend/authorization.rs +++ b/web3_proxy/src/frontend/authorization.rs @@ -125,6 +125,7 @@ pub async fn ip_is_authorized( ip: IpAddr, ) -> Result { // TODO: i think we could write an `impl From` for this + // TODO: move this to an AuthorizedUser extrator let ip = match app.rate_limit_by_ip(ip).await? { RateLimitResult::AllowedIp(x) => x, RateLimitResult::RateLimitedIp(x, retry_at) => { diff --git a/web3_proxy/src/frontend/mod.rs b/web3_proxy/src/frontend/mod.rs index ddc98890..a1f09bb7 100644 --- a/web3_proxy/src/frontend/mod.rs +++ b/web3_proxy/src/frontend/mod.rs @@ -15,6 +15,7 @@ use axum::{ }; use std::net::SocketAddr; use std::sync::Arc; +use tower_cookies::CookieManagerLayer; use tower_http::trace::TraceLayer; use tower_request_id::{RequestId, RequestIdLayer}; use tracing::{error_span, info}; @@ -43,20 +44,25 @@ pub async fn serve(port: u16, proxy_app: Arc) -> anyhow::Result<() }); // build our axum Router + // 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("/", post(rpc_proxy_http::public_proxy_web3_rpc)) - .route("/", get(rpc_proxy_ws::public_websocket_handler)) - .route("/u/:user_key", post(rpc_proxy_http::user_proxy_web3_rpc)) - .route("/u/:user_key", get(rpc_proxy_ws::user_websocket_handler)) - .route("/health", get(http::health)) - .route("/status", get(http::status)) + .route("/rpc", post(rpc_proxy_http::public_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", get(rpc_proxy_ws::user_websocket_handler)) + .route("/rpc/health", get(http::health)) + .route("/rpc/status", get(http::status)) // TODO: make this optional or remove it since it is available on another port - .route("/prometheus", get(http::prometheus)) - .route("/login/:user_address", get(users::get_login)) - .route("/login/:user_address/:message_eip", get(users::get_login)) - .route("/login", post(users::post_login)) - .route("/users", post(users::post_user)) + .route("/rpc/prometheus", get(http::prometheus)) + .route("/rpc/user/login/:user_address", get(users::get_login)) + .route( + "/rpc/user/login/:user_address/:message_eip", + get(users::get_login), + ) + .route("/rpc/user/login", post(users::post_login)) + .route("/rpc/user", post(users::post_user)) + .route("/rpc/user/logout", get(users::get_logout)) // layers are ordered bottom up // the last layer is first for requests and last for responses .layer(Extension(proxy_app)) @@ -64,6 +70,8 @@ pub async fn serve(port: u16, proxy_app: Arc) -> anyhow::Result<() .layer(request_tracing_layer) // create a unique id for each request .layer(RequestIdLayer) + // signed cookies + .layer(CookieManagerLayer::new()) // 404 for any unknown routes .fallback(errors::handler_404.into_service()); diff --git a/web3_proxy/src/frontend/users.rs b/web3_proxy/src/frontend/users.rs index 7e17a011..2d5c209a 100644 --- a/web3_proxy/src/frontend/users.rs +++ b/web3_proxy/src/frontend/users.rs @@ -13,10 +13,10 @@ use crate::{app::Web3ProxyApp, users::new_api_key}; use anyhow::Context; use axum::{ extract::{Path, Query}, + headers::{authorization::Bearer, Authorization}, response::IntoResponse, - Extension, Json, + Extension, Json, TypedHeader, }; -use axum_auth::AuthBearer; use axum_client_ip::ClientIp; use axum_macros::debug_handler; use entities::{user, user_keys}; @@ -30,6 +30,7 @@ use siwe::Message; use std::ops::Add; use std::sync::Arc; use time::{Duration, OffsetDateTime}; +use tower_cookies::Cookies; use ulid::Ulid; use uuid::Uuid; @@ -139,8 +140,8 @@ pub struct PostLoginResponse { #[debug_handler] /// Post to the user endpoint to register or login. pub async fn post_login( - ClientIp(ip): ClientIp, Extension(app): Extension>, + ClientIp(ip): ClientIp, Json(payload): Json, Query(query): Query, ) -> FrontendResult { @@ -255,6 +256,29 @@ pub async fn post_login( Ok(response) } +#[debug_handler] +pub async fn get_logout( + cookies: Cookies, + Extension(app): Extension>, +) -> FrontendResult { + // delete the cookie if it exists + let private_cookies = cookies.private(&app.cookie_key); + + if let Some(c) = private_cookies.get("bearer") { + let bearer_cache_key = format!("bearer:{}", c.value()); + + // TODO: should deleting the cookie be last? redis being down shouldn't block the user + private_cookies.remove(c); + + let mut redis_conn = app.redis_conn().await?; + + redis_conn.del(bearer_cache_key).await?; + } + + // TODO: what should the response be? probably json something + Ok("goodbye".into_response()) +} + /// the JSON input to the `post_user` handler /// This handles updating #[derive(Deserialize)] @@ -268,7 +292,7 @@ pub struct PostUser { #[debug_handler] /// post to the user endpoint to modify your existing account pub async fn post_user( - AuthBearer(bearer_token): AuthBearer, + TypedHeader(Authorization(bearer_token)): TypedHeader>, ClientIp(ip): ClientIp, Extension(app): Extension>, Json(payload): Json, @@ -313,16 +337,18 @@ impl ProtectedAction { async fn verify( self, app: &Web3ProxyApp, - bearer_token: String, + bearer: Bearer, primary_address: &Address, ) -> anyhow::Result { // get the attached address from redis for the given auth_token. - let bearer_key = format!("bearer:{}", bearer_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_key).await?; + let u_id: Option = redis_conn.get(bearer_cache_key).await?; + + // TODO: if not in redis, check the db? // 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 diff --git a/web3_proxy/src/rpcs/connections.rs b/web3_proxy/src/rpcs/connections.rs index 20675d12..45b22474 100644 --- a/web3_proxy/src/rpcs/connections.rs +++ b/web3_proxy/src/rpcs/connections.rs @@ -309,7 +309,6 @@ impl Web3Connections { /// Send the same request to all the handles. Returning the most common success or most common error. pub async fn try_send_parallel_requests( &self, - authorization: Option<&Arc>, active_request_handles: Vec, method: &str, params: Option<&serde_json::Value>, @@ -613,7 +612,6 @@ impl Web3Connections { // TODO: this is not working right. simplify let quorum_response = self .try_send_parallel_requests( - authorization, active_request_handles, request.method.as_ref(), request.params.as_ref(),