it compiles

This commit is contained in:
Bryan Stitt 2022-08-21 08:18:57 +00:00
parent d9be55f83e
commit 5af834d710
6 changed files with 131 additions and 74 deletions

12
Cargo.lock generated

@ -5090,7 +5090,7 @@ dependencies = [
"http", "http",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"ulid", "ulid 0.4.1",
] ]
[[package]] [[package]]
@ -5235,6 +5235,15 @@ dependencies = [
"rand 0.6.5", "rand 0.6.5",
] ]
[[package]]
name = "ulid"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a3aaa69b04e5b66cc27309710a569ea23593612387d67daaf102e73aa974fd"
dependencies = [
"rand 0.8.5",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.8" version = "0.3.8"
@ -5525,6 +5534,7 @@ dependencies = [
"tower-request-id", "tower-request-id",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"ulid 1.0.0",
"url", "url",
"uuid 1.1.2", "uuid 1.1.2",
] ]

@ -62,6 +62,7 @@ tower-http = { version = "0.3.4", features = ["trace"] }
tracing = "0.1.36" tracing = "0.1.36"
# TODO: tracing-subscriber has serde and serde_json features that we might want to use # TODO: tracing-subscriber has serde and serde_json features that we might want to use
tracing-subscriber = { version = "0.3.15", features = ["env-filter", "parking_lot"] } tracing-subscriber = { version = "0.3.15", features = ["env-filter", "parking_lot"] }
ulid = "1.0.0"
url = "2.2.2" url = "2.2.2"
uuid = "1.1.2" uuid = "1.1.2"

@ -215,6 +215,7 @@ mod tests {
app: AppConfig { app: AppConfig {
chain_id: 31337, chain_id: 31337,
db_url: None, db_url: None,
invite_code: None,
redis_url: None, redis_url: None,
public_rate_limit_per_minute: 0, public_rate_limit_per_minute: 0,
response_cache_max_bytes: 10_usize.pow(7), response_cache_max_bytes: 10_usize.pow(7),

@ -44,6 +44,7 @@ pub struct AppConfig {
// TODO: better type for chain_id? max of `u64::MAX / 2 - 36` https://github.com/ethereum/EIPs/issues/2294 // TODO: better type for chain_id? max of `u64::MAX / 2 - 36` https://github.com/ethereum/EIPs/issues/2294
pub chain_id: u64, pub chain_id: u64,
pub db_url: Option<String>, pub db_url: Option<String>,
pub invite_code: Option<String>,
pub redis_url: Option<String>, pub redis_url: Option<String>,
#[serde(default = "default_public_rate_limit_per_minute")] #[serde(default = "default_public_rate_limit_per_minute")]
pub public_rate_limit_per_minute: u64, pub public_rate_limit_per_minute: u64,

@ -1,4 +1,4 @@
/// this should move into web3_proxy once the basics are working mod axum_ext;
mod errors; mod errors;
mod http; mod http;
mod http_proxy; mod http_proxy;
@ -55,32 +55,25 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
) )
}); });
/* // build our axum Router
let some_fallible_service = tower::service_fn(|_req| async {
// thing_that_might_fail().await?;
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
});
*/
// build our application with a route
// order most to least common
let app = Router::new() let app = Router::new()
// routes should be order most to least common
.route("/", post(http_proxy::public_proxy_web3_rpc)) .route("/", post(http_proxy::public_proxy_web3_rpc))
.route("/", get(ws_proxy::public_websocket_handler)) .route("/", get(ws_proxy::public_websocket_handler))
.route("/u/:user_key", post(http_proxy::user_proxy_web3_rpc)) .route("/u/:user_key", post(http_proxy::user_proxy_web3_rpc))
.route("/u/:user_key", get(ws_proxy::user_websocket_handler)) .route("/u/:user_key", get(ws_proxy::user_websocket_handler))
.route("/health", get(http::health)) .route("/health", get(http::health))
// TODO: we probably want to remove /status in favor of the separate prometheus thread
.route("/status", get(http::status)) .route("/status", get(http::status))
.route("/login/:user_address", get(users::get_login)) .route("/login/:user_address", get(users::get_login))
.route("/login/:user_address/:message_eip", get(users::get_login)) .route("/login/:user_address/:message_eip", get(users::get_login))
.route("/users", post(users::create_user)) .route("/users", post(users::post_user))
// .route( // layers are ordered bottom up
// "/foo", // the last layer is first for requests and last for responses
// HandleError::new(some_fallible_service, handle_anyhow_error),
// )
.layer(Extension(proxy_app)) .layer(Extension(proxy_app))
// create a unique id for each request and add it to our tracing logs // add the request id to our tracing logs
.layer(request_tracing_layer) .layer(request_tracing_layer)
// create a unique id for each request
.layer(RequestIdLayer) .layer(RequestIdLayer)
// 404 for any unknown routes // 404 for any unknown routes
.fallback(errors::handler_404.into_service()); .fallback(errors::handler_404.into_service());

@ -13,7 +13,7 @@ use super::{
}; };
use crate::app::Web3ProxyApp; use crate::app::Web3ProxyApp;
use axum::{ use axum::{
extract::Path, extract::{Path, Query},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Extension, Json, Extension, Json,
}; };
@ -30,7 +30,10 @@ use siwe::Message;
use std::ops::Add; use std::ops::Add;
use std::sync::Arc; use std::sync::Arc;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use uuid::Uuid; use ulid::Ulid;
#[allow(unused)]
use super::axum_ext::empty_string_as_none;
// TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead // TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead
#[debug_handler] #[debug_handler]
@ -52,15 +55,16 @@ pub async fn get_login(
}; };
// at first i thought about checking that user_address is in our db // at first i thought about checking that user_address is in our db
// but theres no need to separate the create_user and login flows // but theres no need to separate the registration and login flows
// its a better UX to just click "login with ethereum" and have the account created if it doesn't exist // 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 // we can prompt for an email and and payment after they log in
// create a message and save it in redis
// TODO: how many seconds? get from config? // TODO: how many seconds? get from config?
let expire_seconds: usize = 300; let expire_seconds: usize = 300;
// create a message and save it in redis let nonce = Ulid::new();
let nonce = Uuid::new_v4();
let issued_at = OffsetDateTime::now_utc(); let issued_at = OffsetDateTime::now_utc();
@ -88,18 +92,21 @@ pub async fn get_login(
let session_key = format!("pending:{}", nonce); let session_key = format!("pending:{}", nonce);
// TODO: if no redis server, store in local cache? // TODO: if no redis server, store in local cache?
let redis_pool = app let mut redis_conn = app
.redis_pool .redis_pool
.as_ref() .as_ref()
.expect("login requires a redis server"); .expect("login requires a redis server")
.get()
.await?;
let mut redis_conn = redis_pool.get().await?; // the address isn't enough. we need to save the actual message so we can read the nonce
// TODO: what message format is the most efficient to store in redis? probably eip191_string
// TODO: the address isn't enough. we need to save the actual message
redis_conn redis_conn
.set_ex(session_key, message.to_string(), expire_seconds) .set_ex(session_key, message.to_string(), expire_seconds)
.await?; .await?;
drop(redis_conn);
// there are multiple ways to sign messages and not all wallets support them // there are multiple ways to sign messages and not all wallets support them
let message_eip = params let message_eip = params
.remove("message_eip") .remove("message_eip")
@ -110,21 +117,38 @@ pub async fn get_login(
// https://github.com/spruceid/siwe/issues/98 // https://github.com/spruceid/siwe/issues/98
"eip191_string" => Bytes::from(message.eip191_string().unwrap()).to_string(), "eip191_string" => Bytes::from(message.eip191_string().unwrap()).to_string(),
"eip191_hash" => Bytes::from(&message.eip191_hash().unwrap()).to_string(), "eip191_hash" => Bytes::from(&message.eip191_hash().unwrap()).to_string(),
_ => todo!("return a proper error"), _ => return Err(anyhow::anyhow!("invalid message eip given").into()),
}; };
Ok(message.into_response()) Ok(message.into_response())
} }
/// Query params to our `post_login` handler.
#[derive(Debug, Deserialize)]
pub struct PostLoginQuery {
invite_code: Option<String>,
}
/// JSON body to our `post_login` handler.
#[derive(Deserialize)]
pub struct PostLogin {
address: Address,
msg: String,
sig: Bytes,
version: String,
signer: String,
}
#[debug_handler] #[debug_handler]
pub async fn create_user( /// Post to the user endpoint to register or login.
// this argument tells axum to parse the request body pub async fn post_login(
// as JSON into a `CreateUser` type
Json(payload): Json<CreateUser>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp, ClientIp(ip): ClientIp,
Extension(app): Extension<Arc<Web3ProxyApp>>,
Json(payload): Json<PostLogin>,
Query(query): Query<PostLoginQuery>,
) -> Response { ) -> Response {
// TODO: return a Result instead // TODO: return a Result instead
// TODO: dry this up ip checking up
let _ip = match app.rate_limit_by_ip(ip).await { let _ip = match app.rate_limit_by_ip(ip).await {
Ok(x) => match x.try_into_response().await { Ok(x) => match x.try_into_response().await {
Ok(RateLimitResult::AllowedIp(x)) => x, Ok(RateLimitResult::AllowedIp(x)) => x,
@ -134,11 +158,22 @@ pub async fn create_user(
Err(err) => return anyhow_error_into_response(None, None, err), Err(err) => return anyhow_error_into_response(None, None, err),
}; };
// TODO: check invite_code against the app's config or database let mut new_user = true; // TODO: check the database
if payload.invite_code != "llam4n0des!" {
todo!("proper error message") if let Some(invite_code) = &app.config.invite_code {
// we don't do per-user referral codes because we shouldn't collect what we don't need.
// we don't need to build a social graph between addresses like that.
if query.invite_code.as_ref() != Some(invite_code) {
todo!("if address is already registered, allow login! else, error")
}
} }
// we can't trust that they didn't tamper with the message in some way
let their_msg: siwe::Message = payload.msg.parse().unwrap();
let their_sig: [u8; 65] = payload.sig.as_ref().try_into().unwrap();
// fetch the message we gave them from our redis
let redis_pool = app let redis_pool = app
.redis_pool .redis_pool
.as_ref() .as_ref()
@ -148,60 +183,76 @@ pub async fn create_user(
// TODO: use getdel // TODO: use getdel
// TODO: do not unwrap. make this function return a FrontendResult // TODO: do not unwrap. make this function return a FrontendResult
let message: String = redis_conn.get(payload.nonce.to_string()).await.unwrap(); let our_msg: String = redis_conn.get(&their_msg.nonce).await.unwrap();
let message: Message = message.parse().unwrap(); let our_msg: siwe::Message = our_msg.parse().unwrap();
// TODO: dont unwrap. proper error // check the domain and a nonce. let timestamp be automatic
let signature: [u8; 65] = payload.signature.as_ref().try_into().unwrap(); if let Err(e) = their_msg.verify(their_sig, Some(&our_msg.domain), Some(&our_msg.nonce), None) {
// TODO: calculate the expected message for the current user. include domain and a nonce. let timestamp be automatic
if let Err(e) = message.verify(signature, None, None, None) {
// message cannot be correctly authenticated // message cannot be correctly authenticated
todo!("proper error message: {}", e) todo!("proper error message: {}", e)
} }
let user = user::ActiveModel { if new_user {
address: sea_orm::Set(payload.address.to_fixed_bytes().into()), // the only thing we need from them is an address
email: sea_orm::Set(payload.email), // everything else is optional
..Default::default() let user = user::ActiveModel {
}; address: sea_orm::Set(payload.address.to_fixed_bytes().into()),
..Default::default()
};
let db = app.db_conn.as_ref().unwrap(); let db = app.db_conn.as_ref().unwrap();
// TODO: proper error message let user = user.insert(db).await.unwrap();
let user = user.insert(db).await.unwrap();
// TODO: create let api_key = todo!("create an api key");
let api_key = todo!();
/* /*
let rpm = app.config.something; let rpm = app.config.something;
// create a key for the new user // create a key for the new user
// TODO: requests_per_minute should be configurable // TODO: requests_per_minute should be configurable
let uk = user_keys::ActiveModel { let uk = user_keys::ActiveModel {
user_id: u.id, user_id: u.id,
api_key: sea_orm::Set(api_key), api_key: sea_orm::Set(api_key),
requests_per_minute: sea_orm::Set(rpm), requests_per_minute: sea_orm::Set(rpm),
..Default::default() ..Default::default()
}; };
// TODO: if this fails, rever adding the user, too // TODO: if this fails, rever adding the user, too
let uk = uk.save(&txn).await.context("Failed saving new user key")?; let uk = uk.save(&txn).await.context("Failed saving new user key")?;
// TODO: do not expose user ids // TODO: set a cookie?
(StatusCode::CREATED, Json(user)).into_response()
*/ // TODO: do not expose user ids
(StatusCode::CREATED, Json(user)).into_response()
*/
} else {
todo!("load existing user from the database");
}
} }
// the input to our `create_user` handler /// the JSON input to the `post_user` handler
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CreateUser { pub struct PostUser {
address: Address, address: Address,
// TODO: make sure the email address is valid // TODO: make sure the email address is valid. probably have a "verified" column in the database
email: Option<String>, email: Option<String>,
signature: Bytes, // TODO: make them sign this JSON? cookie in session id is hard because its on a different domain
nonce: Uuid, }
invite_code: String,
#[debug_handler]
/// post to the user endpoint to modify your account
pub async fn post_user(
Json(payload): Json<PostUser>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
) -> FrontendResult {
todo!("finish post_login");
// let user = user::ActiveModel {
// address: sea_orm::Set(payload.address.to_fixed_bytes().into()),
// email: sea_orm::Set(payload.email),
// ..Default::default()
// };
} }