Unverified Commit bf73876d authored by Niklas Adolfsson's avatar Niklas Adolfsson Committed by GitHub
Browse files

replace `WS and HTTP servers` with a server that supports both `WS and HTTP` (#863)



* ws server: support both http and ws

* clean things up

* ws server: add http logger and ws logger

* more cleanup

* fix nits

* middleware example

* remove http and ws server crates

* move things around

* some minor fixes

* fix stop in http context

* fix tests

* fix features

* use header constants

* remove access_control & expose only host filtering

CORS has been removed to tower middleware and doesn't need to supported anymore

* fix merge logging traits + move to server

* supress warnings faulty dead code

* remove unwrap

* support http2

* doesnt work

* feat: simpler stop handling

* Update server/src/future.rs

* some cleanup

* Update server/src/future.rs

Co-authored-by: default avatarAlexandru Vasile <60601340+lexnv@users.noreply.github.com>

* Update server/src/future.rs

Co-authored-by: default avatarAlexandru Vasile <60601340+lexnv@users.noreply.github.com>

* fix nits

* address grumbles

* commit examples and nits

* clarify comment

* remove noise

* remove impl Future for ServerHandle

* remove needless async {}

* add http2 test

* add ServerBuilder::build_from_tcp

* fix super nit: no more Option<SocketAddr>

* fix Option<SocketAddr>

* Update server/src/future.rs

Co-authored-by: default avatarAlexandru Vasile <60601340+lexnv@users.noreply.github.com>

* Update server/src/future.rs

Co-authored-by: default avatarAlexandru Vasile <60601340+lexnv@users.noreply.github.com>

Co-authored-by: default avatarAlexandru Vasile <60601340+lexnv@users.noreply.github.com>
parent 41b8a2c9
Pipeline #214546 passed with stages
in 4 minutes and 56 seconds
...@@ -2,13 +2,12 @@ ...@@ -2,13 +2,12 @@
members = [ members = [
"examples", "examples",
"benches", "benches",
"http-server", "server",
"test-utils", "test-utils",
"jsonrpsee", "jsonrpsee",
"tests", "tests",
"types", "types",
"core", "core",
"ws-server",
"client/ws-client", "client/ws-client",
"client/http-client", "client/http-client",
"client/transport", "client/transport",
......
...@@ -110,12 +110,13 @@ pub async fn ws_server(handle: tokio::runtime::Handle) -> (String, jsonrpc_ws_se ...@@ -110,12 +110,13 @@ pub async fn ws_server(handle: tokio::runtime::Handle) -> (String, jsonrpc_ws_se
/// Run jsonrpsee HTTP server for benchmarks. /// Run jsonrpsee HTTP server for benchmarks.
#[cfg(not(feature = "jsonrpc-crate"))] #[cfg(not(feature = "jsonrpc-crate"))]
pub async fn http_server(handle: tokio::runtime::Handle) -> (String, jsonrpsee::http_server::HttpServerHandle) { pub async fn http_server(handle: tokio::runtime::Handle) -> (String, jsonrpsee::server::ServerHandle) {
use jsonrpsee::http_server::HttpServerBuilder; use jsonrpsee::server::ServerBuilder;
let server = HttpServerBuilder::default() let server = ServerBuilder::default()
.max_request_body_size(u32::MAX) .max_request_body_size(u32::MAX)
.max_response_body_size(u32::MAX) .max_response_body_size(u32::MAX)
.max_connections(10 * 1024)
.custom_tokio_runtime(handle) .custom_tokio_runtime(handle)
.build("127.0.0.1:0") .build("127.0.0.1:0")
.await .await
...@@ -130,10 +131,10 @@ pub async fn http_server(handle: tokio::runtime::Handle) -> (String, jsonrpsee:: ...@@ -130,10 +131,10 @@ pub async fn http_server(handle: tokio::runtime::Handle) -> (String, jsonrpsee::
/// Run jsonrpsee WebSocket server for benchmarks. /// Run jsonrpsee WebSocket server for benchmarks.
#[cfg(not(feature = "jsonrpc-crate"))] #[cfg(not(feature = "jsonrpc-crate"))]
pub async fn ws_server(handle: tokio::runtime::Handle) -> (String, jsonrpsee::ws_server::WsServerHandle) { pub async fn ws_server(handle: tokio::runtime::Handle) -> (String, jsonrpsee::server::ServerHandle) {
use jsonrpsee::ws_server::WsServerBuilder; use jsonrpsee::server::ServerBuilder;
let server = WsServerBuilder::default() let server = ServerBuilder::default()
.max_request_body_size(u32::MAX) .max_request_body_size(u32::MAX)
.max_response_body_size(u32::MAX) .max_response_body_size(u32::MAX)
.max_connections(10 * 1024) .max_connections(10 * 1024)
......
...@@ -13,9 +13,3 @@ documentation = "https://docs.rs/jsonrpsee-ws-client" ...@@ -13,9 +13,3 @@ documentation = "https://docs.rs/jsonrpsee-ws-client"
jsonrpsee-types = { path = "../../types", version = "0.15.1" } jsonrpsee-types = { path = "../../types", version = "0.15.1" }
jsonrpsee-client-transport = { path = "../transport", version = "0.15.1", features = ["web"] } jsonrpsee-client-transport = { path = "../transport", version = "0.15.1", features = ["web"] }
jsonrpsee-core = { path = "../../core", version = "0.15.1", features = ["async-wasm-client"] } jsonrpsee-core = { path = "../../core", version = "0.15.1", features = ["async-wasm-client"] }
[dev-dependencies]
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
jsonrpsee-test-utils = { path = "../../test-utils" }
tokio = { version = "1", features = ["macros"] }
serde_json = "1"
...@@ -46,8 +46,6 @@ server = [ ...@@ -46,8 +46,6 @@ server = [
"rand", "rand",
"tokio/rt", "tokio/rt",
"tokio/sync", "tokio/sync",
"http",
"hyper",
] ]
client = ["futures-util/sink", "futures-channel/sink", "futures-channel/std"] client = ["futures-util/sink", "futures-channel/sink", "futures-channel/std"]
async-client = [ async-client = [
......
...@@ -81,13 +81,13 @@ pub async fn read_body( ...@@ -81,13 +81,13 @@ pub async fn read_body(
/// NOTE: There's no specific hard limit on `Content_length` in HTTP specification. /// NOTE: There's no specific hard limit on `Content_length` in HTTP specification.
/// Thus this method might reject valid `content_length` /// Thus this method might reject valid `content_length`
fn read_header_content_length(headers: &hyper::header::HeaderMap) -> Option<u32> { fn read_header_content_length(headers: &hyper::header::HeaderMap) -> Option<u32> {
let length = read_header_value(headers, "content-length")?; let length = read_header_value(headers, hyper::header::CONTENT_LENGTH)?;
// HTTP Content-Length indicates number of bytes in decimal. // HTTP Content-Length indicates number of bytes in decimal.
length.parse::<u32>().ok() length.parse::<u32>().ok()
} }
/// Returns a string value when there is exactly one value for the given header. /// Returns a string value when there is exactly one value for the given header.
pub fn read_header_value<'a>(headers: &'a hyper::header::HeaderMap, header_name: &str) -> Option<&'a str> { pub fn read_header_value(headers: &hyper::header::HeaderMap, header_name: hyper::header::HeaderName) -> Option<&str> {
let mut values = headers.get_all(header_name).iter(); let mut values = headers.get_all(header_name).iter();
let val = values.next()?; let val = values.next()?;
if values.next().is_none() { if values.next().is_none() {
......
...@@ -49,7 +49,6 @@ cfg_http_helpers! { ...@@ -49,7 +49,6 @@ cfg_http_helpers! {
cfg_server! { cfg_server! {
pub mod id_providers; pub mod id_providers;
pub mod server; pub mod server;
pub mod logger;
} }
cfg_client! { cfg_client! {
......
//! Access control based on HTTP headers
pub mod origin;
pub mod host;
mod matcher;
pub use origin::{AllowOrigins, Origin};
pub use host::{Host, AllowHosts};
use crate::Error;
/// Define access on control on HTTP layer.
#[derive(Clone, Debug)]
pub struct AccessControl {
allowed_hosts: AllowHosts,
allowed_origins: AllowOrigins,
}
impl AccessControl {
/// Validate incoming request by host.
///
/// `host` is the return value from the `host header`
pub fn verify_host(&self, host: &str) -> Result<(), Error> {
self.allowed_hosts.verify(host)
}
/// Validate incoming request by origin.
///
/// `host` is the return value from the `host header`
/// `origin` is the value from the `origin header`.
pub fn verify_origin(&self, origin: Option<&str>, host: &str) -> Result<(), Error> {
self.allowed_origins.verify(origin, host)
}
}
impl Default for AccessControl {
fn default() -> Self {
Self { allowed_hosts: AllowHosts::Any, allowed_origins: AllowOrigins::Any }
}
}
/// Convenience builder pattern
#[derive(Debug)]
pub struct AccessControlBuilder {
allowed_hosts: AllowHosts,
allowed_origins: AllowOrigins,
}
impl Default for AccessControlBuilder {
fn default() -> Self {
Self { allowed_hosts: AllowHosts::Any, allowed_origins: AllowOrigins::Any }
}
}
impl AccessControlBuilder {
/// Create a new builder for `AccessControl`.
pub fn new() -> Self {
Self::default()
}
/// Allow all hosts.
pub fn allow_all_hosts(mut self) -> Self {
self.allowed_hosts = AllowHosts::Any;
self
}
/// Allow all origins.
pub fn allow_all_origins(mut self) -> Self {
self.allowed_origins = AllowOrigins::Any;
self
}
/// Configure allowed hosts.
///
/// Default - allow all.
pub fn set_allowed_hosts<List, H>(mut self, list: List) -> Result<Self, Error>
where
List: IntoIterator<Item = H>,
H: Into<String>,
{
let allowed_hosts: Vec<_> = list.into_iter().map(|s| Host::parse(&s.into())).map(Into::into).collect();
if allowed_hosts.is_empty() {
return Err(Error::EmptyAllowList("Host"));
}
self.allowed_hosts = AllowHosts::Only(allowed_hosts);
Ok(self)
}
/// Configure allowed origins.
///
/// Default - allow all.
pub fn set_allowed_origins<O, List>(mut self, list: List) -> Result<Self, Error>
where
List: IntoIterator<Item = O>,
O: Into<String>,
{
let allowed_origins: Vec<Origin> = list.into_iter().map(Into::into).map(Into::into).collect();
if allowed_origins.is_empty() {
return Err(Error::EmptyAllowList("Origin"));
}
self.allowed_origins = AllowOrigins::Only(allowed_origins);
Ok(self)
}
/// Finalize the `AccessControl` settings.
pub fn build(self) -> AccessControl {
AccessControl {
allowed_hosts: self.allowed_hosts,
allowed_origins: self.allowed_origins,
}
}
}
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//! Origin filtering functions
use std::{fmt, ops};
use crate::server::access_control::host::{Host, Port};
use crate::server::access_control::matcher::{Matcher, Pattern};
use crate::Error;
/// Origin Protocol
#[derive(Clone, Hash, Debug, PartialEq, Eq)]
pub enum OriginProtocol {
/// Http protocol
Http,
/// Https protocol
Https,
/// Custom protocol
Custom(String),
}
/// Request Origin
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub struct InnerOrigin {
protocol: OriginProtocol,
host: Host,
host_with_proto: String,
matcher: Matcher,
}
impl<T: AsRef<str>> From<T> for InnerOrigin {
fn from(origin: T) -> Self {
InnerOrigin::parse(origin.as_ref())
}
}
impl InnerOrigin {
fn with_host(protocol: OriginProtocol, host: Host) -> Self {
let host_with_proto = Self::host_with_proto(&protocol, &host);
let matcher = Matcher::new(&host_with_proto);
InnerOrigin { protocol, host, host_with_proto, matcher }
}
/// Creates new origin given protocol, hostname and port parts.
/// Pre-processes input data if necessary.
pub fn new<T: Into<Port>>(protocol: OriginProtocol, host: &str, port: T) -> Self {
Self::with_host(protocol, Host::new(host, port))
}
/// Attempts to parse given string as a `Origin`.
/// NOTE: This method always succeeds and falls back to sensible defaults.
pub fn parse(origin: &str) -> Self {
let mut parts = origin.split("://");
let proto = parts.next().expect("split always returns non-empty iterator.");
let hostname = parts.next();
let (proto, hostname) = match hostname {
None => (None, proto),
Some(hostname) => (Some(proto), hostname),
};
let proto = proto.map(str::to_lowercase);
let hostname = Host::parse(hostname);
let protocol = match proto {
None => OriginProtocol::Http,
Some(ref p) if p == "http" => OriginProtocol::Http,
Some(ref p) if p == "https" => OriginProtocol::Https,
Some(other) => OriginProtocol::Custom(other),
};
InnerOrigin::with_host(protocol, hostname)
}
fn host_with_proto(protocol: &OriginProtocol, host: &Host) -> String {
format!(
"{}://{}",
match *protocol {
OriginProtocol::Http => "http",
OriginProtocol::Https => "https",
OriginProtocol::Custom(ref protocol) => protocol,
},
&**host,
)
}
}
impl Pattern for InnerOrigin {
fn matches<T: AsRef<str>>(&self, other: T) -> bool {
self.matcher.matches(other)
}
}
impl ops::Deref for InnerOrigin {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.host_with_proto
}
}
/// Origin type allowed to access.
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Origin {
/// Specific origin.
Origin(InnerOrigin),
/// Null origin (file:///, sandboxed iframe).
Null,
/// Allow all origins i.e, the literal value "*" which is regarded as a wildcard.
Wildcard,
}
impl Pattern for Origin {
fn matches<T: AsRef<str>>(&self, other: T) -> bool {
if other.as_ref() == "null" {
return *self == Origin::Null;
}
match self {
Origin::Wildcard => true,
Origin::Null => false,
Origin::Origin(ref origin) => origin.matches(other),
}
}
}
impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
Self::Wildcard => "*",
Self::Null => "null",
Self::Origin(ref val) => val,
}
)
}
}
impl<T: Into<String>> From<T> for Origin {
fn from(s: T) -> Self {
match s.into().as_str() {
"all" | "*" | "any" => Self::Wildcard,
"null" => Self::Null,
origin => Self::Origin(origin.into()),
}
}
}
/// Policy for validating the `HTTP origin header`.
#[derive(Clone, Debug)]
pub enum AllowOrigins {
/// Allow all origins (no filter).
Any,
/// Allow only specified origins.
Only(Vec<Origin>),
}
impl AllowOrigins {
/// Verify a origin.
pub fn verify(&self, origin: Option<&str>, host: &str) -> Result<(), Error> {
// Nothing to be checked if origin is not part of the request's headers.
let origin = match origin {
Some(ref origin) => origin,
None => return Ok(()),
};
// Requests initiated from the same server are allowed.
if origin.ends_with(host) {
// Additional check
let origin = InnerOrigin::parse(origin);
if &*origin.host == host {
return Ok(());
}
}
match self {
AllowOrigins::Any => return Ok(()),
AllowOrigins::Only(list) => {
if !list.iter().any(|allowed_origin| allowed_origin.matches(*origin)) {
return Err(Error::HttpHeaderRejected("origin", origin.to_string()));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::access_control::host::Host;
#[test]
fn should_parse_origin() {
use self::OriginProtocol::*;
assert_eq!(InnerOrigin::parse("http://parity.io"), InnerOrigin::new(Http, "parity.io", None));
assert_eq!(InnerOrigin::parse("https://parity.io:8443"), InnerOrigin::new(Https, "parity.io", Some(8443)));
assert_eq!(
InnerOrigin::parse("chrome-extension://124.0.0.1"),
InnerOrigin::new(Custom("chrome-extension".into()), "124.0.0.1", None)
);
assert_eq!(InnerOrigin::parse("parity.io/somepath"), InnerOrigin::new(Http, "parity.io", None));
assert_eq!(InnerOrigin::parse("127.0.0.1:8545/somepath"), InnerOrigin::new(Http, "127.0.0.1", Some(8545)));
}
#[test]
fn should_not_allow_partially_matching_origin() {
let origin1 = InnerOrigin::parse("http://subdomain.somedomain.io");
let origin2 = InnerOrigin::parse("http://somedomain.io:8080");
let host = Host::parse("http://somedomain.io");
let origin1 = Some(&*origin1);
let origin2 = Some(&*origin2);
let allow_origins = AllowOrigins::Only(vec![]);
assert!(allow_origins.verify(origin1, &*host).is_err());
assert!(allow_origins.verify(origin2, &*host).is_err());
}
#[test]
fn should_allow_origins_that_matches_hosts() {
let origin = InnerOrigin::parse("http://127.0.0.1:8080");
let host = Host::parse("http://127.0.0.1:8080");
let origin = Some(&*origin);
let allow_origins = AllowOrigins::Any;
assert!(allow_origins.verify(origin, &*host).is_ok());
}
#[test]
fn should_allow_when_there_are_no_domains_and_no_origin() {
let origin = None;
let host = "";
let allow_origins = AllowOrigins::Any;
assert!(allow_origins.verify(origin, host).is_ok());
}
#[test]
fn should_allow_domain_when_all_are_allowed() {
let origin = Some("parity.io");
let host = "";
let allow_origins = AllowOrigins::Any;
assert!(allow_origins.verify(origin, host).is_ok());
}
#[test]
fn should_allow_for_empty_origin() {
let origin = None;
let host = "";
let allow_origins = AllowOrigins::Only(vec![Origin::Origin("http://ethereum.org".into())]);
assert!(allow_origins.verify(origin, host).is_ok());
}
#[test]
fn should_allow_specific_empty_list() {
let origin = None;
let host = "";
let allow_origins = AllowOrigins::Only(vec![]);
assert!(allow_origins.verify(origin, host).is_ok());
}
#[test]
fn should_deny_for_different_origin() {
let origin = Some("http://parity.io");
let host = "";
let allow_origins = AllowOrigins::Only(vec![Origin::Origin("http://ethereum.org".into())]);
assert!(allow_origins.verify(origin, host).is_err());
}
#[test]
fn should_allow_for_any() {
let origin = Some("http://parity.io");
let host = "";
let allow_origins = AllowOrigins::Only(vec![Origin::Wildcard]);
assert!(allow_origins.verify(origin, host).is_ok());
}
#[test]
fn should_allow_if_origin_is_not_defined() {
let origin = None;
let host = "";
let allow_origins = AllowOrigins::Only(vec![Origin::Null]);
assert!(allow_origins.verify(origin, host).is_ok());
}
#[test]
fn should_allow_if_origin_is_null() {
let origin = Some("null");
let host = "";
let allow_origins = AllowOrigins::Only(vec![Origin::Null]);
assert!(allow_origins.verify(origin, host).is_ok());
}
#[test]
fn should_allow_if_there_is_a_match() {
let origin = Some("http://parity.io");
let host = "";
let allow_origins = AllowOrigins::Only(vec![
Origin::Origin("http://ethereum.org".into()),
Origin::Origin("http://parity.io".into()),
]);
assert!(allow_origins.verify(origin, host).is_ok());
}
#[test]
fn should_support_wildcards() {
let origin1 = Some("http://parity.io");
let origin2 = Some("http://parity.iot");
let origin3 = Some("chrome-extension://test");