diff --git a/Cargo.lock b/Cargo.lock index a2b1405..cc4b016 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -698,6 +698,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" +[[package]] +name = "auth-server" +version = "0.1.0" +dependencies = [ + "bytes", + "clap", + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "reqwest 0.11.27", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "warp", +] + [[package]] name = "auto_impl" version = "1.2.0" @@ -6170,10 +6188,12 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", + "tokio-util 0.7.12", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", "winreg", @@ -8376,6 +8396,19 @@ version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +[[package]] +name = "wasm-streams" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.70" diff --git a/Cargo.toml b/Cargo.toml index 170ba03..0914efb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "funds-manager/funds-manager-api", "funds-manager/funds-manager-server", "price-reporter", + "auth-server", ] [profile.bench] diff --git a/auth-server/Cargo.toml b/auth-server/Cargo.toml new file mode 100644 index 0000000..e148d08 --- /dev/null +++ b/auth-server/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "auth-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +# === HTTP Server === # +clap = { version = "4.0", features = ["derive", "env"] } +http = "0.2" +hyper = { version = "0.14", features = ["full"] } +reqwest = { version = "0.11", features = ["json", "stream"] } +tokio = { version = "1", features = ["full"] } +warp = "0.3" + +# === Misc Dependencies === # +bytes = "1.0" +futures-util = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +tracing = "0.1" diff --git a/auth-server/src/main.rs b/auth-server/src/main.rs new file mode 100644 index 0000000..8346b68 --- /dev/null +++ b/auth-server/src/main.rs @@ -0,0 +1,156 @@ +//! The relayer authentication server +//! +//! This server is run independently of the relayer and is responsible for +//! issuing and managing API keys that provide access to the relayer's API. +//! +//! As such, the server holds the relayer admin key, and proxies authenticated +//! requests to the relayer directly +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] +#![deny(unsafe_code)] +#![deny(clippy::needless_pass_by_ref_mut)] +#![feature(trivial_bounds)] + +use bytes::Bytes; +use clap::Parser; +use reqwest::{Client, Method, StatusCode}; +use serde_json::json; +use std::net::SocketAddr; +use thiserror::Error; +use tracing::{error, info}; +use warp::{Filter, Rejection, Reply}; + +// ------- +// | CLI | +// ------- + +/// The command line arguments for the auth server +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// The URL of the relayer + #[arg(long, env = "RELAYER_URL")] + relayer_url: String, + /// The admin key for the relayer + #[arg(long, env = "RELAYER_ADMIN_KEY")] + relayer_admin_key: String, + /// The port to run the server on + #[arg(long, env = "PORT", default_value = "3030")] + port: u16, +} + +// ------------- +// | Api Types | +// ------------- + +/// Custom error type for API errors +#[derive(Error, Debug)] +pub enum ApiError { + /// An internal server error + #[error("Internal server error: {0}")] + InternalError(String), + /// A bad request error + #[error("Bad request: {0}")] + BadRequest(String), +} + +// Implement warp::reject::Reject for ApiError +impl warp::reject::Reject for ApiError {} + +// ---------- +// | Server | +// ---------- + +/// The main function for the auth server +#[tokio::main] +async fn main() { + let args = Args::parse(); + let listen_addr: SocketAddr = ([0, 0, 0, 0], args.port).into(); + + // --- Routes --- // + + // Ping route + let ping = warp::path("ping") + .and(warp::get()) + .map(|| warp::reply::with_status("PONG", StatusCode::OK)); + + // Proxy route + let proxy = warp::path::full() + .and(warp::method()) + .and(warp::header::headers_cloned()) + .and(warp::body::bytes()) + .and(warp::any().map(move || args.relayer_url.clone())) + .and(warp::any().map(move || args.relayer_admin_key.clone())) + .and_then(handle_request); + + // Bind the server and listen + info!("Starting auth server on port {}", args.port); + let routes = ping.or(proxy).recover(handle_rejection); + warp::serve(routes).bind(listen_addr).await; +} + +/// Handle a request to the relayer +async fn handle_request( + path: warp::path::FullPath, + method: Method, + headers: warp::hyper::HeaderMap, + body: Bytes, + relayer_url: String, + relayer_admin_key: String, +) -> Result { + let client = Client::new(); + let url = format!("{}{}", relayer_url, path.as_str()); + + let mut req = client.request(method, &url).headers(headers).body(body); + req = req.header("X-Admin-Key", &relayer_admin_key); + + match req.send().await { + Ok(resp) => { + let status = resp.status(); + let headers = resp.headers().clone(); + let body = resp.bytes().await.map_err(|e| { + warp::reject::custom(ApiError::InternalError(format!( + "Failed to read response body: {}", + e + ))) + })?; + + let mut response = warp::http::Response::new(body); + *response.status_mut() = status; + *response.headers_mut() = headers; + + Ok(response) + }, + Err(e) => { + error!("Error proxying request: {}", e); + Err(warp::reject::custom(ApiError::InternalError(e.to_string()))) + }, + } +} + +/// Handle a rejection from an endpoint handler +async fn handle_rejection(err: Rejection) -> Result { + if let Some(api_error) = err.find::() { + let (code, message) = match api_error { + ApiError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), + ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), + }; + + Ok(json_error(message, code)) + } else if err.is_not_found() { + Ok(json_error("Not Found", StatusCode::NOT_FOUND)) + } else { + error!("unhandled rejection: {:?}", err); + Ok(json_error("Internal Server Error", StatusCode::INTERNAL_SERVER_ERROR)) + } +} + +// ----------- +// | Helpers | +// ----------- + +/// Return a json error from a string message +fn json_error(msg: &str, code: StatusCode) -> impl Reply { + let json = json!({ "error": msg }); + warp::reply::with_status(warp::reply::json(&json), code) +}