allow origins on public entrypoints
This commit is contained in:
parent
cbe2c7a6cd
commit
9422a335a7
1
TODO.md
1
TODO.md
@ -191,6 +191,7 @@ These are roughly in order of completition
|
|||||||
- we need this because we need to be sure all the queries are saved in the db. maybe put stuff in Drop
|
- we need this because we need to be sure all the queries are saved in the db. maybe put stuff in Drop
|
||||||
- need an flume::watch on unflushed stats that we can subscribe to. wait for it to flip to true
|
- need an flume::watch on unflushed stats that we can subscribe to. wait for it to flip to true
|
||||||
- [x] don't use unix timestamps for response_millis since leap seconds will confuse it
|
- [x] don't use unix timestamps for response_millis since leap seconds will confuse it
|
||||||
|
- [x] config to allow origins even on the anonymous endpoints
|
||||||
- [-] ability to domain lock or ip lock said key
|
- [-] ability to domain lock or ip lock said key
|
||||||
- the code to check the database and use these entries already exists, but users don't have a way to set them
|
- the code to check the database and use these entries already exists, but users don't have a way to set them
|
||||||
- [-] new endpoints for users (not totally sure about the exact paths, but these features are all needed):
|
- [-] new endpoints for users (not totally sure about the exact paths, but these features are all needed):
|
||||||
|
@ -5,7 +5,7 @@ chain_id = 1
|
|||||||
db_max_connections = 99
|
db_max_connections = 99
|
||||||
db_url = "mysql://root:dev_web3_proxy@dev-db:3306/dev_web3_proxy"
|
db_url = "mysql://root:dev_web3_proxy@dev-db:3306/dev_web3_proxy"
|
||||||
|
|
||||||
min_sum_soft_limit = 2000
|
min_sum_soft_limit = 2_000
|
||||||
min_synced_rpcs = 2
|
min_synced_rpcs = 2
|
||||||
|
|
||||||
# TODO: how do we find the optimal redis_max_connections? too high actually ends up being slower
|
# TODO: how do we find the optimal redis_max_connections? too high actually ends up being slower
|
||||||
@ -19,7 +19,10 @@ redirect_user_url = "https://llamanodes.com/user-rpc-stats/{{user_id}}"
|
|||||||
public_requests_per_minute = 0
|
public_requests_per_minute = 0
|
||||||
|
|
||||||
# 1GB of cache
|
# 1GB of cache
|
||||||
response_cache_max_bytes = 10000000000
|
response_cache_max_bytes = 10_000_000_000
|
||||||
|
|
||||||
|
[app.allowed_origin_requests_per_minute]
|
||||||
|
"https://chainlist.org" = 10_000
|
||||||
|
|
||||||
[balanced_rpcs]
|
[balanced_rpcs]
|
||||||
|
|
||||||
@ -94,11 +97,11 @@ response_cache_max_bytes = 10000000000
|
|||||||
[private_rpcs.flashbots]
|
[private_rpcs.flashbots]
|
||||||
disabled = true
|
disabled = true
|
||||||
url = "https://rpc.flashbots.net/fast"
|
url = "https://rpc.flashbots.net/fast"
|
||||||
soft_limit = 7074
|
soft_limit = 7_074
|
||||||
weight = 0
|
weight = 0
|
||||||
|
|
||||||
[private_rpcs.securerpc]
|
[private_rpcs.securerpc]
|
||||||
disabled = true
|
disabled = true
|
||||||
url = "https://gibson.securerpc.com/v1"
|
url = "https://gibson.securerpc.com/v1"
|
||||||
soft_limit = 4560
|
soft_limit = 4_560
|
||||||
weight = 0
|
weight = 0
|
||||||
|
@ -7,7 +7,7 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
redis-rate-limiter = { path = "../redis-rate-limiter" }
|
redis-rate-limiter = { path = "../redis-rate-limiter" }
|
||||||
|
|
||||||
anyhow = "1.0.65"
|
anyhow = "1.0.66"
|
||||||
hashbrown = "0.12.3"
|
hashbrown = "0.12.3"
|
||||||
moka = { version = "0.9.4", default-features = false, features = ["future"] }
|
moka = { version = "0.9.4", default-features = false, features = ["future"] }
|
||||||
tokio = "1.21.2"
|
tokio = "1.21.2"
|
||||||
|
@ -11,5 +11,5 @@ path = "src/mod.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sea-orm = "0.9.3"
|
sea-orm = "0.9.3"
|
||||||
serde = "1.0.145"
|
serde = "1.0.147"
|
||||||
uuid = "1.2.1"
|
uuid = "1.2.1"
|
||||||
|
@ -5,7 +5,7 @@ authors = ["Bryan Stitt <bryan@stitthappens.com>"]
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.65"
|
anyhow = "1.0.66"
|
||||||
deadpool-redis = { version = "0.10.2", features = ["rt_tokio_1", "serde"] }
|
deadpool-redis = { version = "0.10.2", features = ["rt_tokio_1", "serde"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tokio = "1.21.2"
|
tokio = "1.21.2"
|
||||||
|
@ -19,7 +19,7 @@ entities = { path = "../entities" }
|
|||||||
migration = { path = "../migration" }
|
migration = { path = "../migration" }
|
||||||
redis-rate-limiter = { path = "../redis-rate-limiter" }
|
redis-rate-limiter = { path = "../redis-rate-limiter" }
|
||||||
|
|
||||||
anyhow = { version = "1.0.65", features = ["backtrace"] }
|
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
||||||
arc-swap = "1.5.1"
|
arc-swap = "1.5.1"
|
||||||
argh = "0.1.9"
|
argh = "0.1.9"
|
||||||
axum = { version = "0.5.17", features = ["headers", "serde_json", "tokio-tungstenite", "ws"] }
|
axum = { version = "0.5.17", features = ["headers", "serde_json", "tokio-tungstenite", "ws"] }
|
||||||
@ -55,7 +55,7 @@ handlebars = "4.3.5"
|
|||||||
rustc-hash = "1.1.0"
|
rustc-hash = "1.1.0"
|
||||||
siwe = "0.5.0"
|
siwe = "0.5.0"
|
||||||
sea-orm = { version = "0.9.3", features = ["macros"] }
|
sea-orm = { version = "0.9.3", features = ["macros"] }
|
||||||
serde = { version = "1.0.145", features = [] }
|
serde = { version = "1.0.147", features = [] }
|
||||||
serde_json = { version = "1.0.87", default-features = false, features = ["alloc", "raw_value"] }
|
serde_json = { version = "1.0.87", default-features = false, features = ["alloc", "raw_value"] }
|
||||||
serde_prometheus = "0.1.6"
|
serde_prometheus = "0.1.6"
|
||||||
# TODO: make sure this time version matches siwe. PR to put this in their prelude
|
# TODO: make sure this time version matches siwe. PR to put this in their prelude
|
||||||
|
@ -65,6 +65,9 @@ pub type AnyhowJoinHandle<T> = JoinHandle<anyhow::Result<T>>;
|
|||||||
#[derive(Clone, Debug, Default, From)]
|
#[derive(Clone, Debug, Default, From)]
|
||||||
/// TODO: rename this?
|
/// TODO: rename this?
|
||||||
pub struct UserKeyData {
|
pub struct UserKeyData {
|
||||||
|
/// database id of the primary user
|
||||||
|
pub user_id: u64,
|
||||||
|
/// database id of the api key
|
||||||
pub user_key_id: u64,
|
pub user_key_id: u64,
|
||||||
/// if None, allow unlimited queries
|
/// if None, allow unlimited queries
|
||||||
pub max_requests_per_period: Option<u64>,
|
pub max_requests_per_period: Option<u64>,
|
||||||
|
@ -80,6 +80,8 @@ pub struct AppConfig {
|
|||||||
/// None = allow all requests
|
/// None = allow all requests
|
||||||
#[serde(default = "default_public_requests_per_minute")]
|
#[serde(default = "default_public_requests_per_minute")]
|
||||||
pub public_requests_per_minute: Option<u64>,
|
pub public_requests_per_minute: Option<u64>,
|
||||||
|
/// Request limit for allowed origins for anonymous users.
|
||||||
|
pub allowed_origin_requests_per_minute: HashMap<String, u64>,
|
||||||
/// Rate limit for the login entrypoint.
|
/// Rate limit for the login entrypoint.
|
||||||
/// This is separate from the rpc limits.
|
/// This is separate from the rpc limits.
|
||||||
#[serde(default = "default_login_rate_limit_per_minute")]
|
#[serde(default = "default_login_rate_limit_per_minute")]
|
||||||
|
@ -5,6 +5,7 @@ use crate::app::{UserKeyData, Web3ProxyApp};
|
|||||||
use crate::jsonrpc::JsonRpcRequest;
|
use crate::jsonrpc::JsonRpcRequest;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::headers::{authorization::Bearer, Origin, Referer, UserAgent};
|
use axum::headers::{authorization::Bearer, Origin, Referer, UserAgent};
|
||||||
|
use axum::TypedHeader;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use deferred_rate_limiter::DeferredRateLimitResult;
|
use deferred_rate_limiter::DeferredRateLimitResult;
|
||||||
use entities::{user, user_keys};
|
use entities::{user, user_keys};
|
||||||
@ -49,6 +50,7 @@ pub enum RateLimitResult {
|
|||||||
pub struct AuthorizedKey {
|
pub struct AuthorizedKey {
|
||||||
pub ip: IpAddr,
|
pub ip: IpAddr,
|
||||||
pub origin: Option<String>,
|
pub origin: Option<String>,
|
||||||
|
pub user_id: u64,
|
||||||
pub user_key_id: u64,
|
pub user_key_id: u64,
|
||||||
// TODO: just use an f32? even an f16 is probably fine
|
// TODO: just use an f32? even an f16 is probably fine
|
||||||
pub log_revert_chance: Decimal,
|
pub log_revert_chance: Decimal,
|
||||||
@ -69,14 +71,14 @@ pub struct RequestMetadata {
|
|||||||
pub response_millis: AtomicU64,
|
pub response_millis: AtomicU64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum AuthorizedRequest {
|
pub enum AuthorizedRequest {
|
||||||
/// Request from this app
|
/// Request from this app
|
||||||
Internal,
|
Internal,
|
||||||
/// Request from an anonymous IP address
|
/// Request from an anonymous IP address
|
||||||
Ip(#[serde(skip)] IpAddr),
|
Ip(IpAddr, Option<Origin>),
|
||||||
/// Request from an authenticated and authorized user
|
/// Request from an authenticated and authorized user
|
||||||
User(#[serde(skip)] Option<DatabaseConnection>, AuthorizedKey),
|
User(Option<DatabaseConnection>, AuthorizedKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestMetadata {
|
impl RequestMetadata {
|
||||||
@ -230,6 +232,7 @@ impl AuthorizedKey {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
ip,
|
ip,
|
||||||
origin,
|
origin,
|
||||||
|
user_id: user_key_data.user_id,
|
||||||
user_key_id: user_key_data.user_key_id,
|
user_key_id: user_key_data.user_key_id,
|
||||||
log_revert_chance: user_key_data.log_revert_chance,
|
log_revert_chance: user_key_data.log_revert_chance,
|
||||||
})
|
})
|
||||||
@ -240,9 +243,8 @@ impl AuthorizedRequest {
|
|||||||
/// Only User has a database connection in case it needs to save a revert to the database.
|
/// Only User has a database connection in case it needs to save a revert to the database.
|
||||||
pub fn db_conn(&self) -> Option<&DatabaseConnection> {
|
pub fn db_conn(&self) -> Option<&DatabaseConnection> {
|
||||||
match self {
|
match self {
|
||||||
Self::Internal => None,
|
|
||||||
Self::Ip(_) => None,
|
|
||||||
Self::User(x, _) => x.as_ref(),
|
Self::User(x, _) => x.as_ref(),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,7 +253,7 @@ impl Display for &AuthorizedRequest {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
AuthorizedRequest::Internal => f.write_str("int"),
|
AuthorizedRequest::Internal => f.write_str("int"),
|
||||||
AuthorizedRequest::Ip(x) => f.write_str(&format!("ip-{}", x)),
|
AuthorizedRequest::Ip(x, _) => f.write_str(&format!("ip-{}", x)),
|
||||||
AuthorizedRequest::User(_, x) => f.write_str(&format!("uk-{}", x.user_key_id)),
|
AuthorizedRequest::User(_, x) => f.write_str(&format!("uk-{}", x.user_key_id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -272,7 +274,7 @@ pub async fn login_is_authorized(
|
|||||||
x => unimplemented!("rate_limit_login shouldn't ever see these: {:?}", x),
|
x => unimplemented!("rate_limit_login shouldn't ever see these: {:?}", x),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((AuthorizedRequest::Ip(ip), semaphore))
|
Ok((AuthorizedRequest::Ip(ip, None), semaphore))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: where should we use this?
|
// TODO: where should we use this?
|
||||||
@ -306,10 +308,13 @@ pub async fn bearer_is_authorized(
|
|||||||
pub async fn ip_is_authorized(
|
pub async fn ip_is_authorized(
|
||||||
app: &Web3ProxyApp,
|
app: &Web3ProxyApp,
|
||||||
ip: IpAddr,
|
ip: IpAddr,
|
||||||
|
origin: Option<TypedHeader<Origin>>,
|
||||||
) -> Result<(AuthorizedRequest, Option<OwnedSemaphorePermit>), FrontendErrorResponse> {
|
) -> Result<(AuthorizedRequest, Option<OwnedSemaphorePermit>), FrontendErrorResponse> {
|
||||||
|
let origin = origin.map(|x| x.0);
|
||||||
|
|
||||||
// TODO: i think we could write an `impl From` for this
|
// TODO: i think we could write an `impl From` for this
|
||||||
// TODO: move this to an AuthorizedUser extrator
|
// TODO: move this to an AuthorizedUser extrator
|
||||||
let (ip, semaphore) = match app.rate_limit_by_ip(ip).await? {
|
let (ip, semaphore) = match app.rate_limit_by_ip(ip, origin.as_ref()).await? {
|
||||||
RateLimitResult::AllowedIp(ip, semaphore) => (ip, Some(semaphore)),
|
RateLimitResult::AllowedIp(ip, semaphore) => (ip, Some(semaphore)),
|
||||||
RateLimitResult::RateLimitedIp(x, retry_at) => {
|
RateLimitResult::RateLimitedIp(x, retry_at) => {
|
||||||
return Err(FrontendErrorResponse::RateLimitedIp(x, retry_at));
|
return Err(FrontendErrorResponse::RateLimitedIp(x, retry_at));
|
||||||
@ -319,7 +324,7 @@ pub async fn ip_is_authorized(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// semaphore won't ever be None, but its easier if key auth and ip auth work the same way
|
// semaphore won't ever be None, but its easier if key auth and ip auth work the same way
|
||||||
Ok((AuthorizedRequest::Ip(ip), semaphore))
|
Ok((AuthorizedRequest::Ip(ip, origin), semaphore))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn key_is_authorized(
|
pub async fn key_is_authorized(
|
||||||
@ -432,12 +437,25 @@ impl Web3ProxyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rate_limit_by_ip(&self, ip: IpAddr) -> anyhow::Result<RateLimitResult> {
|
pub async fn rate_limit_by_ip(
|
||||||
|
&self,
|
||||||
|
ip: IpAddr,
|
||||||
|
origin: Option<&Origin>,
|
||||||
|
) -> anyhow::Result<RateLimitResult> {
|
||||||
// TODO: dry this up with rate_limit_by_key
|
// TODO: dry this up with rate_limit_by_key
|
||||||
let semaphore = self.ip_semaphore(ip).await?;
|
let semaphore = self.ip_semaphore(ip).await?;
|
||||||
|
|
||||||
if let Some(rate_limiter) = &self.frontend_ip_rate_limiter {
|
if let Some(rate_limiter) = &self.frontend_ip_rate_limiter {
|
||||||
match rate_limiter.throttle(ip, None, 1).await {
|
let max_requests_per_period = origin
|
||||||
|
.map(|origin| {
|
||||||
|
self.config
|
||||||
|
.allowed_origin_requests_per_minute
|
||||||
|
.get(&origin.to_string())
|
||||||
|
.cloned()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
match rate_limiter.throttle(ip, max_requests_per_period, 1).await {
|
||||||
Ok(DeferredRateLimitResult::Allowed) => {
|
Ok(DeferredRateLimitResult::Allowed) => {
|
||||||
Ok(RateLimitResult::AllowedIp(ip, semaphore))
|
Ok(RateLimitResult::AllowedIp(ip, semaphore))
|
||||||
}
|
}
|
||||||
@ -533,6 +551,7 @@ impl Web3ProxyApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Ok(UserKeyData {
|
Ok(UserKeyData {
|
||||||
|
user_id: user_key_model.user_id,
|
||||||
user_key_id: user_key_model.id,
|
user_key_id: user_key_model.id,
|
||||||
max_requests_per_period: user_key_model.requests_per_minute,
|
max_requests_per_period: user_key_model.requests_per_minute,
|
||||||
max_concurrent_requests: user_key_model.max_concurrent_requests,
|
max_concurrent_requests: user_key_model.max_concurrent_requests,
|
||||||
|
@ -19,12 +19,14 @@ use tracing::{error_span, Instrument};
|
|||||||
pub async fn proxy_web3_rpc(
|
pub async fn proxy_web3_rpc(
|
||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
|
origin: Option<TypedHeader<Origin>>,
|
||||||
Json(payload): Json<JsonRpcRequestEnum>,
|
Json(payload): Json<JsonRpcRequestEnum>,
|
||||||
) -> FrontendResult {
|
) -> FrontendResult {
|
||||||
let request_span = error_span!("request", %ip);
|
let request_span = error_span!("request", %ip);
|
||||||
|
|
||||||
let (authorized_request, _semaphore) =
|
let (authorized_request, _semaphore) = ip_is_authorized(&app, ip, origin)
|
||||||
ip_is_authorized(&app, ip).instrument(request_span).await?;
|
.instrument(request_span)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let request_span = error_span!("request", ?authorized_request);
|
let request_span = error_span!("request", ?authorized_request);
|
||||||
|
|
||||||
|
@ -36,13 +36,15 @@ use crate::{
|
|||||||
pub async fn websocket_handler(
|
pub async fn websocket_handler(
|
||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
|
origin: Option<TypedHeader<Origin>>,
|
||||||
ws_upgrade: Option<WebSocketUpgrade>,
|
ws_upgrade: Option<WebSocketUpgrade>,
|
||||||
) -> FrontendResult {
|
) -> FrontendResult {
|
||||||
// TODO: i don't like logging ips. move this to trace level?
|
// TODO: i don't like logging ips. move this to trace level?
|
||||||
let request_span = error_span!("request", %ip);
|
let request_span = error_span!("request", %ip, ?origin);
|
||||||
|
|
||||||
let (authorized_request, _semaphore) =
|
let (authorized_request, _semaphore) = ip_is_authorized(&app, ip, origin)
|
||||||
ip_is_authorized(&app, ip).instrument(request_span).await?;
|
.instrument(request_span)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let request_span = error_span!("request", ?authorized_request);
|
let request_span = error_span!("request", ?authorized_request);
|
||||||
|
|
||||||
@ -113,15 +115,17 @@ pub async fn websocket_handler_with_key(
|
|||||||
|
|
||||||
// TODO: show the user's address, not their id (remember to update the checks for {{user_id}}} in app.rs)
|
// TODO: show the user's address, not their id (remember to update the checks for {{user_id}}} in app.rs)
|
||||||
// TODO: query to get the user's address. expose that instead of user_id
|
// TODO: query to get the user's address. expose that instead of user_id
|
||||||
|
if let AuthorizedRequest::User(_, authorized_key) = authorized_request.as_ref() {
|
||||||
let user_url = reg
|
let user_url = reg
|
||||||
.render_template(
|
.render_template(redirect, &json!({ "user_id": authorized_key.user_id }))
|
||||||
redirect,
|
|
||||||
&json!({ "authorized_request": authorized_request }),
|
|
||||||
)
|
|
||||||
.expect("templating should always work");
|
.expect("templating should always work");
|
||||||
|
|
||||||
// this is not a websocket. redirect to a page for this user
|
// this is not a websocket. redirect to a page for this user
|
||||||
Ok(Redirect::to(&user_url).into_response())
|
Ok(Redirect::to(&user_url).into_response())
|
||||||
|
} else {
|
||||||
|
// TODO: i think this is impossible
|
||||||
|
Err(anyhow::anyhow!("this page is for rpcs").into())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: do not use an anyhow error. send the user a 400
|
// TODO: do not use an anyhow error. send the user a 400
|
||||||
Err(anyhow::anyhow!("redirect_user_url not set. only websockets work here").into())
|
Err(anyhow::anyhow!("redirect_user_url not set. only websockets work here").into())
|
||||||
|
Loading…
Reference in New Issue
Block a user