add logout endpoint and prefix with /rpc
This commit is contained in:
parent
961ccf7cf2
commit
dbd8ea2429
84
Cargo.lock
generated
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
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(),
|
||||
|
Loading…
Reference in New Issue
Block a user