Skip to content

Commit

Permalink
Allow customising access control allow headers (paritytech#305)
Browse files Browse the repository at this point in the history
* Fix typo

* Refactor CorsHeader to AllowOrigin

* Support Access-Control-Request-Headers in http-server

This commit adds the capability to customise the header fields
which are allowed.

A CORS preflight request uses the `Access-Control-Request-Headers`
header to inquire about header fields which are allowed to be used.

The response is either `403 Forbidden` or `200 OK` with the header
`Access-Control-Allow-Headers` and the list of fields which were
supplied in the `Access-Control-Request-Headers`.

This list is not necessarily a complete list of all allowed fields.
Rather certain fields are always allowed and don't need to be listed.
Also `Access-Control-Request-Headers` might not contain header fields
which would in fact be allowed to send; these fields are currently
not exposed -- only fields which were in `Access-Control-Request-Headers`
are containted in the responding `Access-Control-Allow-Headers`.

The API can be used with two enums:

	`cors_allow_headers(AccessControlAllowHeaders::Only(Vec))`
	`cors_allow_headers(AccessControlAllowHeaders::Any)`

The default is `Any`, as to not break the existing behavior.

If `Only` is chosen there will still be some default headers which
are always allowed (`Content-Type`, etc.).

* (cache always allowed headers and use HashSet for them)

* (get rid of Deref import)

* (do not expose Ascii)

* (convert Vector to HashSet for allowed headers set by user)

* (change to non-owned str to prevent copying the header when testing containment)
  • Loading branch information
cmichi authored and CriesofCarrots committed Sep 28, 2018
1 parent 502313a commit 8903a0c
Show file tree
Hide file tree
Showing 12 changed files with 744 additions and 89 deletions.
8 changes: 7 additions & 1 deletion http/examples/http_meta.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
extern crate solana_jsonrpc_http_server as jsonrpc_http_server;
extern crate unicase;

use jsonrpc_http_server::{ServerBuilder, hyper, RestApi};
use jsonrpc_http_server::{ServerBuilder, hyper, RestApi, AccessControlAllowHeaders};
use jsonrpc_http_server::jsonrpc_core::*;
use self::hyper::header;

Expand All @@ -24,6 +25,11 @@ fn main() {
});

let server = ServerBuilder::new(io)
.cors_allow_headers(AccessControlAllowHeaders::Only(
vec![
"Authorization",
])
)
.rest_api(RestApi::Unsecure)
// You can also implement `MetaExtractor` trait and pass a struct here.
.meta_extractor(|req: &hyper::Request| {
Expand Down
49 changes: 35 additions & 14 deletions http/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use jsonrpc::futures::{Future, Poll, Async, Stream, future};
use jsonrpc::serde_json;
use response::Response;
use server_utils::cors;
use server_utils::cors::AllowHeaders;

use {utils, RequestMiddleware, RequestMiddlewareAction, CorsDomains, AllowedHosts, RestApi};

Expand All @@ -21,6 +22,7 @@ pub struct ServerHandler<M: Metadata = (), S: Middleware<M> = NoopMiddleware> {
allowed_hosts: AllowedHosts,
cors_domains: CorsDomains,
cors_max_age: Option<u32>,
allowed_headers: cors::AccessControlAllowHeadersUnicase,
middleware: Arc<RequestMiddleware>,
rest_api: RestApi,
health_api: Option<(String, String)>,
Expand All @@ -33,6 +35,7 @@ impl<M: Metadata, S: Middleware<M>> ServerHandler<M, S> {
jsonrpc_handler: Rpc<M, S>,
cors_domains: CorsDomains,
cors_max_age: Option<u32>,
allowed_headers: cors::AccessControlAllowHeadersUnicase,
allowed_hosts: AllowedHosts,
middleware: Arc<RequestMiddleware>,
rest_api: RestApi,
Expand All @@ -44,6 +47,7 @@ impl<M: Metadata, S: Middleware<M>> ServerHandler<M, S> {
allowed_hosts,
cors_domains,
cors_max_age,
allowed_headers,
middleware,
rest_api,
health_api,
Expand Down Expand Up @@ -88,10 +92,12 @@ impl<M: Metadata, S: Middleware<M>> server::Service for ServerHandler<M, S> {
continue_on_invalid_cors: should_continue_on_invalid_cors,
},
is_options: false,
cors_header: cors::CorsHeader::NotRequired,
cors_allow_origin: cors::AllowOrigin::NotRequired,
rest_api: self.rest_api,
health_api: self.health_api.clone(),
cors_max_age: self.cors_max_age,
allowed_headers: self.allowed_headers.clone(),
cors_allow_headers: cors::AllowHeaders::NotRequired,
max_request_body_size: self.max_request_body_size,
})
}
Expand Down Expand Up @@ -196,8 +202,10 @@ pub struct RpcHandler<M: Metadata, S: Middleware<M>> {
jsonrpc_handler: Rpc<M, S>,
state: RpcHandlerState<M, S::Future>,
is_options: bool,
cors_header: cors::CorsHeader<header::AccessControlAllowOrigin>,
cors_allow_origin: cors::AllowOrigin<header::AccessControlAllowOrigin>,
cors_max_age: Option<u32>,
cors_allow_headers: cors::AllowHeaders<header::AccessControlAllowHeaders>,
allowed_headers: cors::AccessControlAllowHeadersUnicase,
rest_api: RestApi,
health_api: Option<(String, String)>,
max_request_body_size: usize,
Expand All @@ -208,10 +216,13 @@ impl<M: Metadata, S: Middleware<M>> Future for RpcHandler<M, S> {
type Error = hyper::Error;

fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let allowed_headers = self.allowed_headers.clone();

let new_state = match mem::replace(&mut self.state, RpcHandlerState::Done) {
RpcHandlerState::ReadingHeaders { request, cors_domains, continue_on_invalid_cors, } => {
// Read cors header
self.cors_header = utils::cors_header(&request, &cors_domains);
self.cors_allow_origin = utils::cors_allow_origin(&request, &cors_domains);
self.cors_allow_headers = utils::cors_allow_headers(&request, &allowed_headers);
self.is_options = *request.method() == Method::Options;
// Read other headers
RpcPollState::Ready(self.read_headers(request, continue_on_invalid_cors))
Expand Down Expand Up @@ -269,12 +280,15 @@ impl<M: Metadata, S: Middleware<M>> Future for RpcHandler<M, S> {
match new_state {
RpcHandlerState::Writing(res) => {
let mut response: server::Response = res.into();
let cors_header = mem::replace(&mut self.cors_header, cors::CorsHeader::Invalid);
let cors_allow_origin = mem::replace(&mut self.cors_allow_origin, cors::AllowOrigin::Invalid);
let cors_allow_headers = mem::replace(&mut self.cors_allow_headers, cors::AllowHeaders::Invalid);

Self::set_response_headers(
response.headers_mut(),
self.is_options,
cors_header.into(),
cors_allow_origin.into(),
self.cors_max_age,
cors_allow_headers,
);
Ok(Async::Ready(response))
},
Expand Down Expand Up @@ -310,9 +324,13 @@ impl<M: Metadata, S: Middleware<M>> RpcHandler<M, S> {
request: server::Request,
continue_on_invalid_cors: bool,
) -> RpcHandlerState<M, S::Future> {
if self.cors_header == cors::CorsHeader::Invalid && !continue_on_invalid_cors {
return RpcHandlerState::Writing(Response::invalid_cors());
if self.cors_allow_origin == cors::AllowOrigin::Invalid && !continue_on_invalid_cors {
return RpcHandlerState::Writing(Response::invalid_allow_origin());
}
if self.cors_allow_headers == cors::AllowHeaders::Invalid && !continue_on_invalid_cors {
return RpcHandlerState::Writing(Response::invalid_allow_headers());
}

// Read metadata
let metadata = self.jsonrpc_handler.extractor.read_metadata(&request);

Expand Down Expand Up @@ -481,8 +499,9 @@ impl<M: Metadata, S: Middleware<M>> RpcHandler<M, S> {
fn set_response_headers(
headers: &mut Headers,
is_options: bool,
cors_header: Option<header::AccessControlAllowOrigin>,
cors_allow_origin: Option<header::AccessControlAllowOrigin>,
cors_max_age: Option<u32>,
cors_allow_headers: AllowHeaders<header::AccessControlAllowHeaders>,
) {
if is_options {
headers.set(header::Allow(vec![
Expand All @@ -494,16 +513,18 @@ impl<M: Metadata, S: Middleware<M>> RpcHandler<M, S> {
]));
}

if let Some(cors_domain) = cors_header {
if let Some(cors_domain) = cors_allow_origin {
headers.set(header::AccessControlAllowMethods(vec![
Method::Options,
Method::Post
]));
headers.set(header::AccessControlAllowHeaders(vec![
Ascii::new("origin".to_owned()),
Ascii::new("content-type".to_owned()),
Ascii::new("accept".to_owned()),
]));

if let AllowHeaders::Ok(cors_allow_headers) = cors_allow_headers {
if !cors_allow_headers.is_empty() {
headers.set(cors_allow_headers);
}
}

if let Some(cors_max_age) = cors_max_age {
headers.set(header::AccessControlMaxAge(cors_max_age));
}
Expand Down
20 changes: 17 additions & 3 deletions http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ use jsonrpc::futures::sync::oneshot;
use server_utils::reactor::{Remote, UninitializedRemote};

pub use server_utils::hosts::{Host, DomainsValidation};
pub use server_utils::cors::{AccessControlAllowOrigin, Origin};
pub use server_utils::cors::{AccessControlAllowOrigin, Origin, AccessControlAllowHeaders};
pub use server_utils::cors;
pub use server_utils::tokio_core;
pub use handler::ServerHandler;
pub use utils::{is_host_allowed, cors_header, CorsHeader};
pub use utils::{is_host_allowed, cors_allow_origin, AllowOrigin};
pub use response::Response;

/// Action undertaken by a middleware.
Expand Down Expand Up @@ -198,6 +199,7 @@ pub struct ServerBuilder<M: jsonrpc::Metadata = (), S: jsonrpc::Middleware<M> =
request_middleware: Arc<RequestMiddleware>,
cors_domains: CorsDomains,
cors_max_age: Option<u32>,
allowed_headers: cors::AccessControlAllowHeadersUnicase,
allowed_hosts: AllowedHosts,
rest_api: RestApi,
health_api: Option<(String, String)>,
Expand Down Expand Up @@ -236,6 +238,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
request_middleware: Arc::new(NoopRequestMiddleware::default()),
cors_domains: None,
cors_max_age: None,
allowed_headers: cors::AccessControlAllowHeadersUnicase::Any,
allowed_hosts: None,
rest_api: RestApi::Disabled,
health_api: None,
Expand Down Expand Up @@ -313,13 +316,19 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
/// Configure CORS `AccessControlMaxAge` header returned.
///
/// Passing `Some(millis)` informs the client that the CORS preflight request is not necessary
/// for at list `millis` ms.
/// for at least `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
}

/// Configure the CORS `AccessControlAllowHeaders` header which are allowed.
pub fn cors_allow_headers(mut self, allowed_headers: cors::AccessControlAllowHeaders) -> Self {
self.allowed_headers = allowed_headers.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 @@ -354,6 +363,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
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 allowed_headers = self.allowed_headers;
let request_middleware = self.request_middleware;
let allowed_hosts = self.allowed_hosts;
let jsonrpc_handler = Rpc {
Expand All @@ -375,6 +385,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
addr.to_owned(),
cors_domains.clone(),
cors_max_age,
allowed_headers.clone(),
request_middleware.clone(),
allowed_hosts.clone(),
jsonrpc_handler.clone(),
Expand All @@ -394,6 +405,7 @@ impl<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>> ServerBuilder<M, S> {
addr.to_owned(),
cors_domains.clone(),
cors_max_age,
allowed_headers.clone(),
request_middleware.clone(),
allowed_hosts.clone(),
jsonrpc_handler.clone(),
Expand Down Expand Up @@ -436,6 +448,7 @@ fn serve<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>>(
addr: SocketAddr,
cors_domains: CorsDomains,
cors_max_age: Option<u32>,
allowed_headers: cors::AccessControlAllowHeadersUnicase,
request_middleware: Arc<RequestMiddleware>,
allowed_hosts: AllowedHosts,
jsonrpc_handler: Rpc<M, S>,
Expand Down Expand Up @@ -501,6 +514,7 @@ fn serve<M: jsonrpc::Metadata, S: jsonrpc::Middleware<M>>(
jsonrpc_handler.clone(),
cors_domains.clone(),
cors_max_age,
allowed_headers.clone(),
allowed_hosts.clone(),
request_middleware.clone(),
rest_api,
Expand Down
11 changes: 10 additions & 1 deletion http/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,23 @@ impl Response {
}

/// CORS invalid
pub fn invalid_cors() -> Self {
pub fn invalid_allow_origin() -> Self {
Response {
code: StatusCode::Forbidden,
content_type: header::ContentType::plaintext(),
content: "Origin of the request is not whitelisted. CORS headers would not be sent and any side-effects were cancelled as well.\n".to_owned(),
}
}

/// CORS header invalid
pub fn invalid_allow_headers() -> Self {
Response {
code: StatusCode::Forbidden,
content_type: header::ContentType::plaintext(),
content: "Header field is not allowed.\n".to_owned(),
}
}

/// Create a response for bad request
pub fn bad_request<S: Into<String>>(msg: S) -> Self {
Response {
Expand Down
Loading

0 comments on commit 8903a0c

Please sign in to comment.