web3-proxy/web3_proxy/src/frontend/rpc_proxy_ws.rs

353 lines
12 KiB
Rust
Raw Normal View History

2022-10-18 00:47:58 +03:00
//! Take a user's WebSocket JSON-RPC requests and either respond from local data or proxy the request to a backend rpc server.
//!
//! WebSockets are the preferred method of receiving requests, but not all clients have good support.
use super::authorization::{ip_is_authorized, key_is_authorized, Authorization};
use super::errors::{FrontendErrorResponse, FrontendResult};
2022-10-20 23:26:14 +03:00
use axum::headers::{Origin, Referer, UserAgent};
2022-05-20 08:30:54 +03:00
use axum::{
2022-05-29 20:28:41 +03:00
extract::ws::{Message, WebSocket, WebSocketUpgrade},
2022-08-06 04:17:25 +03:00
extract::Path,
response::{IntoResponse, Redirect},
Extension, TypedHeader,
2022-05-20 08:30:54 +03:00
};
2022-08-04 04:10:27 +03:00
use axum_client_ip::ClientIp;
use axum_macros::debug_handler;
2022-05-29 20:28:41 +03:00
use futures::SinkExt;
2022-05-31 04:55:04 +03:00
use futures::{
future::AbortHandle,
stream::{SplitSink, SplitStream, StreamExt},
};
2022-08-12 22:07:14 +03:00
use handlebars::Handlebars;
use hashbrown::HashMap;
use http::StatusCode;
2022-07-22 22:30:39 +03:00
use serde_json::{json, value::RawValue};
2022-08-27 05:13:36 +03:00
use std::sync::Arc;
2022-07-09 01:14:45 +03:00
use std::{str::from_utf8_mut, sync::atomic::AtomicUsize};
2022-10-29 01:52:47 +03:00
use tracing::{error, error_span, info, instrument, trace, Instrument};
2022-06-16 05:53:37 +03:00
use crate::{
app::Web3ProxyApp,
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
};
2022-05-20 08:30:54 +03:00
2022-10-18 00:47:58 +03:00
/// Public entrypoint for WebSocket JSON-RPC requests.
/// Defaults to rate limiting by IP address, but can also read the Authorization header for a bearer token.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-09-24 08:53:45 +03:00
pub async fn websocket_handler(
2022-07-07 06:22:09 +03:00
Extension(app): Extension<Arc<Web3ProxyApp>>,
2022-08-04 04:10:27 +03:00
ClientIp(ip): ClientIp,
2022-10-21 23:59:05 +03:00
origin: Option<TypedHeader<Origin>>,
2022-08-10 05:37:34 +03:00
ws_upgrade: Option<WebSocketUpgrade>,
2022-08-21 12:39:38 +03:00
) -> FrontendResult {
2022-10-20 23:26:14 +03:00
// TODO: i don't like logging ips. move this to trace level?
2022-10-21 23:59:05 +03:00
let request_span = error_span!("request", %ip, ?origin);
2022-08-04 04:10:27 +03:00
let origin = origin.map(|x| x.0);
let (authorization, _semaphore) = ip_is_authorized(&app, ip, origin)
2022-10-21 23:59:05 +03:00
.instrument(request_span)
.await?;
2022-09-24 08:53:45 +03:00
let request_span = error_span!("request", ?authorization);
let authorization = Arc::new(authorization);
2022-08-11 03:16:13 +03:00
match ws_upgrade {
2022-08-21 12:39:38 +03:00
Some(ws) => Ok(ws
.on_upgrade(|socket| {
proxy_web3_socket(app, authorization, socket).instrument(request_span)
})
2022-08-21 12:39:38 +03:00
.into_response()),
None => {
2022-10-18 00:47:58 +03:00
if let Some(redirect) = &app.config.redirect_public_url {
// this is not a websocket. redirect to a friendly page
Ok(Redirect::to(redirect).into_response())
} else {
// TODO: do not use an anyhow error. send the user a 400
Err(
anyhow::anyhow!("redirect_public_url not set. only websockets work here")
.into(),
)
}
}
}
2022-08-04 04:10:27 +03:00
}
2022-10-18 00:47:58 +03:00
/// Authenticated entrypoint for WebSocket JSON-RPC requests. Web3 wallets use this.
/// Rate limit and billing based on the api key in the url.
/// Can optionally authorized based on origin, referer, or user agent.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-09-24 08:53:45 +03:00
pub async fn websocket_handler_with_key(
2022-08-04 04:10:27 +03:00
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
2022-10-27 03:12:42 +03:00
Path(rpc_key): Path<String>,
origin: Option<TypedHeader<Origin>>,
referer: Option<TypedHeader<Referer>>,
user_agent: Option<TypedHeader<UserAgent>>,
2022-08-11 03:16:13 +03:00
ws_upgrade: Option<WebSocketUpgrade>,
2022-08-21 12:39:38 +03:00
) -> FrontendResult {
2022-10-27 03:12:42 +03:00
let rpc_key = rpc_key.parse()?;
2022-09-24 08:53:45 +03:00
let request_span = error_span!("request", %ip, ?referer, ?user_agent);
let (authorization, _semaphore) = key_is_authorized(
&app,
2022-10-27 03:12:42 +03:00
rpc_key,
ip,
origin.map(|x| x.0),
referer.map(|x| x.0),
user_agent.map(|x| x.0),
)
2022-09-24 08:53:45 +03:00
.instrument(request_span.clone())
.await?;
2022-08-04 04:10:27 +03:00
// TODO: type that wraps Address and have it censor? would protect us from accidently logging addresses or other user info
let request_span = error_span!("request", ?authorization);
let authorization = Arc::new(authorization);
2022-08-11 03:16:13 +03:00
match ws_upgrade {
Some(ws_upgrade) => Ok(ws_upgrade.on_upgrade(move |socket| {
proxy_web3_socket(app, authorization, socket).instrument(request_span)
})),
2022-08-11 03:16:13 +03:00
None => {
// if no websocket upgrade, this is probably a user loading the url with their browser
match (
&app.config.redirect_public_url,
&app.config.redirect_rpc_key_url,
authorization.checks.rpc_key_id,
) {
(None, None, _) => Err(anyhow::anyhow!(
"redirect_rpc_key_url not set. only websockets work here"
)
.into()),
(Some(redirect_public_url), _, None) => {
Ok(Redirect::to(redirect_public_url).into_response())
2022-10-21 23:59:05 +03:00
}
(_, Some(redirect_rpc_key_url), rpc_key_id) => {
let reg = Handlebars::new();
if authorization.checks.rpc_key_id.is_none() {
// TODO: i think this is impossible
Err(anyhow::anyhow!("only authenticated websockets work here").into())
} else {
let redirect_rpc_key_url = reg
.render_template(
redirect_rpc_key_url,
&json!({ "rpc_key_id": rpc_key_id }),
)
.expect("templating should always work");
// this is not a websocket. redirect to a page for this user
Ok(Redirect::to(&redirect_rpc_key_url).into_response())
}
}
// any other combinations get a simple error
_ => Err(FrontendErrorResponse::StatusCode(
StatusCode::BAD_REQUEST,
"this page is for rpcs".to_string(),
None,
)),
2022-10-18 00:47:58 +03:00
}
2022-08-11 03:16:13 +03:00
}
}
2022-05-29 20:28:41 +03:00
}
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
async fn proxy_web3_socket(
app: Arc<Web3ProxyApp>,
authorization: Arc<Authorization>,
socket: WebSocket,
) {
2022-05-29 20:28:41 +03:00
// split the websocket so we can read and write concurrently
let (ws_tx, ws_rx) = socket.split();
// create a channel for our reader and writer can communicate. todo: benchmark different channels
let (response_sender, response_receiver) = flume::unbounded::<Message>();
2022-05-29 20:28:41 +03:00
tokio::spawn(write_web3_socket(response_receiver, ws_tx));
tokio::spawn(read_web3_socket(app, authorization, ws_rx, response_sender));
2022-05-29 20:28:41 +03:00
}
2022-07-25 03:27:00 +03:00
/// websockets support a few more methods than http clients
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-05-31 04:55:04 +03:00
async fn handle_socket_payload(
2022-06-14 10:13:42 +03:00
app: Arc<Web3ProxyApp>,
authorization: &Arc<Authorization>,
2022-05-31 04:55:04 +03:00
payload: &str,
2022-07-09 01:14:45 +03:00
response_sender: &flume::Sender<Message>,
subscription_count: &AtomicUsize,
2022-05-31 04:55:04 +03:00
subscriptions: &mut HashMap<String, AbortHandle>,
) -> Message {
// TODO: do any clients send batches over websockets?
2022-05-31 04:55:04 +03:00
let (id, response) = match serde_json::from_str::<JsonRpcRequest>(payload) {
Ok(json_request) => {
// TODO: should we use this id for the subscription id? it should be unique and means we dont need an atomic
let id = json_request.id.clone();
2022-05-31 04:55:04 +03:00
let response: anyhow::Result<JsonRpcForwardedResponseEnum> = match &json_request.method
[..]
{
"eth_subscribe" => {
// TODO: what should go in this span?
let span = error_span!("eth_subscribe");
2022-06-14 10:13:42 +03:00
let response = app
.eth_subscribe(
authorization.clone(),
json_request,
subscription_count,
response_sender.clone(),
)
.instrument(span)
2022-06-14 10:13:42 +03:00
.await;
2022-05-31 04:55:04 +03:00
match response {
Ok((handle, response)) => {
// TODO: better key
2022-09-30 07:18:18 +03:00
subscriptions.insert(
response
.result
.as_ref()
// TODO: what if there is an error?
.expect("response should always have a result, not an error")
.to_string(),
handle,
);
2022-05-31 04:55:04 +03:00
Ok(response.into())
}
Err(err) => Err(err),
2022-05-31 04:55:04 +03:00
}
}
"eth_unsubscribe" => {
// TODO: how should handle rate limits and stats on this?
2022-09-30 07:18:18 +03:00
// TODO: handle invalid params
let subscription_id = json_request.params.unwrap().to_string();
2022-05-31 04:55:04 +03:00
let partial_response = match subscriptions.remove(&subscription_id) {
None => false,
Some(handle) => {
handle.abort();
true
}
};
2022-05-31 04:55:04 +03:00
2022-07-22 22:30:39 +03:00
let response =
JsonRpcForwardedResponse::from_value(json!(partial_response), id.clone());
2022-05-31 04:55:04 +03:00
Ok(response.into())
}
_ => {
app.proxy_web3_rpc(authorization.clone(), json_request.into())
.await
}
2022-05-31 04:55:04 +03:00
};
(id, response)
}
Err(err) => {
2022-09-30 07:18:18 +03:00
let id = RawValue::from_string("null".to_string()).expect("null can always be a value");
2022-05-31 04:55:04 +03:00
(id, Err(err.into()))
}
};
let response_str = match response {
Ok(x) => serde_json::to_string(&x),
Err(err) => {
2022-09-30 07:18:18 +03:00
// we have an anyhow error. turn it into a response
let response = JsonRpcForwardedResponse::from_anyhow_error(err, None, Some(id));
2022-05-31 04:55:04 +03:00
serde_json::to_string(&response)
}
}
2022-09-30 07:18:18 +03:00
// TODO: what error should this be?
2022-05-31 04:55:04 +03:00
.unwrap();
Message::Text(response_str)
}
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-05-29 20:28:41 +03:00
async fn read_web3_socket(
2022-07-07 06:22:09 +03:00
app: Arc<Web3ProxyApp>,
authorization: Arc<Authorization>,
2022-05-29 20:28:41 +03:00
mut ws_rx: SplitStream<WebSocket>,
2022-07-09 01:14:45 +03:00
response_sender: flume::Sender<Message>,
2022-05-29 20:28:41 +03:00
) {
let mut subscriptions = HashMap::new();
2022-07-09 01:14:45 +03:00
let subscription_count = AtomicUsize::new(1);
2022-05-29 20:28:41 +03:00
while let Some(Ok(msg)) = ws_rx.next().await {
// new message from our client. forward to a backend and then send it through response_tx
let response_msg = match msg {
Message::Text(payload) => {
2022-07-09 01:14:45 +03:00
handle_socket_payload(
app.clone(),
&authorization,
2022-07-09 01:14:45 +03:00
&payload,
&response_sender,
&subscription_count,
&mut subscriptions,
)
.await
2022-05-29 20:28:41 +03:00
}
Message::Ping(x) => Message::Pong(x),
2022-05-31 04:55:04 +03:00
Message::Pong(x) => {
2022-07-07 06:22:09 +03:00
trace!("pong: {:?}", x);
2022-05-31 04:55:04 +03:00
continue;
}
Message::Close(_) => {
info!("closing websocket connection");
break;
}
Message::Binary(mut payload) => {
2022-08-11 03:16:13 +03:00
// TODO: poke rate limit for the user/ip
2022-05-31 04:55:04 +03:00
let payload = from_utf8_mut(&mut payload).unwrap();
2022-07-09 01:14:45 +03:00
handle_socket_payload(
app.clone(),
&authorization,
2022-07-09 01:14:45 +03:00
payload,
&response_sender,
&subscription_count,
&mut subscriptions,
)
.await
2022-05-31 04:55:04 +03:00
}
2022-05-29 20:28:41 +03:00
};
2022-07-09 01:14:45 +03:00
match response_sender.send_async(response_msg).await {
2022-05-30 04:28:22 +03:00
Ok(_) => {}
Err(err) => {
error!("{}", err);
break;
}
2022-05-29 20:28:41 +03:00
};
}
}
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-05-29 20:28:41 +03:00
async fn write_web3_socket(
response_rx: flume::Receiver<Message>,
mut ws_tx: SplitSink<WebSocket, Message>,
) {
2022-07-09 01:14:45 +03:00
// TODO: increment counter for open websockets
2022-05-29 20:28:41 +03:00
while let Ok(msg) = response_rx.recv_async().await {
2022-08-11 03:16:13 +03:00
// a response is ready
// TODO: poke rate limits for this user?
// forward the response to through the websocket
2022-06-16 05:53:37 +03:00
if let Err(err) = ws_tx.send(msg).await {
2022-07-09 01:14:45 +03:00
// this isn't a problem. this is common and happens whenever a client disconnects
trace!(?err, "unable to write to websocket");
2022-05-29 20:28:41 +03:00
break;
};
}
2022-07-09 01:14:45 +03:00
// TODO: decrement counter for open websockets
2022-05-29 20:28:41 +03:00
}