UI server refactoring (#5580)

* Full API in Authenticated WS server.

* Replacing UI server with Hyper.

* Solving CLI, RPCs and tests.

* Porting signer tests.

* Fixing origin recognition for dapps/rpc.

* Fixing tests. Adding parity-rpc-client to test.

* Dapps exposed as RPC method.

* JS code to support new connection scheme.

* Fixing dapps tests.

* Updating allowed origins/hosts to support web3.site.

* Fixing tests, fixing UI.

* Fixing tests.

* Removing invalid tests.

* Fixing merge.

* 404 fallback for UI

* Improve ContentFetcher constructor readability.

* Naming.

* Update .gitlab-ci.yml

fix CI lint error

* Fixing tests and linting issues.

* Fixing new tests.

* UI hosts.

* Submodules fix.
This commit is contained in:
Tomasz Drwięga
2017-05-24 12:24:07 +02:00
committed by Arkadiy Paronyan
parent 7499efecf6
commit cbcc369a2d
91 changed files with 2171 additions and 2591 deletions

View File

@@ -16,42 +16,27 @@
use std::sync::Arc;
use unicase::UniCase;
use hyper::{server, net, Decoder, Encoder, Next, Control};
use hyper::header;
use hyper::method::Method;
use api::types::{App, ApiError};
use api::types::ApiError;
use api::response;
use apps::fetcher::Fetcher;
use handlers::extract_url;
use endpoint::{Endpoint, Endpoints, Handler, EndpointPath};
use jsonrpc_http_server::{self, AccessControlAllowOrigin};
use endpoint::{Endpoint, Handler, EndpointPath};
#[derive(Clone)]
pub struct RestApi {
// TODO [ToDr] cors_domains should be handled by the server to avoid duplicated logic.
// RequestMiddleware should be able to tell that cors headers should be included.
cors_domains: Option<Vec<AccessControlAllowOrigin>>,
apps: Vec<App>,
fetcher: Arc<Fetcher>,
}
impl RestApi {
pub fn new(cors_domains: Vec<AccessControlAllowOrigin>, endpoints: &Endpoints, fetcher: Arc<Fetcher>) -> Box<Endpoint> {
pub fn new(fetcher: Arc<Fetcher>) -> Box<Endpoint> {
Box::new(RestApi {
cors_domains: Some(cors_domains),
apps: Self::list_apps(endpoints),
fetcher: fetcher,
})
}
fn list_apps(endpoints: &Endpoints) -> Vec<App> {
endpoints.iter().filter_map(|(ref k, ref e)| {
e.info().map(|ref info| App::from_info(k, info))
}).collect()
}
}
impl Endpoint for RestApi {
@@ -62,7 +47,6 @@ impl Endpoint for RestApi {
struct RestApiRouter {
api: RestApi,
cors_header: Option<header::AccessControlAllowOrigin>,
path: Option<EndpointPath>,
control: Option<Control>,
handler: Box<Handler>,
@@ -72,7 +56,6 @@ impl RestApiRouter {
fn new(api: RestApi, path: EndpointPath, control: Control) -> Self {
RestApiRouter {
path: Some(path),
cors_header: None,
control: Some(control),
api: api,
handler: response::as_json_error(&ApiError {
@@ -92,35 +75,10 @@ impl RestApiRouter {
_ => None
}
}
/// Returns basic headers for a response (it may be overwritten by the handler)
fn response_headers(cors_header: Option<header::AccessControlAllowOrigin>) -> header::Headers {
let mut headers = header::Headers::new();
if let Some(cors_header) = cors_header {
headers.set(header::AccessControlAllowCredentials);
headers.set(header::AccessControlAllowMethods(vec![
Method::Options,
Method::Post,
Method::Get,
]));
headers.set(header::AccessControlAllowHeaders(vec![
UniCase("origin".to_owned()),
UniCase("content-type".to_owned()),
UniCase("accept".to_owned()),
]));
headers.set(cors_header);
}
headers
}
}
impl server::Handler<net::HttpStream> for RestApiRouter {
fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next {
self.cors_header = jsonrpc_http_server::cors_header(&request, &self.api.cors_domains).into();
if let Method::Options = *request.method() {
self.handler = response::empty();
return Next::write();
@@ -144,7 +102,6 @@ impl server::Handler<net::HttpStream> for RestApiRouter {
if let Some(ref hash) = hash { path.app_id = hash.clone().to_owned() }
let handler = endpoint.and_then(|v| match v {
"apps" => Some(response::as_json(&self.api.apps)),
"ping" => Some(response::ping()),
"content" => self.resolve_content(hash, path, control),
_ => None
@@ -163,7 +120,6 @@ impl server::Handler<net::HttpStream> for RestApiRouter {
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
*res.headers_mut() = Self::response_headers(self.cors_header.take());
self.handler.on_response(res)
}

View File

@@ -21,4 +21,3 @@ mod response;
mod types;
pub use self::api::RestApi;
pub use self::types::App;

View File

@@ -23,12 +23,6 @@ pub fn empty() -> Box<Handler> {
Box::new(ContentHandler::ok("".into(), mime!(Text/Plain)))
}
pub fn as_json<T: Serialize>(val: &T) -> Box<Handler> {
let json = serde_json::to_string(val)
.expect("serialization to string is infallible; qed");
Box::new(ContentHandler::ok(json, mime!(Application/Json)))
}
pub fn as_json_error<T: Serialize>(val: &T) -> Box<Handler> {
let json = serde_json::to_string(val)
.expect("serialization to string is infallible; qed");

View File

@@ -14,46 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use endpoint::EndpointInfo;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct App {
pub id: String,
pub name: String,
pub description: String,
pub version: String,
pub author: String,
#[serde(rename="iconUrl")]
pub icon_url: String,
}
impl App {
/// Creates `App` instance from `EndpointInfo` and `id`.
pub fn from_info(id: &str, info: &EndpointInfo) -> Self {
App {
id: id.to_owned(),
name: info.name.to_owned(),
description: info.description.to_owned(),
version: info.version.to_owned(),
author: info.author.to_owned(),
icon_url: info.icon_url.to_owned(),
}
}
}
impl Into<EndpointInfo> for App {
fn into(self) -> EndpointInfo {
EndpointInfo {
name: self.name,
description: self.description,
version: self.version,
author: self.author,
icon_url: self.icon_url,
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApiError {

55
dapps/src/apps/app.rs Normal file
View File

@@ -0,0 +1,55 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use endpoint::EndpointInfo;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct App {
pub id: String,
pub name: String,
pub description: String,
pub version: String,
pub author: String,
#[serde(rename="iconUrl")]
pub icon_url: String,
}
impl App {
/// Creates `App` instance from `EndpointInfo` and `id`.
pub fn from_info(id: &str, info: &EndpointInfo) -> Self {
App {
id: id.to_owned(),
name: info.name.to_owned(),
description: info.description.to_owned(),
version: info.version.to_owned(),
author: info.author.to_owned(),
icon_url: info.icon_url.to_owned(),
}
}
}
impl Into<EndpointInfo> for App {
fn into(self) -> EndpointInfo {
EndpointInfo {
name: self.name,
description: self.description,
version: self.version,
author: self.author,
icon_url: self.icon_url,
}
}
}

View File

@@ -55,6 +55,7 @@ pub struct ContentFetcher<F: Fetch = FetchClient, R: URLHint + 'static = URLHint
embeddable_on: Option<(String, u16)>,
remote: Remote,
fetch: F,
only_content: bool,
}
impl<R: URLHint + 'static, F: Fetch> Drop for ContentFetcher<F, R> {
@@ -66,7 +67,12 @@ impl<R: URLHint + 'static, F: Fetch> Drop for ContentFetcher<F, R> {
impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
pub fn new(resolver: R, sync_status: Arc<SyncStatus>, embeddable_on: Option<(String, u16)>, remote: Remote, fetch: F) -> Self {
pub fn new(
resolver: R,
sync_status: Arc<SyncStatus>,
remote: Remote,
fetch: F,
) -> Self {
let mut dapps_path = env::temp_dir();
dapps_path.push(random_filename());
@@ -75,12 +81,23 @@ impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
resolver: resolver,
sync: sync_status,
cache: Arc::new(Mutex::new(ContentCache::default())),
embeddable_on: embeddable_on,
embeddable_on: None,
remote: remote,
fetch: fetch,
only_content: true,
}
}
pub fn allow_dapps(mut self, dapps: bool) -> Self {
self.only_content = !dapps;
self
}
pub fn embeddable_on(mut self, embeddable_on: Option<(String, u16)>) -> Self {
self.embeddable_on = embeddable_on;
self
}
fn still_syncing(address: Option<(String, u16)>) -> Box<Handler> {
Box::new(ContentHandler::error(
StatusCode::ServiceUnavailable,
@@ -91,6 +108,16 @@ impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
))
}
fn dapps_disabled(address: Option<(String, u16)>) -> Box<Handler> {
Box::new(ContentHandler::error(
StatusCode::ServiceUnavailable,
"Network Dapps Not Available",
"This interface doesn't support network dapps for security reasons.",
None,
address,
))
}
#[cfg(test)]
fn set_status(&self, content_id: &str, status: ContentStatus) {
self.cache.lock().insert(content_id.to_owned(), status);
@@ -163,6 +190,9 @@ impl<R: URLHint + 'static, F: Fetch> Fetcher for ContentFetcher<F, R> {
Some(URLHintResult::Dapp(_)) if self.sync.is_major_importing() => {
(None, Self::still_syncing(self.embeddable_on.clone()))
},
Some(URLHintResult::Dapp(_)) if self.only_content => {
(None, Self::dapps_disabled(self.embeddable_on.clone()))
},
Some(URLHintResult::Dapp(dapp)) => {
let handler = ContentFetcherHandler::new(
dapp.url(),
@@ -254,7 +284,8 @@ mod tests {
fn should_true_if_contains_the_app() {
// given
let path = env::temp_dir();
let fetcher = ContentFetcher::new(FakeResolver, Arc::new(|| false), None, Remote::new_sync(), Client::new().unwrap());
let fetcher = ContentFetcher::new(FakeResolver, Arc::new(|| false), Remote::new_sync(), Client::new().unwrap())
.allow_dapps(true);
let handler = LocalPageEndpoint::new(path, EndpointInfo {
name: "fake".into(),
description: "".into(),

View File

@@ -14,12 +14,13 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::collections::BTreeMap;
use std::io;
use std::io::Read;
use std::fs;
use std::path::{Path, PathBuf};
use page::{LocalPageEndpoint, PageCache};
use endpoint::{Endpoints, EndpointInfo};
use endpoint::{Endpoint, EndpointInfo};
use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest};
struct LocalDapp {
@@ -85,8 +86,8 @@ fn local_dapp(name: String, path: PathBuf) -> LocalDapp {
/// Returns endpoints for Local Dapps found for given filesystem path.
/// Scans the directory and collects `LocalPageEndpoints`.
pub fn local_endpoints<P: AsRef<Path>>(dapps_path: P, signer_address: Option<(String, u16)>) -> Endpoints {
let mut pages = Endpoints::new();
pub fn local_endpoints<P: AsRef<Path>>(dapps_path: P, signer_address: Option<(String, u16)>) -> BTreeMap<String, Box<Endpoint>> {
let mut pages = BTreeMap::<String, Box<Endpoint>>::new();
for dapp in local_dapps(dapps_path.as_ref()) {
pages.insert(
dapp.id,

View File

@@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use serde_json;
pub use api::App as Manifest;
pub use apps::App as Manifest;
pub const MANIFEST_FILENAME: &'static str = "manifest.json";

View File

@@ -14,8 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use endpoint::{Endpoints, Endpoint};
use page::PageEndpoint;
use proxypac::ProxyPac;
@@ -23,17 +25,19 @@ use web::Web;
use fetch::Fetch;
use parity_dapps::WebApp;
use parity_reactor::Remote;
use parity_ui;
use {WebProxyTokens};
mod app;
mod cache;
mod fs;
mod ui;
pub mod fetcher;
pub mod manifest;
extern crate parity_ui;
pub use self::app::App;
pub const HOME_PAGE: &'static str = "parity";
pub const DAPPS_DOMAIN: &'static str = ".web3.site";
pub const HOME_PAGE: &'static str = "home";
pub const RPC_PATH: &'static str = "rpc";
pub const API_PATH: &'static str = "api";
pub const UTILS_PATH: &'static str = "parity-utils";
@@ -44,18 +48,27 @@ pub fn utils() -> Box<Endpoint> {
Box::new(PageEndpoint::with_prefix(parity_ui::App::default(), UTILS_PATH.to_owned()))
}
pub fn ui() -> Box<Endpoint> {
Box::new(PageEndpoint::with_fallback_to_index(parity_ui::App::default()))
}
pub fn ui_redirection(ui_address: Option<(String, u16)>) -> Box<Endpoint> {
Box::new(ui::Redirection::new(ui_address))
}
pub fn all_endpoints<F: Fetch>(
dapps_path: PathBuf,
extra_dapps: Vec<PathBuf>,
signer_address: Option<(String, u16)>,
dapps_domain: String,
ui_address: Option<(String, u16)>,
web_proxy_tokens: Arc<WebProxyTokens>,
remote: Remote,
fetch: F,
) -> Endpoints {
// fetch fs dapps at first to avoid overwriting builtins
let mut pages = fs::local_endpoints(dapps_path, signer_address.clone());
let mut pages = fs::local_endpoints(dapps_path, ui_address.clone());
for path in extra_dapps {
if let Some((id, endpoint)) = fs::local_endpoint(path.clone(), signer_address.clone()) {
if let Some((id, endpoint)) = fs::local_endpoint(path.clone(), ui_address.clone()) {
pages.insert(id, endpoint);
} else {
warn!(target: "dapps", "Ignoring invalid dapp at {}", path.display());
@@ -63,14 +76,14 @@ pub fn all_endpoints<F: Fetch>(
}
// NOTE [ToDr] Dapps will be currently embeded on 8180
insert::<parity_ui::App>(&mut pages, "ui", Embeddable::Yes(signer_address.clone()));
pages.insert("proxy".into(), ProxyPac::boxed(signer_address.clone()));
pages.insert(WEB_PATH.into(), Web::boxed(signer_address.clone(), web_proxy_tokens.clone(), remote.clone(), fetch.clone()));
insert::<parity_ui::App>(&mut pages, "ui", Embeddable::Yes(ui_address.clone()));
pages.insert("proxy".into(), ProxyPac::boxed(ui_address.clone(), dapps_domain));
pages.insert(WEB_PATH.into(), Web::boxed(ui_address.clone(), web_proxy_tokens.clone(), remote.clone(), fetch.clone()));
pages
Arc::new(pages)
}
fn insert<T : WebApp + Default + 'static>(pages: &mut Endpoints, id: &str, embed_at: Embeddable) {
fn insert<T : WebApp + Default + 'static>(pages: &mut BTreeMap<String, Box<Endpoint>>, id: &str, embed_at: Embeddable) {
pages.insert(id.to_owned(), Box::new(match embed_at {
Embeddable::Yes(address) => PageEndpoint::new_safe_to_embed(T::default(), address),
Embeddable::No => PageEndpoint::new(T::default()),

55
dapps/src/apps/ui.rs Normal file
View File

@@ -0,0 +1,55 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! UI redirections
use hyper::{Control, StatusCode};
use endpoint::{Endpoint, Handler, EndpointPath};
use {address, handlers};
/// Redirection to UI server.
pub struct Redirection {
signer_address: Option<(String, u16)>,
}
impl Redirection {
pub fn new(
signer_address: Option<(String, u16)>,
) -> Self {
Redirection {
signer_address: signer_address,
}
}
}
impl Endpoint for Redirection {
fn to_async_handler(&self, _path: EndpointPath, _control: Control) -> Box<Handler> {
if let Some(ref signer_address) = self.signer_address {
trace!(target: "dapps", "Redirecting to signer interface.");
handlers::Redirection::boxed(&format!("http://{}", address(signer_address)))
} else {
trace!(target: "dapps", "Signer disabled, returning 404.");
Box::new(handlers::ContentHandler::error(
StatusCode::NotFound,
"404 Not Found",
"Your homepage is not available when Trusted Signer is disabled.",
Some("You can still access dapps by writing a correct address, though. Re-enable Signer to get your homepage back."),
self.signer_address.clone(),
))
}
}
}

View File

@@ -16,6 +16,7 @@
//! URL Endpoint traits
use std::sync::Arc;
use std::collections::BTreeMap;
use hyper::{self, server, net};
@@ -38,7 +39,7 @@ pub struct EndpointInfo {
pub icon_url: String,
}
pub type Endpoints = BTreeMap<String, Box<Endpoint>>;
pub type Endpoints = Arc<BTreeMap<String, Box<Endpoint>>>;
pub type Handler = server::Handler<net::HttpStream> + Send;
pub trait Endpoint : Send + Sync {

View File

@@ -40,6 +40,7 @@ extern crate fetch;
extern crate parity_dapps_glue as parity_dapps;
extern crate parity_hash_fetch as hash_fetch;
extern crate parity_reactor;
extern crate parity_ui;
#[macro_use]
extern crate log;
@@ -70,7 +71,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::collections::HashMap;
use jsonrpc_http_server::{self as http, hyper, AccessControlAllowOrigin};
use jsonrpc_http_server::{self as http, hyper};
use fetch::Fetch;
use parity_reactor::Remote;
@@ -97,18 +98,74 @@ impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync {
fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) }
}
/// Current supported endpoints.
pub struct Endpoints {
endpoints: endpoint::Endpoints,
}
impl Endpoints {
/// Returns a current list of app endpoints.
pub fn list(&self) -> Vec<apps::App> {
self.endpoints.iter().filter_map(|(ref k, ref e)| {
e.info().map(|ref info| apps::App::from_info(k, info))
}).collect()
}
}
/// Dapps server as `jsonrpc-http-server` request middleware.
pub struct Middleware {
router: router::Router,
endpoints: endpoint::Endpoints,
}
impl Middleware {
/// Creates new Dapps server middleware.
pub fn new<F: Fetch + Clone>(
/// Get local endpoints handle.
pub fn endpoints(&self) -> Endpoints {
Endpoints {
endpoints: self.endpoints.clone(),
}
}
/// Creates new middleware for UI server.
pub fn ui<F: Fetch + Clone>(
remote: Remote,
signer_address: Option<(String, u16)>,
registrar: Arc<ContractClient>,
sync_status: Arc<SyncStatus>,
fetch: F,
dapps_domain: String,
) -> Self {
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(
hash_fetch::urlhint::URLHintContract::new(registrar),
sync_status,
remote.clone(),
fetch.clone(),
).embeddable_on(None).allow_dapps(false));
let special = {
let mut special = special_endpoints(content_fetcher.clone());
special.insert(router::SpecialEndpoint::Home, Some(apps::ui()));
special
};
let router = router::Router::new(
content_fetcher,
None,
special,
None,
dapps_domain,
);
Middleware {
router: router,
endpoints: Default::default(),
}
}
/// Creates new Dapps server middleware.
pub fn dapps<F: Fetch + Clone>(
remote: Remote,
ui_address: Option<(String, u16)>,
dapps_path: PathBuf,
extra_dapps: Vec<PathBuf>,
dapps_domain: String,
registrar: Arc<ContractClient>,
sync_status: Arc<SyncStatus>,
web_proxy_tokens: Arc<WebProxyTokens>,
@@ -117,45 +174,36 @@ impl Middleware {
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(
hash_fetch::urlhint::URLHintContract::new(registrar),
sync_status,
signer_address.clone(),
remote.clone(),
fetch.clone(),
));
).embeddable_on(ui_address.clone()).allow_dapps(true));
let endpoints = apps::all_endpoints(
dapps_path,
extra_dapps,
signer_address.clone(),
dapps_domain.clone(),
ui_address.clone(),
web_proxy_tokens,
remote.clone(),
fetch.clone(),
);
let cors_domains = cors_domains(signer_address.clone());
let special = {
let mut special = HashMap::new();
special.insert(router::SpecialEndpoint::Rpc, None);
special.insert(router::SpecialEndpoint::Utils, Some(apps::utils()));
special.insert(
router::SpecialEndpoint::Api,
Some(api::RestApi::new(
cors_domains.clone(),
&endpoints,
content_fetcher.clone()
)),
);
let mut special = special_endpoints(content_fetcher.clone());
special.insert(router::SpecialEndpoint::Home, Some(apps::ui_redirection(ui_address.clone())));
special
};
let router = router::Router::new(
signer_address,
content_fetcher,
endpoints,
Some(endpoints.clone()),
special,
ui_address,
dapps_domain,
);
Middleware {
router: router,
endpoints: endpoints,
}
}
}
@@ -166,21 +214,12 @@ impl http::RequestMiddleware for Middleware {
}
}
/// Returns a list of CORS domains for API endpoint.
fn cors_domains(signer_address: Option<(String, u16)>) -> Vec<AccessControlAllowOrigin> {
use self::apps::{HOME_PAGE, DAPPS_DOMAIN};
match signer_address {
Some(signer_address) => [
format!("http://{}{}", HOME_PAGE, DAPPS_DOMAIN),
format!("http://{}{}:{}", HOME_PAGE, DAPPS_DOMAIN, signer_address.1),
format!("http://{}", address(&signer_address)),
format!("https://{}{}", HOME_PAGE, DAPPS_DOMAIN),
format!("https://{}{}:{}", HOME_PAGE, DAPPS_DOMAIN, signer_address.1),
format!("https://{}", address(&signer_address)),
].into_iter().map(|val| AccessControlAllowOrigin::Value(val.into())).collect(),
None => vec![],
}
fn special_endpoints(content_fetcher: Arc<apps::fetcher::Fetcher>) -> HashMap<router::SpecialEndpoint, Option<Box<endpoint::Endpoint>>> {
let mut special = HashMap::new();
special.insert(router::SpecialEndpoint::Rpc, None);
special.insert(router::SpecialEndpoint::Utils, Some(apps::utils()));
special.insert(router::SpecialEndpoint::Api, Some(api::RestApi::new(content_fetcher)));
special
}
fn address(address: &(String, u16)) -> String {
@@ -193,29 +232,3 @@ fn random_filename() -> String {
let mut rng = ::rand::OsRng::new().unwrap();
rng.gen_ascii_chars().take(12).collect()
}
#[cfg(test)]
mod util_tests {
use super::cors_domains;
use jsonrpc_http_server::AccessControlAllowOrigin;
#[test]
fn should_return_cors_domains() {
// given
// when
let none = cors_domains(None);
let some = cors_domains(Some(("127.0.0.1".into(), 18180)));
// then
assert_eq!(none, Vec::<AccessControlAllowOrigin>::new());
assert_eq!(some, vec![
"http://parity.web3.site".into(),
"http://parity.web3.site:18180".into(),
"http://127.0.0.1:18180".into(),
"https://parity.web3.site".into(),
"https://parity.web3.site:18180".into(),
"https://127.0.0.1:18180".into(),
]);
}
}

View File

@@ -27,6 +27,7 @@ pub struct PageEndpoint<T : WebApp + 'static> {
/// Safe to be loaded in frame by other origin. (use wisely!)
safe_to_embed_on: Option<(String, u16)>,
info: EndpointInfo,
fallback_to_index_html: bool,
}
impl<T: WebApp + 'static> PageEndpoint<T> {
@@ -38,6 +39,20 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
prefix: None,
safe_to_embed_on: None,
info: EndpointInfo::from(info),
fallback_to_index_html: false,
}
}
/// Creates a new `PageEndpoint` for builtin (compile time) Dapp.
/// Instead of returning 404 this endpoint will always server index.html.
pub fn with_fallback_to_index(app: T) -> Self {
let info = app.info();
PageEndpoint {
app: Arc::new(app),
prefix: None,
safe_to_embed_on: None,
info: EndpointInfo::from(info),
fallback_to_index_html: true,
}
}
@@ -51,6 +66,7 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
prefix: Some(prefix),
safe_to_embed_on: None,
info: EndpointInfo::from(info),
fallback_to_index_html: false,
}
}
@@ -64,6 +80,7 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
prefix: None,
safe_to_embed_on: address,
info: EndpointInfo::from(info),
fallback_to_index_html: false,
}
}
}
@@ -76,7 +93,7 @@ impl<T: WebApp> Endpoint for PageEndpoint<T> {
fn to_handler(&self, path: EndpointPath) -> Box<Handler> {
Box::new(handler::PageHandler {
app: BuiltinDapp::new(self.app.clone()),
app: BuiltinDapp::new(self.app.clone(), self.fallback_to_index_html),
prefix: self.prefix.clone(),
path: path,
file: handler::ServedFile::new(self.safe_to_embed_on.clone()),
@@ -100,12 +117,14 @@ impl From<Info> for EndpointInfo {
struct BuiltinDapp<T: WebApp + 'static> {
app: Arc<T>,
fallback_to_index_html: bool,
}
impl<T: WebApp + 'static> BuiltinDapp<T> {
fn new(app: Arc<T>) -> Self {
fn new(app: Arc<T>, fallback_to_index_html: bool) -> Self {
BuiltinDapp {
app: app,
fallback_to_index_html: fallback_to_index_html,
}
}
}
@@ -114,13 +133,19 @@ impl<T: WebApp + 'static> handler::Dapp for BuiltinDapp<T> {
type DappFile = BuiltinDappFile<T>;
fn file(&self, path: &str) -> Option<Self::DappFile> {
self.app.file(path).map(|_| {
let file = |path| self.app.file(path).map(|_| {
BuiltinDappFile {
app: self.app.clone(),
path: path.into(),
write_pos: 0,
}
})
});
let res = file(path);
if self.fallback_to_index_html {
res.or_else(|| file("index.html"))
} else {
res
}
}
}

View File

@@ -18,17 +18,19 @@
use endpoint::{Endpoint, Handler, EndpointPath};
use handlers::ContentHandler;
use apps::{HOME_PAGE, DAPPS_DOMAIN};
use apps::HOME_PAGE;
use address;
pub struct ProxyPac {
signer_address: Option<(String, u16)>,
dapps_domain: String,
}
impl ProxyPac {
pub fn boxed(signer_address: Option<(String, u16)>) -> Box<Endpoint> {
pub fn boxed(signer_address: Option<(String, u16)>, dapps_domain: String) -> Box<Endpoint> {
Box::new(ProxyPac {
signer_address: signer_address
signer_address: signer_address,
dapps_domain: dapps_domain,
})
}
}
@@ -43,12 +45,12 @@ impl Endpoint for ProxyPac {
let content = format!(
r#"
function FindProxyForURL(url, host) {{
if (shExpMatch(host, "{0}{1}"))
if (shExpMatch(host, "{0}.{1}"))
{{
return "PROXY {4}";
}}
if (shExpMatch(host, "*{1}"))
if (shExpMatch(host, "*.{1}"))
{{
return "PROXY {2}:{3}";
}}
@@ -56,7 +58,7 @@ function FindProxyForURL(url, host) {{
return "DIRECT";
}}
"#,
HOME_PAGE, DAPPS_DOMAIN, path.host, path.port, signer);
HOME_PAGE, self.dapps_domain, path.host, path.port, signer);
Box::new(ContentHandler::ok(content, mime!(Application/Javascript)))
}

View File

@@ -17,20 +17,19 @@
//! Router implementation
//! Dispatch requests to proper application.
use address;
use std::cmp;
use std::sync::Arc;
use std::collections::HashMap;
use url::{Url, Host};
use hyper::{self, server, header, Control, StatusCode};
use hyper::{self, server, header, Control};
use hyper::net::HttpStream;
use jsonrpc_http_server as http;
use apps::{self, DAPPS_DOMAIN};
use apps;
use apps::fetcher::Fetcher;
use endpoint::{Endpoint, Endpoints, EndpointPath, Handler};
use handlers::{self, Redirection, ContentHandler};
use handlers;
/// Special endpoints are accessible on every domain (every dapp)
#[derive(Debug, PartialEq, Hash, Eq)]
@@ -38,26 +37,28 @@ pub enum SpecialEndpoint {
Rpc,
Api,
Utils,
Home,
None,
}
pub struct Router {
signer_address: Option<(String, u16)>,
endpoints: Endpoints,
endpoints: Option<Endpoints>,
fetch: Arc<Fetcher>,
special: HashMap<SpecialEndpoint, Option<Box<Endpoint>>>,
embeddable_on: Option<(String, u16)>,
dapps_domain: String,
}
impl http::RequestMiddleware for Router {
fn on_request(&self, req: &server::Request<HttpStream>, control: &Control) -> http::RequestMiddlewareAction {
// Choose proper handler depending on path / domain
let url = handlers::extract_url(req);
let endpoint = extract_endpoint(&url);
let referer = extract_referer_endpoint(req);
let endpoint = extract_endpoint(&url, &self.dapps_domain);
let referer = extract_referer_endpoint(req, &self.dapps_domain);
let is_utils = endpoint.1 == SpecialEndpoint::Utils;
let is_dapps_domain = endpoint.0.as_ref().map(|endpoint| endpoint.using_dapps_domains).unwrap_or(false);
let is_origin_set = req.headers().get::<http::hyper::header::Origin>().is_some();
let is_origin_set = req.headers().get::<header::Origin>().is_some();
let is_get_request = *req.method() == hyper::Method::Get;
let is_head_request = *req.method() == hyper::Method::Head;
trace!(target: "dapps", "Routing request to {:?}. Details: {:?}", url, req);
@@ -67,7 +68,7 @@ impl http::RequestMiddleware for Router {
// Handle invalid web requests that we can recover from
(ref path, SpecialEndpoint::None, Some((ref referer, ref referer_url)))
if referer.app_id == apps::WEB_PATH
&& self.endpoints.contains_key(apps::WEB_PATH)
&& self.endpoints.as_ref().map(|ep| ep.contains_key(apps::WEB_PATH)).unwrap_or(false)
&& !is_web_endpoint(path)
=>
{
@@ -75,7 +76,7 @@ impl http::RequestMiddleware for Router {
let len = cmp::min(referer_url.path.len(), 2); // /web/<encoded>/
let base = referer_url.path[..len].join("/");
let requested = url.map(|u| u.path.join("/")).unwrap_or_default();
Some(Redirection::boxed(&format!("/{}/{}", base, requested)))
Some(handlers::Redirection::boxed(&format!("/{}/{}", base, requested)))
},
// First check special endpoints
(ref path, ref endpoint, _) if self.special.contains_key(endpoint) => {
@@ -86,9 +87,12 @@ impl http::RequestMiddleware for Router {
.map(|special| special.to_async_handler(path.clone().unwrap_or_default(), control))
},
// Then delegate to dapp
(Some(ref path), _, _) if self.endpoints.contains_key(&path.app_id) => {
(Some(ref path), _, _) if self.endpoints.as_ref().map(|ep| ep.contains_key(&path.app_id)).unwrap_or(false) => {
trace!(target: "dapps", "Resolving to local/builtin dapp.");
Some(self.endpoints.get(&path.app_id)
Some(self.endpoints
.as_ref()
.expect("endpoints known to be set; qed")
.get(&path.app_id)
.expect("endpoints known to contain key; qed")
.to_async_handler(path.clone(), control))
},
@@ -97,36 +101,28 @@ impl http::RequestMiddleware for Router {
trace!(target: "dapps", "Resolving to fetchable content.");
Some(self.fetch.to_async_handler(path.clone(), control))
},
// NOTE [todr] /home is redirected to home page since some users may have the redirection cached
// (in the past we used 301 instead of 302)
// It should be safe to remove it in (near) future.
//
// 404 for non-existent content
(Some(ref path), _, _) if is_get_request && path.app_id != "home" => {
// 404 for non-existent content (only if serving endpoints and not homepage)
(Some(ref path), _, _)
if (is_get_request || is_head_request)
&& self.endpoints.is_some()
&& path.app_id != apps::HOME_PAGE
=>
{
trace!(target: "dapps", "Resolving to 404.");
Some(Box::new(ContentHandler::error(
StatusCode::NotFound,
Some(Box::new(handlers::ContentHandler::error(
hyper::StatusCode::NotFound,
"404 Not Found",
"Requested content was not found.",
None,
self.signer_address.clone(),
self.embeddable_on.clone(),
)))
},
// Redirect any other GET request to signer.
_ if is_get_request => {
if let Some(ref signer_address) = self.signer_address {
trace!(target: "dapps", "Redirecting to signer interface.");
Some(Redirection::boxed(&format!("http://{}", address(signer_address))))
} else {
trace!(target: "dapps", "Signer disabled, returning 404.");
Some(Box::new(ContentHandler::error(
StatusCode::NotFound,
"404 Not Found",
"Your homepage is not available when Trusted Signer is disabled.",
Some("You can still access dapps by writing a correct address, though. Re-enable Signer to get your homepage back."),
self.signer_address.clone(),
)))
}
// Any other GET|HEAD requests to home page.
_ if (is_get_request || is_head_request) && self.special.contains_key(&SpecialEndpoint::Home) => {
self.special.get(&SpecialEndpoint::Home)
.expect("special known to contain key; qed")
.as_ref()
.map(|special| special.to_async_handler(Default::default(), control))
},
// RPC by default
_ => {
@@ -137,7 +133,7 @@ impl http::RequestMiddleware for Router {
match handler {
Some(handler) => http::RequestMiddlewareAction::Respond {
should_validate_hosts: !(is_utils || is_dapps_domain),
should_validate_hosts: !is_utils,
handler: handler,
},
None => http::RequestMiddlewareAction::Proceed {
@@ -149,16 +145,18 @@ impl http::RequestMiddleware for Router {
impl Router {
pub fn new(
signer_address: Option<(String, u16)>,
content_fetcher: Arc<Fetcher>,
endpoints: Endpoints,
endpoints: Option<Endpoints>,
special: HashMap<SpecialEndpoint, Option<Box<Endpoint>>>,
embeddable_on: Option<(String, u16)>,
dapps_domain: String,
) -> Self {
Router {
signer_address: signer_address,
endpoints: endpoints,
fetch: content_fetcher,
special: special,
embeddable_on: embeddable_on,
dapps_domain: format!(".{}", dapps_domain),
}
}
}
@@ -170,19 +168,19 @@ fn is_web_endpoint(path: &Option<EndpointPath>) -> bool {
}
}
fn extract_referer_endpoint(req: &server::Request<HttpStream>) -> Option<(EndpointPath, Url)> {
fn extract_referer_endpoint(req: &server::Request<HttpStream>, dapps_domain: &str) -> Option<(EndpointPath, Url)> {
let referer = req.headers().get::<header::Referer>();
let url = referer.and_then(|referer| Url::parse(&referer.0).ok());
url.and_then(|url| {
let option = Some(url);
extract_url_referer_endpoint(&option).or_else(|| {
extract_endpoint(&option).0.map(|endpoint| (endpoint, option.expect("Just wrapped; qed")))
extract_url_referer_endpoint(&option, dapps_domain).or_else(|| {
extract_endpoint(&option, dapps_domain).0.map(|endpoint| (endpoint, option.expect("Just wrapped; qed")))
})
})
}
fn extract_url_referer_endpoint(url: &Option<Url>) -> Option<(EndpointPath, Url)> {
fn extract_url_referer_endpoint(url: &Option<Url>, dapps_domain: &str) -> Option<(EndpointPath, Url)> {
let query = url.as_ref().and_then(|url| url.query.as_ref());
match (url, query) {
(&Some(ref url), Some(ref query)) if query.starts_with(apps::URL_REFERER) => {
@@ -190,7 +188,7 @@ fn extract_url_referer_endpoint(url: &Option<Url>) -> Option<(EndpointPath, Url)
debug!(target: "dapps", "Recovering referer from query parameter: {}", referer_url);
let referer_url = Url::parse(&referer_url).ok();
extract_endpoint(&referer_url).0.map(|endpoint| {
extract_endpoint(&referer_url, dapps_domain).0.map(|endpoint| {
(endpoint, referer_url.expect("Endpoint returned only when url `is_some`").clone())
})
},
@@ -198,7 +196,7 @@ fn extract_url_referer_endpoint(url: &Option<Url>) -> Option<(EndpointPath, Url)
}
}
fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint) {
fn extract_endpoint(url: &Option<Url>, dapps_domain: &str) -> (Option<EndpointPath>, SpecialEndpoint) {
fn special_endpoint(url: &Url) -> SpecialEndpoint {
if url.path.len() <= 1 {
return SpecialEndpoint::None;
@@ -208,14 +206,15 @@ fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint
apps::RPC_PATH => SpecialEndpoint::Rpc,
apps::API_PATH => SpecialEndpoint::Api,
apps::UTILS_PATH => SpecialEndpoint::Utils,
apps::HOME_PAGE => SpecialEndpoint::Home,
_ => SpecialEndpoint::None,
}
}
match *url {
Some(ref url) => match url.host {
Host::Domain(ref domain) if domain.ends_with(DAPPS_DOMAIN) => {
let id = &domain[0..(domain.len() - DAPPS_DOMAIN.len())];
Host::Domain(ref domain) if domain.ends_with(dapps_domain) => {
let id = &domain[0..(domain.len() - dapps_domain.len())];
let (id, params) = if let Some(split) = id.rfind('.') {
let (params, id) = id.split_at(split);
(id[1..].to_owned(), [params.to_owned()].into_iter().chain(&url.path).cloned().collect())
@@ -249,11 +248,12 @@ fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint
#[test]
fn should_extract_endpoint() {
assert_eq!(extract_endpoint(&None), (None, SpecialEndpoint::None));
let dapps_domain = ".web3.site";
assert_eq!(extract_endpoint(&None, dapps_domain), (None, SpecialEndpoint::None));
// With path prefix
assert_eq!(
extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok()),
extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["index.html".to_owned()],
@@ -265,7 +265,7 @@ fn should_extract_endpoint() {
// With path prefix
assert_eq!(
extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok()),
extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "rpc".to_owned(),
app_params: vec!["".to_owned()],
@@ -276,7 +276,7 @@ fn should_extract_endpoint() {
);
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/parity-utils/inject.js").ok()),
extract_endpoint(&Url::parse("http://my.status.web3.site/parity-utils/inject.js").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "parity-utils".into(), "inject.js".into()],
@@ -288,7 +288,7 @@ fn should_extract_endpoint() {
// By Subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://status.web3.site/test.html").ok()),
extract_endpoint(&Url::parse("http://status.web3.site/test.html").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["test.html".to_owned()],
@@ -300,7 +300,7 @@ fn should_extract_endpoint() {
// RPC by subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/rpc/").ok()),
extract_endpoint(&Url::parse("http://my.status.web3.site/rpc/").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "rpc".into(), "".into()],
@@ -312,7 +312,7 @@ fn should_extract_endpoint() {
// API by subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/api/").ok()),
extract_endpoint(&Url::parse("http://my.status.web3.site/api/").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "api".into(), "".into()],

View File

@@ -1,97 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::sync::Arc;
use hyper;
use parity_rpc::{Metadata, Origin};
use jsonrpc_core::{Middleware, MetaIoHandler};
use jsonrpc_http_server::{self as http, AccessControlAllowOrigin, HttpMetaExtractor};
use jsonrpc_http_server::tokio_core::reactor::Remote;
use endpoint::{Endpoint, EndpointPath, Handler};
pub fn rpc<T: Middleware<Metadata>>(
handler: MetaIoHandler<Metadata, T>,
remote: Remote,
cors_domains: Vec<AccessControlAllowOrigin>,
) -> Box<Endpoint> {
Box::new(RpcEndpoint {
handler: Arc::new(handler),
remote: remote,
meta_extractor: Arc::new(MetadataExtractor),
cors_domain: Some(cors_domains),
// NOTE [ToDr] We don't need to do any hosts validation here. It's already done in router.
allowed_hosts: None,
})
}
struct RpcEndpoint<T: Middleware<Metadata>> {
handler: Arc<MetaIoHandler<Metadata, T>>,
remote: Remote,
meta_extractor: Arc<HttpMetaExtractor<Metadata>>,
cors_domain: Option<Vec<AccessControlAllowOrigin>>,
allowed_hosts: Option<Vec<http::Host>>,
}
impl<T: Middleware<Metadata>> Endpoint for RpcEndpoint<T> {
fn to_async_handler(&self, _path: EndpointPath, control: hyper::Control) -> Box<Handler> {
Box::new(http::ServerHandler::new(
http::Rpc {
handler: self.handler.clone(),
remote: self.remote.clone(),
extractor: self.meta_extractor.clone(),
},
self.cors_domain.clone(),
self.allowed_hosts.clone(),
Arc::new(NoopMiddleware),
control,
))
}
}
#[derive(Default)]
struct NoopMiddleware;
impl http::RequestMiddleware for NoopMiddleware {
fn on_request(&self, request: &http::hyper::server::Request<http::hyper::net::HttpStream>, _control: &http::hyper::Control) -> http::RequestMiddlewareAction {
http::RequestMiddlewareAction::Proceed {
should_continue_on_invalid_cors: request.headers().get::<http::hyper::header::Origin>().is_none(),
}
}
}
pub struct MetadataExtractor;
impl HttpMetaExtractor<Metadata> for MetadataExtractor {
fn read_metadata(&self, request: &http::hyper::server::Request<http::hyper::net::HttpStream>) -> Metadata {
let dapp_id = request.headers().get::<http::hyper::header::Origin>()
.map(|origin| format!("{}://{}", origin.scheme, origin.host))
.or_else(|| {
// fallback to custom header, but only if origin is null
request.headers().get_raw("origin")
.and_then(|raw| raw.one())
.and_then(|raw| if raw == "null".as_bytes() {
request.headers().get_raw("x-parity-origin")
.and_then(|raw| raw.one())
.map(|raw| String::from_utf8_lossy(raw).into_owned())
} else {
None
})
});
Metadata {
origin: Origin::Dapps(dapp_id.map(Into::into).unwrap_or_default()),
}
}
}

View File

@@ -39,29 +39,6 @@ fn should_return_error() {
assert_security_headers(&response.headers);
}
#[test]
fn should_serve_apps() {
// given
let server = serve();
// when
let response = request(server,
"\
GET /api/apps HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Content-Type", "application/json");
assert!(response.body.contains("Parity UI"), response.body);
assert_security_headers(&response.headers);
}
#[test]
fn should_handle_ping() {
// given
@@ -106,92 +83,3 @@ fn should_try_to_resolve_dapp() {
assert_eq!(registrar.calls.lock().len(), 2);
assert_security_headers(&response.headers);
}
#[test]
fn should_return_signer_port_cors_headers() {
// given
let server = serve();
// when
let response = request(server,
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: http://127.0.0.1:18180\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Access-Control-Allow-Origin", "http://127.0.0.1:18180");
}
#[test]
fn should_return_signer_port_cors_headers_for_home_parity() {
// given
let server = serve();
// when
let response = request(server,
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: http://parity.web3.site\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Access-Control-Allow-Origin", "http://parity.web3.site");
}
#[test]
fn should_return_signer_port_cors_headers_for_home_parity_with_https() {
// given
let server = serve();
// when
let response = request(server,
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: https://parity.web3.site\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Access-Control-Allow-Origin", "https://parity.web3.site");
}
#[test]
fn should_return_signer_port_cors_headers_for_home_parity_with_port() {
// given
let server = serve();
// when
let response = request(server,
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: http://parity.web3.site:18180\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Access-Control-Allow-Origin", "http://parity.web3.site:18180");
}

View File

@@ -26,7 +26,7 @@ use jsonrpc_http_server::{self as http, Host, DomainsValidation};
use devtools::http_client;
use hash_fetch::urlhint::ContractClient;
use fetch::{Fetch, Client as FetchClient};
use parity_reactor::{EventLoop, Remote};
use parity_reactor::Remote;
use {Middleware, SyncStatus, WebProxyTokens};
@@ -47,20 +47,7 @@ fn init_logger() {
}
}
pub struct ServerLoop {
pub server: Server,
pub event_loop: EventLoop,
}
impl ::std::ops::Deref for ServerLoop {
type Target = Server;
fn deref(&self) -> &Self::Target {
&self.server
}
}
pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (ServerLoop, Arc<FakeRegistrar>) where
pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (Server, Arc<FakeRegistrar>) where
F: FnOnce(ServerBuilder) -> ServerBuilder<B>,
B: Fetch,
{
@@ -69,44 +56,41 @@ pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (ServerLo
let mut dapps_path = env::temp_dir();
dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading");
// TODO [ToDr] When https://github.com/paritytech/jsonrpc/issues/26 is resolved
// this additional EventLoop wouldn't be needed, we should be able to re-use remote.
let event_loop = EventLoop::spawn();
let server = process(ServerBuilder::new(
&dapps_path, registrar.clone(), remote,
))
.signer_address(Some(("127.0.0.1".into(), SIGNER_PORT)))
.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), io).unwrap();
(
ServerLoop { server: server, event_loop: event_loop },
server,
registrar,
)
}
pub fn serve_with_rpc(io: IoHandler) -> ServerLoop {
pub fn serve_with_rpc(io: IoHandler) -> Server {
init_server(|builder| builder, io, Remote::new_sync()).0
}
pub fn serve_hosts(hosts: Option<Vec<String>>) -> ServerLoop {
pub fn serve_hosts(hosts: Option<Vec<String>>) -> Server {
let hosts = hosts.map(|hosts| hosts.into_iter().map(Into::into).collect());
init_server(|builder| builder.allowed_hosts(hosts.into()), Default::default(), Remote::new_sync()).0
}
pub fn serve_with_registrar() -> (ServerLoop, Arc<FakeRegistrar>) {
pub fn serve_with_registrar() -> (Server, Arc<FakeRegistrar>) {
init_server(|builder| builder, Default::default(), Remote::new_sync())
}
pub fn serve_with_registrar_and_sync() -> (ServerLoop, Arc<FakeRegistrar>) {
pub fn serve_with_registrar_and_sync() -> (Server, Arc<FakeRegistrar>) {
init_server(|builder| {
builder.sync_status(Arc::new(|| true))
}, Default::default(), Remote::new_sync())
}
pub fn serve_with_registrar_and_fetch() -> (ServerLoop, FakeFetch, Arc<FakeRegistrar>) {
pub fn serve_with_registrar_and_fetch() -> (Server, FakeFetch, Arc<FakeRegistrar>) {
serve_with_registrar_and_fetch_and_threads(false)
}
pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (ServerLoop, FakeFetch, Arc<FakeRegistrar>) {
pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Server, FakeFetch, Arc<FakeRegistrar>) {
let fetch = FakeFetch::default();
let f = fetch.clone();
let (server, reg) = init_server(move |builder| {
@@ -116,7 +100,7 @@ pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Serv
(server, fetch, reg)
}
pub fn serve_with_fetch(web_token: &'static str) -> (ServerLoop, FakeFetch) {
pub fn serve_with_fetch(web_token: &'static str) -> (Server, FakeFetch) {
let fetch = FakeFetch::default();
let f = fetch.clone();
let (server, _) = init_server(move |builder| {
@@ -128,11 +112,11 @@ pub fn serve_with_fetch(web_token: &'static str) -> (ServerLoop, FakeFetch) {
(server, fetch)
}
pub fn serve() -> ServerLoop {
pub fn serve() -> Server {
init_server(|builder| builder, Default::default(), Remote::new_sync()).0
}
pub fn request(server: ServerLoop, request: &str) -> http_client::Response {
pub fn request(server: Server, request: &str) -> http_client::Response {
http_client::request(server.addr(), request)
}
@@ -240,6 +224,7 @@ impl<T: Fetch> ServerBuilder<T> {
}
}
const DAPPS_DOMAIN: &'static str = "web3.site";
/// Webapps HTTP server.
pub struct Server {
@@ -260,19 +245,27 @@ impl Server {
remote: Remote,
fetch: F,
) -> Result<Server, http::Error> {
let middleware = Middleware::new(
let middleware = Middleware::dapps(
remote,
signer_address,
dapps_path,
extra_dapps,
DAPPS_DOMAIN.into(),
registrar,
sync_status,
web_proxy_tokens,
fetch,
);
let mut allowed_hosts: Option<Vec<Host>> = allowed_hosts.into();
allowed_hosts.as_mut().map(|mut hosts| {
hosts.push(format!("http://*.{}:*", DAPPS_DOMAIN).into());
hosts.push(format!("http://*.{}", DAPPS_DOMAIN).into());
});
http::ServerBuilder::new(io)
.request_middleware(middleware)
.allowed_hosts(allowed_hosts)
.allowed_hosts(allowed_hosts.into())
.cors(http::DomainsValidation::Disabled)
.start_http(addr)
.map(|server| Server {

View File

@@ -37,15 +37,15 @@ fn should_redirect_to_home() {
}
#[test]
fn should_redirect_to_home_when_trailing_slash_is_missing() {
fn should_redirect_to_home_with_domain() {
// given
let server = serve();
// when
let response = request(server,
"\
GET /app HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
GET / HTTP/1.1\r\n\
Host: home.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@@ -57,14 +57,14 @@ fn should_redirect_to_home_when_trailing_slash_is_missing() {
}
#[test]
fn should_redirect_to_home_for_users_with_cached_redirection() {
fn should_redirect_to_home_when_trailing_slash_is_missing() {
// given
let server = serve();
// when
let response = request(server,
"\
GET /home/ HTTP/1.1\r\n\
GET /app HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
@@ -179,7 +179,7 @@ fn should_serve_proxy_pac() {
// then
response.assert_status("HTTP/1.1 200 OK");
assert_eq!(response.body, "DD\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"parity.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned());
assert_eq!(response.body, "DB\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"home.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned());
assert_security_headers(&response.headers);
}