Add ability to configure CORS header (#1345)

## Issue Addressed

https://github.com/sigp/lighthouse/issues/1177

## Proposed Changes

Add a command line option (`--http-allow-origin`) and a config item for configuring the `Access-Control-Allow-Origin` response header.  This should unblock making XMLHttpRequests.
This commit is contained in:
Adam Szkoda 2020-07-16 07:23:14 +00:00
parent 4a01f44206
commit fc5e6cbbb0
9 changed files with 210 additions and 152 deletions

1
Cargo.lock generated
View File

@ -396,6 +396,7 @@ dependencies = [
"exit-future",
"futures 0.3.5",
"genesis",
"hyper 0.13.6",
"logging",
"node_test_rig",
"rand 0.7.3",

View File

@ -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"

View File

@ -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(),
}
}
}

View File

@ -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<Response<Body>, 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<std::io::Error> for ApiError {
}
}
impl From<hyper::header::InvalidHeaderValue> 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

View File

@ -64,12 +64,14 @@ pub fn start_server<T: BeaconChainTypes>(
) -> Result<SocketAddr, hyper::Error> {
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<T: BeaconChainTypes>(
beacon_chain.clone(),
network_globals.clone(),
network_channel.clone(),
rest_api_config.clone(),
eth2_config.clone(),
log.clone(),
db_path.clone(),

View File

@ -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,20 +22,23 @@ pub async fn route<T: BeaconChainTypes>(
beacon_chain: Arc<BeaconChain<T>>,
network_globals: Arc<NetworkGlobals<T::EthSpec>>,
network_channel: NetworkChannel<T::EthSpec>,
rest_api_config: Arc<Config>,
eth2_config: Arc<Eth2Config>,
local_log: slog::Logger,
db_path: PathBuf,
freezer_db_path: PathBuf,
events: Arc<Mutex<Bus<SignedBeaconBlockHash>>>,
) -> Result<Response<Body>, Error> {
) -> Result<Response<Body>, 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()) {
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),
@ -50,7 +54,9 @@ pub async fn route<T: BeaconChainTypes>(
// Methods for Network
(&Method::GET, "/network/enr") => network::get_enr::<T>(req, network_globals),
(&Method::GET, "/network/peer_count") => network::get_peer_count::<T>(req, network_globals),
(&Method::GET, "/network/peer_count") => {
network::get_peer_count::<T>(req, network_globals)
}
(&Method::GET, "/network/peer_id") => network::get_peer_id::<T>(req, network_globals),
(&Method::GET, "/network/peers") => network::get_peer_list::<T>(req, network_globals),
(&Method::GET, "/network/listen_port") => {
@ -70,7 +76,9 @@ pub async fn route<T: BeaconChainTypes>(
let reader = events.lock().add_rx();
beacon::stream_forks::<T>(log, reader)
}
(&Method::GET, "/beacon/genesis_time") => beacon::get_genesis_time::<T>(req, beacon_chain),
(&Method::GET, "/beacon/genesis_time") => {
beacon::get_genesis_time::<T>(req, beacon_chain)
}
(&Method::GET, "/beacon/genesis_validators_root") => {
beacon::get_genesis_validators_root::<T>(req, beacon_chain)
}
@ -99,7 +107,8 @@ pub async fn route<T: BeaconChainTypes>(
// Methods for Validator
(&Method::POST, "/validator/duties") => {
let timer = metrics::start_timer(&metrics::VALIDATOR_GET_DUTIES_REQUEST_RESPONSE_TIME);
let timer =
metrics::start_timer(&metrics::VALIDATOR_GET_DUTIES_REQUEST_RESPONSE_TIME);
let response = validator::post_validator_duties::<T>(req, beacon_chain);
drop(timer);
response.await
@ -114,7 +123,8 @@ pub async fn route<T: BeaconChainTypes>(
validator::get_active_validator_duties::<T>(req, beacon_chain)
}
(&Method::GET, "/validator/block") => {
let timer = metrics::start_timer(&metrics::VALIDATOR_GET_BLOCK_REQUEST_RESPONSE_TIME);
let timer =
metrics::start_timer(&metrics::VALIDATOR_GET_BLOCK_REQUEST_RESPONSE_TIME);
let response = validator::get_new_beacon_block::<T>(req, beacon_chain, log);
drop(timer);
response
@ -136,7 +146,12 @@ pub async fn route<T: BeaconChainTypes>(
validator::publish_attestations::<T>(req, beacon_chain, network_channel, log).await
}
(&Method::POST, "/validator/aggregate_and_proofs") => {
validator::publish_aggregate_and_proofs::<T>(req, beacon_chain, network_channel, log)
validator::publish_aggregate_and_proofs::<T>(
req,
beacon_chain,
network_channel,
log,
)
.await
}
@ -151,7 +166,9 @@ pub async fn route<T: BeaconChainTypes>(
// Methods for bootstrap and checking configuration
(&Method::GET, "/spec") => spec::get_spec::<T>(req, beacon_chain),
(&Method::GET, "/spec/slots_per_epoch") => spec::get_slots_per_epoch::<T>(req),
(&Method::GET, "/spec/deposit_contract") => helpers::implementation_pending_response(req),
(&Method::GET, "/spec/deposit_contract") => {
helpers::implementation_pending_response(req)
}
(&Method::GET, "/spec/eth2_config") => spec::get_eth2_config::<T>(req, eth2_config),
// Methods for advanced parameters
@ -181,37 +198,44 @@ pub async fn route<T: BeaconChainTypes>(
_ => 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())
}
}
}

View File

@ -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. Use * to allow any origin (not recommended in production)")
.default_value("")
.takes_value(true),
)
/* Websocket related arguments */
.arg(
Arg::with_name("ws")

View File

@ -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<E: EthSpec>(
.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
*/

View File

@ -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() {