From f6f633f521d4d2ef5def6cd6fad739edebfddcbc Mon Sep 17 00:00:00 2001 From: Adam Szkoda Date: Wed, 8 Jul 2020 16:22:32 +0200 Subject: [PATCH 1/2] Add ability to configure CORS header --- Cargo.lock | 1 + beacon_node/Cargo.toml | 1 + beacon_node/rest_api/src/config.rs | 4 + beacon_node/rest_api/src/error.rs | 8 + beacon_node/rest_api/src/lib.rs | 3 + beacon_node/rest_api/src/router.rs | 348 +++++++++++++++-------------- beacon_node/src/cli.rs | 8 + beacon_node/src/config.rs | 10 + lighthouse/src/main.rs | 1 - 9 files changed, 221 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 821b6e2413a..4c8cad0c505 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -396,6 +396,7 @@ dependencies = [ "exit-future", "futures 0.3.5", "genesis", + "hyper 0.13.6", "logging", "node_test_rig", "rand 0.7.3", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index ca5ad055383..b9f8e8f3601 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -39,3 +39,4 @@ eth2_libp2p = { path = "./eth2_libp2p" } eth2_ssz = "0.1.2" serde = "1.0.110" clap_utils = { path = "../common/clap_utils" } +hyper = "0.13.5" diff --git a/beacon_node/rest_api/src/config.rs b/beacon_node/rest_api/src/config.rs index a491ae8bfa3..815fccfd01c 100644 --- a/beacon_node/rest_api/src/config.rs +++ b/beacon_node/rest_api/src/config.rs @@ -38,6 +38,9 @@ pub struct Config { pub listen_address: Ipv4Addr, /// The port the REST API HTTP server will listen on. pub port: u16, + /// If something else than "", a 'Access-Control-Allow-Origin' header will be present in + /// responses. Put *, to allow any origin. + pub allow_origin: String, } impl Default for Config { @@ -46,6 +49,7 @@ impl Default for Config { enabled: false, listen_address: Ipv4Addr::new(127, 0, 0, 1), port: 5052, + allow_origin: "".to_string(), } } } diff --git a/beacon_node/rest_api/src/error.rs b/beacon_node/rest_api/src/error.rs index b6ce5182d3f..1eac8d4a468 100644 --- a/beacon_node/rest_api/src/error.rs +++ b/beacon_node/rest_api/src/error.rs @@ -11,6 +11,7 @@ pub enum ApiError { UnsupportedType(String), ImATeapot(String), // Just in case. ProcessingError(String), // A 202 error, for when a block/attestation cannot be processed, but still transmitted. + InvalidHeaderValue(String), } pub type ApiResult = Result, ApiError>; @@ -26,6 +27,7 @@ impl ApiError { ApiError::UnsupportedType(desc) => (StatusCode::UNSUPPORTED_MEDIA_TYPE, desc), ApiError::ImATeapot(desc) => (StatusCode::IM_A_TEAPOT, desc), ApiError::ProcessingError(desc) => (StatusCode::ACCEPTED, desc), + ApiError::InvalidHeaderValue(desc) => (StatusCode::INTERNAL_SERVER_ERROR, desc), } } } @@ -77,6 +79,12 @@ impl From for ApiError { } } +impl From for ApiError { + fn from(e: hyper::header::InvalidHeaderValue) -> ApiError { + ApiError::InvalidHeaderValue(format!("Invalid CORS header value: {:?}", e)) + } +} + impl StdError for ApiError { fn cause(&self) -> Option<&dyn StdError> { None diff --git a/beacon_node/rest_api/src/lib.rs b/beacon_node/rest_api/src/lib.rs index 00043d7b9be..988a0910c7f 100644 --- a/beacon_node/rest_api/src/lib.rs +++ b/beacon_node/rest_api/src/lib.rs @@ -64,12 +64,14 @@ pub fn start_server( ) -> Result { let log = executor.log(); let inner_log = log.clone(); + let rest_api_config = Arc::new(config.clone()); let eth2_config = Arc::new(eth2_config); // Define the function that will build the request handler. let make_service = make_service_fn(move |_socket: &AddrStream| { let beacon_chain = beacon_chain.clone(); let log = inner_log.clone(); + let rest_api_config = rest_api_config.clone(); let eth2_config = eth2_config.clone(); let network_globals = network_info.network_globals.clone(); let network_channel = network_info.network_chan.clone(); @@ -84,6 +86,7 @@ pub fn start_server( beacon_chain.clone(), network_globals.clone(), network_channel.clone(), + rest_api_config.clone(), eth2_config.clone(), log.clone(), db_path.clone(), diff --git a/beacon_node/rest_api/src/router.rs b/beacon_node/rest_api/src/router.rs index d7e41feab28..a66094db13f 100644 --- a/beacon_node/rest_api/src/router.rs +++ b/beacon_node/rest_api/src/router.rs @@ -1,12 +1,13 @@ use crate::{ - advanced, beacon, consensus, error::ApiError, helpers, lighthouse, metrics, network, node, - spec, validator, NetworkChannel, + advanced, beacon, config::Config, consensus, error::ApiError, helpers, lighthouse, metrics, + network, node, spec, validator, NetworkChannel, }; use beacon_chain::{BeaconChain, BeaconChainTypes}; use bus::Bus; use eth2_config::Eth2Config; use eth2_libp2p::NetworkGlobals; -use hyper::{Body, Error, Method, Request, Response}; +use hyper::header::HeaderValue; +use hyper::{Body, Method, Request, Response}; use parking_lot::Mutex; use slog::debug; use std::path::PathBuf; @@ -21,197 +22,220 @@ pub async fn route( beacon_chain: Arc>, network_globals: Arc>, network_channel: NetworkChannel, + rest_api_config: Arc, eth2_config: Arc, local_log: slog::Logger, db_path: PathBuf, freezer_db_path: PathBuf, events: Arc>>, -) -> Result, Error> { +) -> Result, ApiError> { metrics::inc_counter(&metrics::REQUEST_COUNT); - let timer = metrics::start_timer(&metrics::REQUEST_RESPONSE_TIME); let received_instant = Instant::now(); let path = req.uri().path().to_string(); let log = local_log.clone(); - let request_result = match (req.method(), path.as_ref()) { - // Methods for Client - (&Method::GET, "/node/health") => node::get_health(req), - (&Method::GET, "/node/version") => node::get_version(req), - (&Method::GET, "/node/syncing") => { - // inform the current slot, or set to 0 - let current_slot = beacon_chain - .head_info() - .map(|info| info.slot) - .unwrap_or_else(|_| Slot::from(0u64)); - - node::syncing::(req, network_globals, current_slot) - } - - // Methods for Network - (&Method::GET, "/network/enr") => network::get_enr::(req, network_globals), - (&Method::GET, "/network/peer_count") => network::get_peer_count::(req, network_globals), - (&Method::GET, "/network/peer_id") => network::get_peer_id::(req, network_globals), - (&Method::GET, "/network/peers") => network::get_peer_list::(req, network_globals), - (&Method::GET, "/network/listen_port") => { - network::get_listen_port::(req, network_globals) - } - (&Method::GET, "/network/listen_addresses") => { - network::get_listen_addresses::(req, network_globals) - } - - // Methods for Beacon Node - (&Method::GET, "/beacon/head") => beacon::get_head::(req, beacon_chain), - (&Method::GET, "/beacon/heads") => beacon::get_heads::(req, beacon_chain), - (&Method::GET, "/beacon/block") => beacon::get_block::(req, beacon_chain), - (&Method::GET, "/beacon/block_root") => beacon::get_block_root::(req, beacon_chain), - (&Method::GET, "/beacon/fork") => beacon::get_fork::(req, beacon_chain), - (&Method::GET, "/beacon/fork/stream") => { - let reader = events.lock().add_rx(); - beacon::stream_forks::(log, reader) - } - (&Method::GET, "/beacon/genesis_time") => beacon::get_genesis_time::(req, beacon_chain), - (&Method::GET, "/beacon/genesis_validators_root") => { - beacon::get_genesis_validators_root::(req, beacon_chain) - } - (&Method::GET, "/beacon/validators") => beacon::get_validators::(req, beacon_chain), - (&Method::POST, "/beacon/validators") => { - beacon::post_validators::(req, beacon_chain).await - } - (&Method::GET, "/beacon/validators/all") => { - beacon::get_all_validators::(req, beacon_chain) - } - (&Method::GET, "/beacon/validators/active") => { - beacon::get_active_validators::(req, beacon_chain) - } - (&Method::GET, "/beacon/state") => beacon::get_state::(req, beacon_chain), - (&Method::GET, "/beacon/state_root") => beacon::get_state_root::(req, beacon_chain), - (&Method::GET, "/beacon/state/genesis") => { - beacon::get_genesis_state::(req, beacon_chain) - } - (&Method::GET, "/beacon/committees") => beacon::get_committees::(req, beacon_chain), - (&Method::POST, "/beacon/proposer_slashing") => { - beacon::proposer_slashing::(req, beacon_chain).await - } - (&Method::POST, "/beacon/attester_slashing") => { - beacon::attester_slashing::(req, beacon_chain).await - } - - // Methods for Validator - (&Method::POST, "/validator/duties") => { - let timer = metrics::start_timer(&metrics::VALIDATOR_GET_DUTIES_REQUEST_RESPONSE_TIME); - let response = validator::post_validator_duties::(req, beacon_chain); - drop(timer); - response.await - } - (&Method::POST, "/validator/subscribe") => { - validator::post_validator_subscriptions::(req, network_channel).await - } - (&Method::GET, "/validator/duties/all") => { - validator::get_all_validator_duties::(req, beacon_chain) - } - (&Method::GET, "/validator/duties/active") => { - validator::get_active_validator_duties::(req, beacon_chain) - } - (&Method::GET, "/validator/block") => { - let timer = metrics::start_timer(&metrics::VALIDATOR_GET_BLOCK_REQUEST_RESPONSE_TIME); - let response = validator::get_new_beacon_block::(req, beacon_chain, log); - drop(timer); - response - } - (&Method::POST, "/validator/block") => { - validator::publish_beacon_block::(req, beacon_chain, network_channel, log).await - } - (&Method::GET, "/validator/attestation") => { - let timer = - metrics::start_timer(&metrics::VALIDATOR_GET_ATTESTATION_REQUEST_RESPONSE_TIME); - let response = validator::get_new_attestation::(req, beacon_chain); - drop(timer); - response - } - (&Method::GET, "/validator/aggregate_attestation") => { - validator::get_aggregate_attestation::(req, beacon_chain) - } - (&Method::POST, "/validator/attestations") => { - validator::publish_attestations::(req, beacon_chain, network_channel, log).await - } - (&Method::POST, "/validator/aggregate_and_proofs") => { - validator::publish_aggregate_and_proofs::(req, beacon_chain, network_channel, log) + let result = { + let _timer = metrics::start_timer(&metrics::REQUEST_RESPONSE_TIME); + + match (req.method(), path.as_ref()) { + // Methods for Client + (&Method::GET, "/node/health") => node::get_health(req), + (&Method::GET, "/node/version") => node::get_version(req), + (&Method::GET, "/node/syncing") => { + // inform the current slot, or set to 0 + let current_slot = beacon_chain + .head_info() + .map(|info| info.slot) + .unwrap_or_else(|_| Slot::from(0u64)); + + node::syncing::(req, network_globals, current_slot) + } + + // Methods for Network + (&Method::GET, "/network/enr") => network::get_enr::(req, network_globals), + (&Method::GET, "/network/peer_count") => { + network::get_peer_count::(req, network_globals) + } + (&Method::GET, "/network/peer_id") => network::get_peer_id::(req, network_globals), + (&Method::GET, "/network/peers") => network::get_peer_list::(req, network_globals), + (&Method::GET, "/network/listen_port") => { + network::get_listen_port::(req, network_globals) + } + (&Method::GET, "/network/listen_addresses") => { + network::get_listen_addresses::(req, network_globals) + } + + // Methods for Beacon Node + (&Method::GET, "/beacon/head") => beacon::get_head::(req, beacon_chain), + (&Method::GET, "/beacon/heads") => beacon::get_heads::(req, beacon_chain), + (&Method::GET, "/beacon/block") => beacon::get_block::(req, beacon_chain), + (&Method::GET, "/beacon/block_root") => beacon::get_block_root::(req, beacon_chain), + (&Method::GET, "/beacon/fork") => beacon::get_fork::(req, beacon_chain), + (&Method::GET, "/beacon/fork/stream") => { + let reader = events.lock().add_rx(); + beacon::stream_forks::(log, reader) + } + (&Method::GET, "/beacon/genesis_time") => { + beacon::get_genesis_time::(req, beacon_chain) + } + (&Method::GET, "/beacon/genesis_validators_root") => { + beacon::get_genesis_validators_root::(req, beacon_chain) + } + (&Method::GET, "/beacon/validators") => beacon::get_validators::(req, beacon_chain), + (&Method::POST, "/beacon/validators") => { + beacon::post_validators::(req, beacon_chain).await + } + (&Method::GET, "/beacon/validators/all") => { + beacon::get_all_validators::(req, beacon_chain) + } + (&Method::GET, "/beacon/validators/active") => { + beacon::get_active_validators::(req, beacon_chain) + } + (&Method::GET, "/beacon/state") => beacon::get_state::(req, beacon_chain), + (&Method::GET, "/beacon/state_root") => beacon::get_state_root::(req, beacon_chain), + (&Method::GET, "/beacon/state/genesis") => { + beacon::get_genesis_state::(req, beacon_chain) + } + (&Method::GET, "/beacon/committees") => beacon::get_committees::(req, beacon_chain), + (&Method::POST, "/beacon/proposer_slashing") => { + beacon::proposer_slashing::(req, beacon_chain).await + } + (&Method::POST, "/beacon/attester_slashing") => { + beacon::attester_slashing::(req, beacon_chain).await + } + + // Methods for Validator + (&Method::POST, "/validator/duties") => { + let timer = + metrics::start_timer(&metrics::VALIDATOR_GET_DUTIES_REQUEST_RESPONSE_TIME); + let response = validator::post_validator_duties::(req, beacon_chain); + drop(timer); + response.await + } + (&Method::POST, "/validator/subscribe") => { + validator::post_validator_subscriptions::(req, network_channel).await + } + (&Method::GET, "/validator/duties/all") => { + validator::get_all_validator_duties::(req, beacon_chain) + } + (&Method::GET, "/validator/duties/active") => { + validator::get_active_validator_duties::(req, beacon_chain) + } + (&Method::GET, "/validator/block") => { + let timer = + metrics::start_timer(&metrics::VALIDATOR_GET_BLOCK_REQUEST_RESPONSE_TIME); + let response = validator::get_new_beacon_block::(req, beacon_chain, log); + drop(timer); + response + } + (&Method::POST, "/validator/block") => { + validator::publish_beacon_block::(req, beacon_chain, network_channel, log).await + } + (&Method::GET, "/validator/attestation") => { + let timer = + metrics::start_timer(&metrics::VALIDATOR_GET_ATTESTATION_REQUEST_RESPONSE_TIME); + let response = validator::get_new_attestation::(req, beacon_chain); + drop(timer); + response + } + (&Method::GET, "/validator/aggregate_attestation") => { + validator::get_aggregate_attestation::(req, beacon_chain) + } + (&Method::POST, "/validator/attestations") => { + validator::publish_attestations::(req, beacon_chain, network_channel, log).await + } + (&Method::POST, "/validator/aggregate_and_proofs") => { + validator::publish_aggregate_and_proofs::( + req, + beacon_chain, + network_channel, + log, + ) .await + } + + // Methods for consensus + (&Method::GET, "/consensus/global_votes") => { + consensus::get_vote_count::(req, beacon_chain) + } + (&Method::POST, "/consensus/individual_votes") => { + consensus::post_individual_votes::(req, beacon_chain).await + } + + // Methods for bootstrap and checking configuration + (&Method::GET, "/spec") => spec::get_spec::(req, beacon_chain), + (&Method::GET, "/spec/slots_per_epoch") => spec::get_slots_per_epoch::(req), + (&Method::GET, "/spec/deposit_contract") => { + helpers::implementation_pending_response(req) + } + (&Method::GET, "/spec/eth2_config") => spec::get_eth2_config::(req, eth2_config), + + // Methods for advanced parameters + (&Method::GET, "/advanced/fork_choice") => { + advanced::get_fork_choice::(req, beacon_chain) + } + (&Method::GET, "/advanced/operation_pool") => { + advanced::get_operation_pool::(req, beacon_chain) + } + + (&Method::GET, "/metrics") => { + metrics::get_prometheus::(req, beacon_chain, db_path, freezer_db_path) + } + + // Lighthouse specific + (&Method::GET, "/lighthouse/syncing") => { + lighthouse::syncing::(req, network_globals) + } + + (&Method::GET, "/lighthouse/peers") => { + lighthouse::peers::(req, network_globals) + } + + (&Method::GET, "/lighthouse/connected_peers") => { + lighthouse::connected_peers::(req, network_globals) + } + _ => Err(ApiError::NotFound( + "Request path and/or method not found.".to_owned(), + )), } - - // Methods for consensus - (&Method::GET, "/consensus/global_votes") => { - consensus::get_vote_count::(req, beacon_chain) - } - (&Method::POST, "/consensus/individual_votes") => { - consensus::post_individual_votes::(req, beacon_chain).await - } - - // Methods for bootstrap and checking configuration - (&Method::GET, "/spec") => spec::get_spec::(req, beacon_chain), - (&Method::GET, "/spec/slots_per_epoch") => spec::get_slots_per_epoch::(req), - (&Method::GET, "/spec/deposit_contract") => helpers::implementation_pending_response(req), - (&Method::GET, "/spec/eth2_config") => spec::get_eth2_config::(req, eth2_config), - - // Methods for advanced parameters - (&Method::GET, "/advanced/fork_choice") => { - advanced::get_fork_choice::(req, beacon_chain) - } - (&Method::GET, "/advanced/operation_pool") => { - advanced::get_operation_pool::(req, beacon_chain) - } - - (&Method::GET, "/metrics") => { - metrics::get_prometheus::(req, beacon_chain, db_path, freezer_db_path) - } - - // Lighthouse specific - (&Method::GET, "/lighthouse/syncing") => { - lighthouse::syncing::(req, network_globals) - } - - (&Method::GET, "/lighthouse/peers") => { - lighthouse::peers::(req, network_globals) - } - - (&Method::GET, "/lighthouse/connected_peers") => { - lighthouse::connected_peers::(req, network_globals) - } - _ => Err(ApiError::NotFound( - "Request path and/or method not found.".to_owned(), - )), }; + let request_processing_duration = Instant::now().duration_since(received_instant); + // Map the Rust-friendly `Result` in to a http-friendly response. In effect, this ensures that // any `Err` returned from our response handlers becomes a valid http response to the client // (e.g., a response with a 404 or 500 status). - let duration = Instant::now().duration_since(received_instant); - match request_result { - Ok(response) => { + + match result { + Ok(mut response) => { + if rest_api_config.allow_origin != "" { + let headers = response.headers_mut(); + headers.insert( + hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN, + HeaderValue::from_str(&rest_api_config.allow_origin)?, + ); + headers.insert(hyper::header::VARY, HeaderValue::from_static("Origin")); + } + debug!( local_log, "HTTP API request successful"; "path" => path, - "duration_ms" => duration.as_millis() + "duration_ms" => request_processing_duration.as_millis() ); metrics::inc_counter(&metrics::SUCCESS_COUNT); - metrics::stop_timer(timer); - Ok(response) } - Err(e) => { - let error_response = e.into(); + Err(error) => { debug!( local_log, "HTTP API request failure"; "path" => path, - "duration_ms" => duration.as_millis() + "duration_ms" => request_processing_duration.as_millis() ); - metrics::stop_timer(timer); - - Ok(error_response) + Ok(error.into()) } } } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 6e319e84901..167e1c4f4ca 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -161,6 +161,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("5052") .takes_value(true), ) + .arg( + Arg::with_name("http-allow-origin") + .long("http-allow-origin") + .value_name("ORIGIN") + .help("Set the value of the Access-Control-Allow-Origin response HTTP header") + .default_value("") + .takes_value(true), + ) /* Websocket related arguments */ .arg( Arg::with_name("ws") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index dd8bb9928d3..79ec6c6f877 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -4,6 +4,7 @@ use clap_utils::BAD_TESTNET_DIR_MESSAGE; use client::{config::DEFAULT_DATADIR, ClientConfig, ClientGenesis}; use eth2_libp2p::{Enr, Multiaddr}; use eth2_testnet_config::Eth2TestnetConfig; +use hyper; use slog::{crit, info, Logger}; use ssz::Encode; use std::fs; @@ -220,6 +221,15 @@ pub fn get_config( .map_err(|_| "http-port is not a valid u16.")?; } + if let Some(allow_origin) = cli_args.value_of("http-allow-origin") { + // Pre-validate the config value to give feedback to the user on node startup, instead of + // as late as when the first API response is produced. + hyper::header::HeaderValue::from_str(allow_origin) + .map_err(|_| "Invalid allow-origin value")?; + + client_config.rest_api.allow_origin = allow_origin.to_string(); + } + /* * Websocket server */ diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 08af75141a3..ed5bb47caa6 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -20,7 +20,6 @@ pub const VERSION: &str = git_version!( fallback = crate_version!() ); pub const DEFAULT_DATA_DIR: &str = ".lighthouse"; -pub const CLIENT_CONFIG_FILENAME: &str = "beacon-node.toml"; pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml"; fn main() { From 760e936086730b220113ff8bd6ab0ad25538f0e6 Mon Sep 17 00:00:00 2001 From: Adam Szkoda Date: Tue, 14 Jul 2020 13:40:54 +0200 Subject: [PATCH 2/2] Extend CLI option description --- beacon_node/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 167e1c4f4ca..2c52d66015f 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -165,7 +165,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("http-allow-origin") .long("http-allow-origin") .value_name("ORIGIN") - .help("Set the value of the Access-Control-Allow-Origin response HTTP header") + .help("Set the value of the Access-Control-Allow-Origin response HTTP header. Use * to allow any origin (not recommended in production)") .default_value("") .takes_value(true), )