add logout endpoint and prefix with /rpc

This commit is contained in:
Bryan Stitt 2022-09-23 21:46:27 +00:00
parent 961ccf7cf2
commit dbd8ea2429
8 changed files with 142 additions and 41 deletions

84
Cargo.lock generated

@ -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",

14
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
- <https://discord.com/channels/873880840487206962/900758376164757555/1012942974608474142>
- 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

@ -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"

@ -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<Web3Connections>,
/// Send private requests (like eth_sendRawTransaction) to all these servers
pub private_rpcs: Option<Arc<Web3Connections>>,
// 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,

@ -125,6 +125,7 @@ pub async fn ip_is_authorized(
ip: IpAddr,
) -> Result<AuthorizedRequest, FrontendErrorResponse> {
// 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) => {

@ -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<Web3ProxyApp>) -> 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<Web3ProxyApp>) -> 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());

@ -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<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
Json(payload): Json<PostLogin>,
Query(query): Query<PostLoginQuery>,
) -> FrontendResult {
@ -255,6 +256,29 @@ pub async fn post_login(
Ok(response)
}
#[debug_handler]
pub async fn get_logout(
cookies: Cookies,
Extension(app): Extension<Arc<Web3ProxyApp>>,
) -> 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<Authorization<Bearer>>,
ClientIp(ip): ClientIp,
Extension(app): Extension<Arc<Web3ProxyApp>>,
Json(payload): Json<PostUser>,
@ -313,16 +337,18 @@ impl ProtectedAction {
async fn verify(
self,
app: &Web3ProxyApp,
bearer_token: String,
bearer: Bearer,
primary_address: &Address,
) -> anyhow::Result<user::Model> {
// 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<u64> = redis_conn.get(bearer_key).await?;
let u_id: Option<u64> = 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

@ -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<AuthorizedRequest>>,
active_request_handles: Vec<OpenRequestHandle>,
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(),