Skip to content

Commit

Permalink
CORS Max Age (#280)
Browse files Browse the repository at this point in the history
* Set Access-Control-Max-Age header to 600 to prevent every request having an OPTIONS request.

* Make max age configurable, add test.

* Fix tests
  • Loading branch information
debris authored and tomusdrw committed Jul 23, 2018
1 parent 7b81e01 commit fa521c0
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 8 deletions.
22 changes: 20 additions & 2 deletions http/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct ServerHandler<M: Metadata = (), S: Middleware<M> = NoopMiddleware> {
jsonrpc_handler: Rpc<M, S>,
allowed_hosts: AllowedHosts,
cors_domains: CorsDomains,
cors_max_age: Option<u32>,
middleware: Arc<RequestMiddleware>,
rest_api: RestApi,
max_request_body_size: usize,
Expand All @@ -30,6 +31,7 @@ impl<M: Metadata, S: Middleware<M>> ServerHandler<M, S> {
pub fn new(
jsonrpc_handler: Rpc<M, S>,
cors_domains: CorsDomains,
cors_max_age: Option<u32>,
allowed_hosts: AllowedHosts,
middleware: Arc<RequestMiddleware>,
rest_api: RestApi,
Expand All @@ -39,6 +41,7 @@ impl<M: Metadata, S: Middleware<M>> ServerHandler<M, S> {
jsonrpc_handler,
allowed_hosts,
cors_domains,
cors_max_age,
middleware,
rest_api,
max_request_body_size,
Expand Down Expand Up @@ -84,6 +87,7 @@ impl<M: Metadata, S: Middleware<M>> server::Service for ServerHandler<M, S> {
is_options: false,
cors_header: cors::CorsHeader::NotRequired,
rest_api: self.rest_api,
cors_max_age: self.cors_max_age,
max_request_body_size: self.max_request_body_size,
})
}
Expand Down Expand Up @@ -176,6 +180,7 @@ pub struct RpcHandler<M: Metadata, S: Middleware<M>> {
state: RpcHandlerState<M, S::Future>,
is_options: bool,
cors_header: cors::CorsHeader<header::AccessControlAllowOrigin>,
cors_max_age: Option<u32>,
rest_api: RestApi,
max_request_body_size: usize,
}
Expand Down Expand Up @@ -233,7 +238,12 @@ impl<M: Metadata, S: Middleware<M>> Future for RpcHandler<M, S> {
RpcHandlerState::Writing(res) => {
let mut response: server::Response = res.into();
let cors_header = mem::replace(&mut self.cors_header, cors::CorsHeader::Invalid);
Self::set_response_headers(response.headers_mut(), self.is_options, cors_header.into());
Self::set_response_headers(
response.headers_mut(),
self.is_options,
cors_header.into(),
self.cors_max_age,
);
Ok(Async::Ready(response))
},
state => {
Expand Down Expand Up @@ -392,7 +402,12 @@ impl<M: Metadata, S: Middleware<M>> RpcHandler<M, S> {
}
}

fn set_response_headers(headers: &mut Headers, is_options: bool, cors_header: Option<header::AccessControlAllowOrigin>) {
fn set_response_headers(
headers: &mut Headers,
is_options: bool,
cors_header: Option<header::AccessControlAllowOrigin>,
cors_max_age: Option<u32>,
) {
if is_options {
headers.set(header::Allow(vec![
Method::Options,
Expand All @@ -413,6 +428,9 @@ impl<M: Metadata, S: Middleware<M>> RpcHandler<M, S> {
Ascii::new("content-type".to_owned()),
Ascii::new("accept".to_owned()),
]));
if let Some(cors_max_age) = cors_max_age {
headers.set(header::AccessControlMaxAge(cors_max_age));
}
headers.set(cors_domain);
headers.set(header::Vary::Items(vec![
Ascii::new("origin".to_owned())
Expand Down
26 changes: 24 additions & 2 deletions http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ pub struct ServerBuilder<M: jsonrpc::Metadata = (), S: jsonrpc::Middleware<M> =
meta_extractor: Arc<MetaExtractor<M>>,
request_middleware: Arc<RequestMiddleware>,
cors_domains: CorsDomains,
cors_max_age: Option<u32>,
allowed_hosts: AllowedHosts,
rest_api: RestApi,
keep_alive: bool,
Expand Down Expand Up @@ -234,6 +235,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
meta_extractor: Arc::new(extractor),
request_middleware: Arc::new(NoopRequestMiddleware::default()),
cors_domains: None,
cors_max_age: None,
allowed_hosts: None,
rest_api: RestApi::Disabled,
keep_alive: true,
Expand All @@ -243,28 +245,32 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
}

/// Utilize existing event loop remote to poll RPC results.
///
/// Applies only to 1 of the threads. Other threads will spawn their own Event Loops.
pub fn event_loop_remote(mut self, remote: tokio_core::reactor::Remote) -> Self {
self.remote = UninitializedRemote::Shared(remote);
self
}

/// Enable the REST -> RPC converter. Allows you to invoke RPCs
/// by sending `POST /<method>/<param1>/<param2>` requests
/// Enable the REST -> RPC converter.
///
/// Allows you to invoke RPCs by sending `POST /<method>/<param1>/<param2>` requests
/// (with no body). Disabled by default.
pub fn rest_api(mut self, rest_api: RestApi) -> Self {
self.rest_api = rest_api;
self
}

/// Sets Enables or disables HTTP keep-alive.
///
/// Default is true.
pub fn keep_alive(mut self, val: bool) -> Self {
self.keep_alive = val;
self
}

/// Sets number of threads of the server to run.
///
/// Panics when set to `0`.
#[cfg(not(unix))]
pub fn threads(mut self, _threads: usize) -> Self {
Expand All @@ -273,6 +279,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
}

/// Sets number of threads of the server to run.
///
/// Panics when set to `0`.
#[cfg(unix)]
pub fn threads(mut self, threads: usize) -> Self {
Expand All @@ -286,6 +293,16 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
self
}

/// Configure CORS `AccessControlMaxAge` header returned.
///
/// Passing `Some(millis)` informs the client that the CORS preflight request is not necessary
/// for at list `millis` ms.
/// Disabled by default.
pub fn cors_max_age<T: Into<Option<u32>>>(mut self, cors_max_age: T) -> Self {
self.cors_max_age = cors_max_age.into();
self
}

/// Configures request middleware
pub fn request_middleware<T: RequestMiddleware>(mut self, middleware: T) -> Self {
self.request_middleware = Arc::new(middleware);
Expand Down Expand Up @@ -319,6 +336,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
/// Start this JSON-RPC HTTP server trying to bind to specified `SocketAddr`.
pub fn start_http(self, addr: &SocketAddr) -> io::Result<Server> {
let cors_domains = self.cors_domains;
let cors_max_age = self.cors_max_age;
let request_middleware = self.request_middleware;
let allowed_hosts = self.allowed_hosts;
let jsonrpc_handler = Rpc {
Expand All @@ -338,6 +356,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
eloop.remote(),
addr.to_owned(),
cors_domains.clone(),
cors_max_age,
request_middleware.clone(),
allowed_hosts.clone(),
jsonrpc_handler.clone(),
Expand All @@ -355,6 +374,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
eloop.remote(),
addr.to_owned(),
cors_domains.clone(),
cors_max_age,
request_middleware.clone(),
allowed_hosts.clone(),
jsonrpc_handler.clone(),
Expand Down Expand Up @@ -395,6 +415,7 @@ fn serve<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>>(
remote: tokio_core::reactor::Remote,
addr: SocketAddr,
cors_domains: CorsDomains,
cors_max_age: Option<u32>,
request_middleware: Arc<RequestMiddleware>,
allowed_hosts: AllowedHosts,
jsonrpc_handler: Rpc<M, S>,
Expand Down Expand Up @@ -454,6 +475,7 @@ fn serve<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>>(
http.bind_connection(&handle, socket, addr, ServerHandler::new(
jsonrpc_handler.clone(),
cors_domains.clone(),
cors_max_age,
allowed_hosts.clone(),
request_middleware.clone(),
rest_api,
Expand Down
36 changes: 32 additions & 4 deletions http/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ fn serve_hosts(hosts: Vec<Host>) -> Server {
}

fn serve() -> Server {
serve_rest(RestApi::Secure, false)
serve_rest(RestApi::Secure, false, None)
}

fn serve_rest(rest: RestApi, cors_all: bool) -> Server {
fn serve_rest(rest: RestApi, cors_all: bool, cors_max_age: Option<u32>) -> Server {
use std::thread;
let mut io = IoHandler::default();
io.add_method("hello", |params: Params| {
Expand Down Expand Up @@ -50,6 +50,7 @@ fn serve_rest(rest: RestApi, cors_all: bool) -> Server {
AccessControlAllowOrigin::Null,
])
})
.cors_max_age(cors_max_age)
.rest_api(rest)
.start_http(&"127.0.0.1:0".parse().unwrap())
.unwrap()
Expand Down Expand Up @@ -260,6 +261,33 @@ fn should_add_cors_headers() {
assert!(response.headers.contains("Access-Control-Allow-Origin: http://parity.io"), "Headers missing in {}", response.headers);
}

#[test]
fn should_add_cors_max_age_headers() {
// given
let server = serve_rest(RestApi::Disabled, false, Some(1_000));

// when
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://parity.io\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);

// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
assert!(response.headers.contains("Access-Control-Allow-Origin: http://parity.io"), "Headers missing in {}", response.headers);
assert!(response.headers.contains("Access-Control-Max-Age: 1000"), "Headers missing in {}", response.headers);
}

#[test]
fn should_not_add_cors_headers() {
// given
Expand Down Expand Up @@ -363,7 +391,7 @@ fn should_add_cors_header_for_null_origin() {
#[test]
fn should_add_cors_header_for_null_origin_when_all() {
// given
let server = serve_rest(RestApi::Secure, true);
let server = serve_rest(RestApi::Secure, true, None);

// when
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
Expand Down Expand Up @@ -680,7 +708,7 @@ fn should_handle_rest_request_with_params() {
#[test]
fn should_return_error_in_case_of_unsecure_rest_and_no_method() {
// given
let server = serve_rest(RestApi::Unsecure, false);
let server = serve_rest(RestApi::Unsecure, false, None);
let addr = server.address().clone();

// when
Expand Down

0 comments on commit fa521c0

Please sign in to comment.