diff --git a/src/main.rs b/src/main.rs index 1a9adae..13fbcbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,19 +15,22 @@ use axum::{ Router, }; use std::{net::SocketAddr, sync::Arc, time::Duration}; -use tower_http::{ - services::ServeDir, - trace::TraceLayer, -}; +use tower_http::{services::ServeDir, trace::TraceLayer}; use tracing::{error, info, Level}; use tracing_subscriber::FmtSubscriber; -use crate::proxy::{handle_rpc, ProxyState}; +use crate::mev::mev_handler::{handle_flashbots_with_mev, MevProxyState}; +use crate::proxy::{handle_inbound, handle_rpc, InboundState, ProxyState}; use crate::rate_limit::{rate_limit_middleware, RateLimitConfig, RateLimiter}; -use crate::security::{build_security_layers, security_headers_middleware, SecurityConfig, health_check, security_metrics, monitor_request_patterns}; +use crate::security::{ + build_security_layers, health_check, monitor_request_patterns, security_headers_middleware, + security_metrics, SecurityConfig, +}; use crate::tor::TorService; -use crate::mev::mev_handler::{handle_flashbots_with_mev, MevProxyState}; -use crate::mev::mev_client_impl::{MevConfig, create_mev_client}; +use crate::{ + mev::mev_client_impl::{create_mev_client, MevConfig}, + tor::{Onion, OnionConfig}, +}; #[tokio::main] async fn main() { @@ -36,40 +39,58 @@ async fn main() { .with_max_level(Level::INFO) .with_target(false) .finish(); - - tracing::subscriber::set_global_default(subscriber) - .expect("setting default subscriber failed"); - + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + info!("Starting TorPC proxy server"); - + // Configuration - let geth_url = std::env::var("GETH_URL") - .unwrap_or_else(|_| "http://127.0.0.1:8545".to_string()); + // MOO: not GETH but EL client in general! + let geth_url = + std::env::var("GETH_URL").unwrap_or_else(|_| "http://127.0.0.1:8656".to_string()); // MOO: figure out default addr! let flashbots_url = std::env::var("FLASHBOTS_URL") .unwrap_or_else(|_| "https://relay.flashbots.net".to_string()); - let bind_addr = std::env::var("BIND_ADDR") - .unwrap_or_else(|_| "127.0.0.1:8080".to_string()); - + let outbound_bind_addr = + std::env::var("OUTBOUND_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".to_string()); + + let inbound_bind_addr = + std::env::var("INBOUND_ADDR").unwrap_or_else(|_| "127.0.0.1:8545".to_string()); + + // MOO: how we want to handle case with no onion peers? + let onion_peers: Vec<_> = std::env::var("ONION_PEERS") + .unwrap_or_else(|_| { + "ethereumbbdyhyy33d4f3frmsxm6anm6bdrffvzft4kdk5p43odcvaid.onion:8545".to_string() + }) + .split(',') + .map(|s| s.to_string()) + .collect(); + info!("Geth URL: {}", geth_url); info!("Flashbots URL: {}", flashbots_url); - info!("Bind address: {}", bind_addr); - + info!("Outbound address: {}", outbound_bind_addr); + info!("Inbound address: {}", inbound_bind_addr); + // Create base proxy state let base_state = Arc::new(ProxyState::new(geth_url.clone(), flashbots_url.clone())); - + + let inbound_state = InboundState { + proxy_state: ProxyState::new(geth_url.clone(), flashbots_url.clone()), + onion_peers: Onion::try_new(OnionConfig::default().with_peers(onion_peers)).unwrap(), + }; + // Create MEV-aware state if signing key is configured let (mev_state, _has_mev) = if let Ok(signing_key) = std::env::var("FLASHBOTS_SIGNING_KEY") { // MEV protection is enabled - let relay_url = std::env::var("FLASHBOTS_RELAY_URL") - .unwrap_or_else(|_| flashbots_url.clone()); - + let relay_url = + std::env::var("FLASHBOTS_RELAY_URL").unwrap_or_else(|_| flashbots_url.clone()); + let mev_config = MevConfig { relay_url: relay_url.clone(), signing_key, request_timeout: Duration::from_secs(5), blocks_ahead: 1, }; - + match create_mev_client(mev_config) { Ok(mev_client) => { info!("MEV protection enabled with relay: {}", relay_url); @@ -97,14 +118,14 @@ async fn main() { }); (mev_proxy_state, false) }; - + // Create rate limiter let rate_limit_config = RateLimitConfig { max_requests: 100, window_duration: Duration::from_secs(60), // 100 requests per minute }; let rate_limiter = Arc::new(RateLimiter::new(rate_limit_config)); - + // Spawn cleanup task for rate limiter let cleanup_limiter = rate_limiter.clone(); tokio::spawn(async move { @@ -114,39 +135,34 @@ async fn main() { cleanup_limiter.cleanup().await; } }); - + // Load security configuration let security_config = SecurityConfig::from_env(); - info!("Security config: max_body_size={}KB, timeout={}s, strict_headers={}", + info!( + "Security config: max_body_size={}KB, timeout={}s, strict_headers={}", security_config.max_body_size / 1024, security_config.request_timeout.as_secs(), security_config.strict_headers ); - - // Build router - always use MEV state for consistency - let app = Router::new() + + // accepts connections from TOR network + let outbound_server = Router::new() // Health and monitoring endpoints (no rate limiting) .route("/health", get(health_check)) .route("/metrics", get(security_metrics)) // RPC endpoints with rate limiting - .route("/rpc", post({ - move |axum::extract::State(s): axum::extract::State>, req| async move { - handle_rpc(axum::extract::State(s.base_state.clone()), req).await - } - })) - .route("/rpc/", post({ + .route("/", post({ move |axum::extract::State(s): axum::extract::State>, req| async move { handle_rpc(axum::extract::State(s.base_state.clone()), req).await } })) - .route("/rpc/flashbots", post(handle_flashbots_with_mev)) - .route("/rpc/flashbots/", post(handle_flashbots_with_mev)) + .route("/flashbots", post(handle_flashbots_with_mev)) + .route("/flashbots/", post(handle_flashbots_with_mev)) .route_layer(middleware::from_fn_with_state( rate_limiter.clone(), rate_limit_middleware, )) // Static file serving (no rate limiting) - .nest_service("/", ServeDir::new("static")) // Add MEV state .with_state(mev_state) // Add request body limit @@ -159,14 +175,39 @@ async fn main() { .layer(middleware::from_fn(security_headers_middleware)) // Add tracing .layer(TraceLayer::new_for_http()); - + + // accepts connections from local node and forwards them into TOR network + let inbound_server = Router::new() + // RPC endpoints with rate limiting + .route("/", post({ + move |axum::extract::State(s): axum::extract::State>, req| async move { + handle_inbound(axum::extract::State(s), req).await + } + })) + .route_layer(middleware::from_fn_with_state( + rate_limiter.clone(), + rate_limit_middleware, + )) + // Add inbound + .with_state(inbound_state.into()) + // Add request body limit + .layer(DefaultBodyLimit::max(security_config.max_body_size)) + // Add request pattern monitoring + .layer(middleware::from_fn(monitor_request_patterns)) + // Add security layers (timeouts) + .layer(build_security_layers(security_config.clone())) + // Add security headers middleware + .layer(middleware::from_fn(security_headers_middleware)) + // Add tracing + .layer(TraceLayer::new_for_http()); + // Parse bind address - let addr: SocketAddr = bind_addr.parse() - .expect("Invalid bind address"); - - info!("Server listening on {}", addr); - info!("Access the web interface at http://{}", addr); - + let outbound_addr: SocketAddr = outbound_bind_addr.parse().expect("Invalid bind address"); + let inbound_addr: SocketAddr = inbound_bind_addr.parse().expect("Invalid bind address"); + + info!("Server listening on {}", outbound_addr); + info!("Server listening on {}", inbound_addr); + // Check Tor status let tor_service = TorService::new(); if let Err(e) = tor_service.check_configuration() { @@ -188,15 +229,34 @@ async fn main() { } } } - + // Run server - let listener = tokio::net::TcpListener::bind(addr) + let outbound_listener = tokio::net::TcpListener::bind(outbound_addr) .await .expect("Failed to bind to address"); - - axum::serve(listener, app) + + let inbound_listener = tokio::net::TcpListener::bind(inbound_addr) .await - .expect("Server failed"); + .expect("Failed to bind to address"); + + // Spawn tasks to serve each application concurrently + let res = tokio::join!( + axum::serve(outbound_listener, outbound_server), + axum::serve(inbound_listener, inbound_server) + ); + + match res { + (Ok(_), Ok(_)) => {} + (Err(e1), Ok(_)) => { + println!("{}", e1) + } + (Ok(_), Err(e2)) => { + println!("{}", e2) + } + (Err(e1), Err(e2)) => { + println!("{}, {}", e1, e2) + } + } } #[cfg(test)] @@ -211,22 +271,23 @@ mod tests { server_url.clone(), format!("{}/flashbots", server_url), )); - + let mev_state = Arc::new(MevProxyState { base_state: base_state.clone(), mev_client: None, }); - + Router::new() - .route("/rpc", post({ - let base_state = base_state.clone(); - move |axum::extract::State(_): axum::extract::State>, req| { - let state = base_state.clone(); - async move { - handle_rpc(axum::extract::State(state), req).await + .route( + "/rpc", + post({ + let base_state = base_state.clone(); + move |axum::extract::State(_): axum::extract::State>, req| { + let state = base_state.clone(); + async move { handle_rpc(axum::extract::State(state), req).await } } - } - })) + }), + ) .route("/rpc/flashbots", post(handle_flashbots_with_mev)) .with_state(mev_state) } @@ -236,7 +297,7 @@ mod tests { let mock_server = mockito::Server::new_async().await; let app = create_test_app(mock_server.url()).await; let server = TestServer::new(app).unwrap(); - + // Test RPC endpoint exists let response = server .post("/rpc") @@ -246,9 +307,9 @@ mod tests { "id": 1 })) .await; - + assert_eq!(response.status_code(), StatusCode::METHOD_NOT_ALLOWED); - + // Test Flashbots endpoint exists let response = server .post("/rpc/flashbots") @@ -258,7 +319,7 @@ mod tests { "id": 1 })) .await; - + assert_eq!(response.status_code(), StatusCode::METHOD_NOT_ALLOWED); } } diff --git a/src/proxy.rs b/src/proxy.rs index 317fc6e..a526300 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -5,13 +5,13 @@ use std::sync::Arc; use tracing::{debug, error, info, warn}; use crate::{ + tor::Onion, error::{ProxyError, ProxyResult}, - rpc_types::{JsonRpcRequest, JsonRpcResponse, JsonRpcError}, - whitelist::is_method_allowed, + rpc_types::{JsonRpcError, JsonRpcRequest, JsonRpcResponse}, security::{SecurityEvent, SecurityEventType}, + whitelist::{is_method_allowed, is_send_method}, }; - #[derive(Clone)] pub struct ProxyState { pub geth_client: Client, @@ -25,7 +25,7 @@ impl ProxyState { .timeout(std::time::Duration::from_secs(30)) .build() .expect("Failed to create HTTP client"); - + Self { geth_client, geth_url, @@ -40,28 +40,72 @@ pub async fn handle_rpc( Json(request): Json, ) -> ProxyResult> { debug!("Received RPC request: method={}", request.method); - + // Validate request - request.validate() + request + .validate() .map_err(|e| ProxyError::InvalidRequest(e.to_string()))?; - + // Check if method is allowed if !is_method_allowed(&request.method) { warn!("Blocked disallowed method: {}", request.method); - + // Log security event let event = SecurityEvent::new( SecurityEventType::BlockedMethod, - format!("Blocked disallowed method: {}", request.method) - ).with_method(request.method.clone()); + format!("Blocked disallowed method: {}", request.method), + ) + .with_method(request.method.clone()); event.log(); - + return Err(ProxyError::MethodNotAllowed(request.method.clone())); } - + // Forward to Geth let response = proxy_to_geth(&state, request).await?; - + + Ok(Json(response)) +} + +pub struct InboundState { + pub proxy_state: ProxyState, + pub onion_peers: Onion, +} + +/// Handle RPC requests to the standard endpoint +pub async fn handle_inbound( + State(state): State>, + Json(request): Json, +) -> ProxyResult> { + info!("Received RPC request: method={}", request.method); + + // Validate request + request + .validate() + .map_err(|e| ProxyError::InvalidRequest(e.to_string()))?; + + // Check if method is allowed + if !is_method_allowed(&request.method) { + warn!("Blocked disallowed method: {}", request.method); + + // Log security event + let event = SecurityEvent::new( + SecurityEventType::BlockedMethod, + format!("Blocked disallowed method: {}", request.method), + ) + .with_method(request.method.clone()); + event.log(); + + return Err(ProxyError::MethodNotAllowed(request.method.clone())); + } + + if is_send_method(&request.method) { + return Ok(Json(state.onion_peers.send_request(&request, 3).await?)); + } + + // Forward to Geth + let response = proxy_to_geth(&state.proxy_state, request).await?; + Ok(Json(response)) } @@ -71,25 +115,27 @@ pub async fn handle_flashbots( Json(request): Json, ) -> ProxyResult> { debug!("Received Flashbots RPC request: method={}", request.method); - + // Validate request - request.validate() + request + .validate() .map_err(|e| ProxyError::InvalidRequest(e.to_string()))?; - + // Check if method is allowed if !is_method_allowed(&request.method) { warn!("Blocked disallowed method: {}", request.method); - + // Log security event let event = SecurityEvent::new( SecurityEventType::BlockedMethod, - format!("Blocked disallowed method: {}", request.method) - ).with_method(request.method.clone()); + format!("Blocked disallowed method: {}", request.method), + ) + .with_method(request.method.clone()); event.log(); - + return Err(ProxyError::MethodNotAllowed(request.method.clone())); } - + // Route based on method let response = match request.method.as_str() { "eth_sendRawTransaction" | "eth_sendBundle" => { @@ -101,7 +147,7 @@ pub async fn handle_flashbots( proxy_to_geth(&state, request).await? } }; - + Ok(Json(response)) } @@ -110,7 +156,8 @@ pub async fn proxy_to_geth( state: &ProxyState, request: JsonRpcRequest, ) -> ProxyResult { - let response = state.geth_client + let response = state + .geth_client .post(&state.geth_url) .json(&request) .send() @@ -119,7 +166,7 @@ pub async fn proxy_to_geth( error!("Failed to send request to Geth: {}", e); ProxyError::UpstreamError(format!("Geth connection failed: {}", e)) })?; - + if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); @@ -129,13 +176,12 @@ pub async fn proxy_to_geth( status, body ))); } - - let json_response: JsonRpcResponse = response.json().await - .map_err(|e| { - error!("Failed to parse Geth response: {}", e); - ProxyError::UpstreamError(format!("Failed to parse response: {}", e)) - })?; - + + let json_response: JsonRpcResponse = response.json().await.map_err(|e| { + error!("Failed to parse Geth response: {}", e); + ProxyError::UpstreamError(format!("Failed to parse response: {}", e)) + })?; + Ok(json_response) } @@ -147,11 +193,11 @@ async fn proxy_to_flashbots( // Without MEV protection configured, route flashbots requests to local Geth // This allows testing without requiring actual Flashbots authentication warn!("MEV protection not configured, routing flashbots requests to local Geth"); - + // For bundle requests, we'll simulate a response if request.method == "eth_sendBundle" { info!("Simulating bundle submission for testing"); - + // Validate bundle parameters if let Some(params) = &request.params { if let Some(arr) = params.as_array() { @@ -174,7 +220,7 @@ async fn proxy_to_flashbots( } } } - + return Ok(JsonRpcResponse { jsonrpc: "2.0".to_string(), result: Some(serde_json::json!({ @@ -184,7 +230,7 @@ async fn proxy_to_flashbots( id: request.id, }); } - + // For other requests, forward to local Geth proxy_to_geth(state, request).await } @@ -196,33 +242,31 @@ mod tests { use serde_json::json; fn create_test_state(server_url: String) -> ProxyState { - ProxyState::new( - server_url.clone(), - format!("{}/flashbots", server_url), - ) + ProxyState::new(server_url.clone(), format!("{}/flashbots", server_url)) } #[tokio::test] async fn test_handle_rpc_valid_request() { let mut server = Server::new_async().await; - let _m = server.mock("POST", "/") + let _m = server + .mock("POST", "/") .with_status(200) .with_header("content-type", "application/json") .with_body(r#"{"jsonrpc":"2.0","result":"0x123","id":1}"#) .create(); - + let state = Arc::new(create_test_state(server.url())); - + let request = JsonRpcRequest { jsonrpc: "2.0".to_string(), method: "eth_blockNumber".to_string(), params: None, id: Some(json!(1)), }; - + let result = handle_rpc(State(state), Json(request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert_eq!(response.result, Some(json!("0x123"))); } @@ -231,17 +275,17 @@ mod tests { async fn test_handle_rpc_blocked_method() { let server = Server::new_async().await; let state = Arc::new(create_test_state(server.url())); - + let request = JsonRpcRequest { jsonrpc: "2.0".to_string(), method: "eth_accounts".to_string(), params: None, id: Some(json!(1)), }; - + let result = handle_rpc(State(state), Json(request)).await; assert!(result.is_err()); - + match result.unwrap_err() { ProxyError::MethodNotAllowed(method) => { assert_eq!(method, "eth_accounts"); @@ -253,24 +297,25 @@ mod tests { #[tokio::test] async fn test_handle_flashbots_transaction_routing() { let mut server = Server::new_async().await; - let _m = server.mock("POST", "/flashbots") + let _m = server + .mock("POST", "/flashbots") .with_status(200) .with_header("content-type", "application/json") .with_body(r#"{"jsonrpc":"2.0","result":"0xhash","id":1}"#) .create(); - + let state = Arc::new(create_test_state(server.url())); - + let request = JsonRpcRequest { jsonrpc: "2.0".to_string(), method: "eth_sendRawTransaction".to_string(), params: Some(json!(["0xrawtx"])), id: Some(json!(1)), }; - + let result = handle_flashbots(State(state), Json(request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert_eq!(response.result, Some(json!("0xhash"))); } @@ -279,17 +324,17 @@ mod tests { async fn test_invalid_json_rpc_version() { let server = Server::new_async().await; let state = Arc::new(create_test_state(server.url())); - + let request = JsonRpcRequest { jsonrpc: "1.0".to_string(), method: "eth_blockNumber".to_string(), params: None, id: Some(json!(1)), }; - + let result = handle_rpc(State(state), Json(request)).await; assert!(result.is_err()); - + match result.unwrap_err() { ProxyError::InvalidRequest(msg) => { assert!(msg.contains("jsonrpc version")); @@ -297,4 +342,4 @@ mod tests { _ => panic!("Expected InvalidRequest error"), } } -} \ No newline at end of file +} diff --git a/src/tor.rs b/src/tor.rs index cce8d18..f89390c 100644 --- a/src/tor.rs +++ b/src/tor.rs @@ -3,6 +3,17 @@ use std::path::Path; use anyhow::{Context, Result}; use tracing::{info, warn}; +use reqwest::{Client, Proxy, Url}; +use std::{ + sync::atomic::{AtomicU32, Ordering}, + time::Duration, +}; + +use crate::{ + error::ProxyError, + rpc_types::{JsonRpcRequest, JsonRpcResponse}, +}; + /// Tor service configuration and utilities pub struct TorService { pub hostname_path: String, @@ -94,6 +105,170 @@ impl Default for TorService { } } +pub struct TorJsonRpcClient { + client: Client, + endpoint: Url, +} + +impl TorJsonRpcClient { + fn new( + endpoint: String, + tor_proxy: String, + dial_timeout: Duration, + keep_alive: Duration, + request_timeout: Duration, + idle_conn_timeout: Duration, + max_idle_conns: usize, + ) -> Result> { + let endpoint = Url::parse(format!("http://{}", endpoint).as_str())?; + let proxy = Proxy::http(format!("socks5h://{}", tor_proxy))?; + + let client = Client::builder() + .proxy(proxy) + .timeout(request_timeout) + .connect_timeout(dial_timeout) + .pool_idle_timeout(idle_conn_timeout) + .pool_max_idle_per_host(max_idle_conns) + .tcp_keepalive(Some(keep_alive)) + .build()?; + + Ok(Self { client, endpoint }) + } + async fn send_request(&self, request: &JsonRpcRequest) -> Result { + let response = self + .client + .post(self.endpoint.as_str()) + .header("Content-Type", "application/json") + .json(&request) + .send() + .await?; + + let rpc_response: JsonRpcResponse = response.json().await?; + + if let Some(error) = rpc_response.error { + return Err(ProxyError::UpstreamError(format!( + "JSON-RPC request through TOR returned error: {}", + error.message + ))); + } + + return Ok(rpc_response); + } +} + +pub struct Onion { + peers: Vec, + next_peer: AtomicU32, +} + +#[derive(Debug)] +pub struct OnionConfig { + pub tor_proxy: String, + pub addresses: Vec, + pub dial_timeout: Duration, + pub keep_alive: Duration, + pub request_timeout: Duration, + pub idle_conn_timeout: Duration, + pub max_idle_conns: usize, +} + +impl OnionConfig { + pub fn default() -> Self { + Self { + tor_proxy: "127.0.0.1:9050".to_string(), + addresses: vec![], + dial_timeout: Duration::from_secs(60), + keep_alive: Duration::from_secs(60), + request_timeout: Duration::from_secs(60), + idle_conn_timeout: Duration::from_secs(60), + max_idle_conns: 0usize, + } + } + + pub fn with_peers(self, addresses: Vec) -> Self { + Self { + tor_proxy: self.tor_proxy, + addresses, + dial_timeout: self.dial_timeout, + keep_alive: self.keep_alive, + request_timeout: self.request_timeout, + idle_conn_timeout: self.idle_conn_timeout, + max_idle_conns: self.max_idle_conns, + } + } +} + +impl Onion { + pub fn try_new(config: OnionConfig) -> Result { + let OnionConfig { + addresses, + tor_proxy, + dial_timeout, + keep_alive, + request_timeout, + idle_conn_timeout, + max_idle_conns, + } = config; + let peers: Vec = addresses + .into_iter() + .filter_map(|addr| { + TorJsonRpcClient::new( + addr, + tor_proxy.clone(), + dial_timeout, + keep_alive, + request_timeout, + idle_conn_timeout, + max_idle_conns, + ) + .ok() + }) + .collect(); + + if peers.is_empty() { + // TODO: forward last error + return Err("no valid peers!".to_string()); + } + + Ok(Self { + peers, + next_peer: AtomicU32::new(0), + }) + } + pub async fn send_request( + &self, + request: &JsonRpcRequest, + retries: usize, + ) -> Result { + for attempt in 0..retries { + // plain and simple RR (for now) + let peer_id = self.next_peer.fetch_add(1, Ordering::SeqCst) as usize % self.peers.len(); + let peer = &self.peers[peer_id]; + + let result = peer.send_request(request).await; + + match result { + Ok(hash) => { + return Ok(hash); + } + Err(err) => { + warn!("Failed to send tx to {} ({})", peer.endpoint, err); + } + } + + if attempt + 1 < retries { + tokio::time::sleep(Duration::from_secs(attempt as u64 + 1)).await; + } + } + + return Err(ProxyError::UpstreamError(format!( + "failed to send tx through TOR after {} retries", + retries + ))); + } +} + + #[cfg(test)] mod tests { use super::*; diff --git a/src/whitelist.rs b/src/whitelist.rs index 74b6b6c..8e4f9a5 100644 --- a/src/whitelist.rs +++ b/src/whitelist.rs @@ -49,11 +49,30 @@ static ALLOWED_METHODS: Lazy> = Lazy::new(|| { methods }); +/// List of send RPC methods +static SEND_METHODS: Lazy> = Lazy::new(|| { + let mut methods = HashSet::new(); + + // Write methods we allow + methods.insert("eth_sendRawTransaction"); + + // MOO: do we want to handle other send methods ? + + methods +}); + /// Check if a method is allowed pub fn is_method_allowed(method: &str) -> bool { ALLOWED_METHODS.contains(method) } +/// Check if a method is send tx method +/// MOO: move this to other module? +pub fn is_send_method(method: &str) -> bool { + SEND_METHODS.contains(method) +} + + /// Get a list of all allowed methods (for documentation) pub fn get_allowed_methods() -> Vec<&'static str> { let mut methods: Vec<_> = ALLOWED_METHODS.iter().copied().collect();