Merge pull request #2033 from ethcore/dapps-sync
Nice error pages for Dapps & Signer
This commit is contained in:
commit
9655920896
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -310,6 +310,7 @@ version = "1.4.0"
|
||||
dependencies = [
|
||||
"clippy 0.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ethcore-devtools 1.4.0",
|
||||
"ethcore-rpc 1.4.0",
|
||||
"ethcore-util 1.4.0",
|
||||
"https-fetch 0.1.0",
|
||||
@ -478,6 +479,7 @@ version = "1.4.0"
|
||||
dependencies = [
|
||||
"clippy 0.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ethcore-devtools 1.4.0",
|
||||
"ethcore-io 1.4.0",
|
||||
"ethcore-rpc 1.4.0",
|
||||
"ethcore-util 1.4.0",
|
||||
@ -1109,7 +1111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
[[package]]
|
||||
name = "parity-dapps"
|
||||
version = "1.4.0"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#e4dddf36e7c9fa5c6e746790119c71f67438784a"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#926b09b66c4940b09dc82c52adb4afd9e31155bc"
|
||||
dependencies = [
|
||||
"aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -1123,7 +1125,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-dapps-home"
|
||||
version = "1.4.0"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#e4dddf36e7c9fa5c6e746790119c71f67438784a"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#926b09b66c4940b09dc82c52adb4afd9e31155bc"
|
||||
dependencies = [
|
||||
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
||||
]
|
||||
@ -1131,7 +1133,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-dapps-signer"
|
||||
version = "1.4.0"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#e4dddf36e7c9fa5c6e746790119c71f67438784a"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#926b09b66c4940b09dc82c52adb4afd9e31155bc"
|
||||
dependencies = [
|
||||
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
||||
]
|
||||
@ -1139,7 +1141,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-dapps-status"
|
||||
version = "1.4.0"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#e4dddf36e7c9fa5c6e746790119c71f67438784a"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#926b09b66c4940b09dc82c52adb4afd9e31155bc"
|
||||
dependencies = [
|
||||
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
||||
]
|
||||
@ -1147,7 +1149,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-dapps-wallet"
|
||||
version = "1.4.0"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#e4dddf36e7c9fa5c6e746790119c71f67438784a"
|
||||
source = "git+https://github.com/ethcore/parity-ui.git#926b09b66c4940b09dc82c52adb4afd9e31155bc"
|
||||
dependencies = [
|
||||
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
||||
]
|
||||
|
@ -23,6 +23,7 @@ serde_macros = { version = "0.8", optional = true }
|
||||
zip = { version = "0.1", default-features = false }
|
||||
ethabi = "0.2.2"
|
||||
linked-hash-map = "0.3"
|
||||
ethcore-devtools = { path = "../devtools" }
|
||||
ethcore-rpc = { path = "../rpc" }
|
||||
ethcore-util = { path = "../util" }
|
||||
https-fetch = { path = "../util/https-fetch" }
|
||||
|
@ -30,6 +30,7 @@ use hyper;
|
||||
use hyper::status::StatusCode;
|
||||
|
||||
use random_filename;
|
||||
use SyncStatus;
|
||||
use util::{Mutex, H256};
|
||||
use util::sha3::sha3;
|
||||
use page::LocalPageEndpoint;
|
||||
@ -44,6 +45,7 @@ const MAX_CACHED_DAPPS: usize = 10;
|
||||
pub struct AppFetcher<R: URLHint = URLHintContract> {
|
||||
dapps_path: PathBuf,
|
||||
resolver: R,
|
||||
sync: Arc<SyncStatus>,
|
||||
dapps: Arc<Mutex<ContentCache>>,
|
||||
}
|
||||
|
||||
@ -56,13 +58,14 @@ impl<R: URLHint> Drop for AppFetcher<R> {
|
||||
|
||||
impl<R: URLHint> AppFetcher<R> {
|
||||
|
||||
pub fn new(resolver: R) -> Self {
|
||||
pub fn new(resolver: R, sync_status: Arc<SyncStatus>) -> Self {
|
||||
let mut dapps_path = env::temp_dir();
|
||||
dapps_path.push(random_filename());
|
||||
|
||||
AppFetcher {
|
||||
dapps_path: dapps_path,
|
||||
resolver: resolver,
|
||||
sync: sync_status,
|
||||
dapps: Arc::new(Mutex::new(ContentCache::default())),
|
||||
}
|
||||
}
|
||||
@ -74,14 +77,20 @@ impl<R: URLHint> AppFetcher<R> {
|
||||
|
||||
pub fn contains(&self, app_id: &str) -> bool {
|
||||
let mut dapps = self.dapps.lock();
|
||||
match dapps.get(app_id) {
|
||||
// Check if we already have the app
|
||||
Some(_) => true,
|
||||
// fallback to resolver
|
||||
None => match app_id.from_hex() {
|
||||
Ok(app_id) => self.resolver.resolve(app_id).is_some(),
|
||||
_ => false,
|
||||
},
|
||||
// Check if we already have the app
|
||||
if dapps.get(app_id).is_some() {
|
||||
return true;
|
||||
}
|
||||
// fallback to resolver
|
||||
if let Ok(app_id) = app_id.from_hex() {
|
||||
// if app_id is valid, but we are syncing always return true.
|
||||
if self.sync.is_major_syncing() {
|
||||
return true;
|
||||
}
|
||||
// else try to resolve the app_id
|
||||
self.resolver.resolve(app_id).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +98,15 @@ impl<R: URLHint> AppFetcher<R> {
|
||||
let mut dapps = self.dapps.lock();
|
||||
let app_id = path.app_id.clone();
|
||||
|
||||
if self.sync.is_major_syncing() {
|
||||
return Box::new(ContentHandler::error(
|
||||
StatusCode::ServiceUnavailable,
|
||||
"Sync In Progress",
|
||||
"Your node is still syncing. We cannot resolve any content before it's fully synced.",
|
||||
Some("<a href=\"javascript:window.location.reload()\">Refresh</a>")
|
||||
));
|
||||
}
|
||||
|
||||
let (new_status, handler) = {
|
||||
let status = dapps.get(&app_id);
|
||||
match status {
|
||||
@ -98,32 +116,42 @@ impl<R: URLHint> AppFetcher<R> {
|
||||
},
|
||||
// App is already being fetched
|
||||
Some(&mut ContentStatus::Fetching(_)) => {
|
||||
(None, Box::new(ContentHandler::html(
|
||||
(None, Box::new(ContentHandler::error_with_refresh(
|
||||
StatusCode::ServiceUnavailable,
|
||||
format!(
|
||||
"<html><head>{}</head><body>{}</body></html>",
|
||||
"<meta http-equiv=\"refresh\" content=\"1\">",
|
||||
"<h1>This dapp is already being downloaded.</h1><h2>Please wait...</h2>",
|
||||
)
|
||||
"Download In Progress",
|
||||
"This dapp is already being downloaded. Please wait...",
|
||||
None,
|
||||
)) as Box<Handler>)
|
||||
},
|
||||
// We need to start fetching app
|
||||
None => {
|
||||
let app_hex = app_id.from_hex().expect("to_handler is called only when `contains` returns true.");
|
||||
let app = self.resolver.resolve(app_hex).expect("to_handler is called only when `contains` returns true.");
|
||||
let abort = Arc::new(AtomicBool::new(false));
|
||||
let app = self.resolver.resolve(app_hex);
|
||||
|
||||
(Some(ContentStatus::Fetching(abort.clone())), Box::new(ContentFetcherHandler::new(
|
||||
app,
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
DappInstaller {
|
||||
dapp_id: app_id.clone(),
|
||||
dapps_path: self.dapps_path.clone(),
|
||||
dapps: self.dapps.clone(),
|
||||
}
|
||||
)) as Box<Handler>)
|
||||
if let Some(app) = app {
|
||||
let abort = Arc::new(AtomicBool::new(false));
|
||||
|
||||
(Some(ContentStatus::Fetching(abort.clone())), Box::new(ContentFetcherHandler::new(
|
||||
app,
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
DappInstaller {
|
||||
dapp_id: app_id.clone(),
|
||||
dapps_path: self.dapps_path.clone(),
|
||||
dapps: self.dapps.clone(),
|
||||
}
|
||||
)) as Box<Handler>)
|
||||
} else {
|
||||
// This may happen when sync status changes in between
|
||||
// `contains` and `to_handler`
|
||||
(None, Box::new(ContentHandler::error(
|
||||
StatusCode::NotFound,
|
||||
"Resource Not Found",
|
||||
"Requested resource was not found.",
|
||||
None
|
||||
)) as Box<Handler>)
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
@ -294,6 +322,7 @@ impl ContentValidator for DappInstaller {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use util::Bytes;
|
||||
use endpoint::EndpointInfo;
|
||||
use page::LocalPageEndpoint;
|
||||
@ -312,7 +341,7 @@ mod tests {
|
||||
fn should_true_if_contains_the_app() {
|
||||
// given
|
||||
let path = env::temp_dir();
|
||||
let fetcher = AppFetcher::new(FakeResolver);
|
||||
let fetcher = AppFetcher::new(FakeResolver, Arc::new(|| false));
|
||||
let handler = LocalPageEndpoint::new(path, EndpointInfo {
|
||||
name: "fake".into(),
|
||||
description: "".into(),
|
||||
|
22
dapps/src/error_tpl.html
Normal file
22
dapps/src/error_tpl.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
{meta}
|
||||
<title>{title}</title>
|
||||
<link rel="stylesheet" href="/parity-utils/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="parity-navbar">
|
||||
</div>
|
||||
<div class="parity-box">
|
||||
<h1>{title}</h1>
|
||||
<h3>{message}</h3>
|
||||
<p><code>{details}</code></p>
|
||||
</div>
|
||||
<div class="parity-status">
|
||||
<small>{version}</small>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -21,6 +21,8 @@ use hyper::{header, server, Decoder, Encoder, Next};
|
||||
use hyper::net::HttpStream;
|
||||
use hyper::status::StatusCode;
|
||||
|
||||
use util::version;
|
||||
|
||||
pub struct ContentHandler {
|
||||
code: StatusCode,
|
||||
content: String,
|
||||
@ -38,15 +40,6 @@ impl ContentHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forbidden(content: String, mimetype: String) -> Self {
|
||||
ContentHandler {
|
||||
code: StatusCode::Forbidden,
|
||||
content: content,
|
||||
mimetype: mimetype,
|
||||
write_pos: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_found(content: String, mimetype: String) -> Self {
|
||||
ContentHandler {
|
||||
code: StatusCode::NotFound,
|
||||
@ -60,6 +53,28 @@ impl ContentHandler {
|
||||
Self::new(code, content, "text/html".into())
|
||||
}
|
||||
|
||||
pub fn error(code: StatusCode, title: &str, message: &str, details: Option<&str>) -> Self {
|
||||
Self::html(code, format!(
|
||||
include_str!("../error_tpl.html"),
|
||||
title=title,
|
||||
meta="",
|
||||
message=message,
|
||||
details=details.unwrap_or_else(|| ""),
|
||||
version=version(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn error_with_refresh(code: StatusCode, title: &str, message: &str, details: Option<&str>) -> Self {
|
||||
Self::html(code, format!(
|
||||
include_str!("../error_tpl.html"),
|
||||
title=title,
|
||||
meta="<meta http-equiv=\"refresh\" content=\"1\">",
|
||||
message=message,
|
||||
details=details.unwrap_or_else(|| ""),
|
||||
version=version(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn new(code: StatusCode, content: String, mimetype: String) -> Self {
|
||||
ContentHandler {
|
||||
code: code,
|
||||
|
@ -121,16 +121,20 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
|
||||
deadline: Instant::now() + Duration::from_secs(FETCH_TIMEOUT),
|
||||
receiver: receiver,
|
||||
},
|
||||
Err(e) => FetchState::Error(ContentHandler::html(
|
||||
Err(e) => FetchState::Error(ContentHandler::error(
|
||||
StatusCode::BadGateway,
|
||||
format!("<h1>Error starting dapp download.</h1><pre>{}</pre>", e),
|
||||
"Unable To Start Dapp Download",
|
||||
"Could not initialize download of the dapp. It might be a problem with the remote server.",
|
||||
Some(&format!("{}", e)),
|
||||
)),
|
||||
}
|
||||
},
|
||||
// or return error
|
||||
_ => FetchState::Error(ContentHandler::html(
|
||||
_ => FetchState::Error(ContentHandler::error(
|
||||
StatusCode::MethodNotAllowed,
|
||||
"<h1>Only <code>GET</code> requests are allowed.</h1>".into(),
|
||||
"Method Not Allowed",
|
||||
"Only <code>GET</code> requests are allowed.",
|
||||
None,
|
||||
)),
|
||||
})
|
||||
} else { None };
|
||||
@ -147,10 +151,12 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
|
||||
// Request may time out
|
||||
FetchState::InProgress { ref deadline, .. } if *deadline < Instant::now() => {
|
||||
trace!(target: "dapps", "Fetching dapp failed because of timeout.");
|
||||
let timeout = ContentHandler::html(
|
||||
let timeout = ContentHandler::error(
|
||||
StatusCode::GatewayTimeout,
|
||||
format!("<h1>Could not fetch app bundle within {} seconds.</h1>", FETCH_TIMEOUT),
|
||||
);
|
||||
"Download Timeout",
|
||||
&format!("Could not fetch dapp bundle within {} seconds.", FETCH_TIMEOUT),
|
||||
None
|
||||
);
|
||||
Self::close_client(&mut self.client);
|
||||
(Some(FetchState::Error(timeout)), Next::write())
|
||||
},
|
||||
@ -166,9 +172,11 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
|
||||
let state = match self.dapp.validate_and_install(path.clone()) {
|
||||
Err(e) => {
|
||||
trace!(target: "dapps", "Error while validating dapp: {:?}", e);
|
||||
FetchState::Error(ContentHandler::html(
|
||||
FetchState::Error(ContentHandler::error(
|
||||
StatusCode::BadGateway,
|
||||
format!("<h1>Downloaded bundle does not contain valid app.</h1><pre>{}</pre>", e),
|
||||
"Invalid Dapp",
|
||||
"Downloaded bundle does not contain a valid dapp.",
|
||||
Some(&format!("{:?}", e))
|
||||
))
|
||||
},
|
||||
Ok(manifest) => FetchState::Done(manifest)
|
||||
@ -180,9 +188,11 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
warn!(target: "dapps", "Unable to fetch new dapp: {:?}", e);
|
||||
let error = ContentHandler::html(
|
||||
let error = ContentHandler::error(
|
||||
StatusCode::BadGateway,
|
||||
"<h1>There was an error when fetching the dapp.</h1>".into(),
|
||||
"Download Error",
|
||||
"There was an error when fetching the dapp.",
|
||||
Some(&format!("{:?}", e)),
|
||||
);
|
||||
(Some(FetchState::Error(error)), Next::write())
|
||||
},
|
||||
|
@ -62,6 +62,8 @@ extern crate https_fetch;
|
||||
extern crate ethcore_rpc;
|
||||
extern crate ethcore_util as util;
|
||||
extern crate linked_hash_map;
|
||||
#[cfg(test)]
|
||||
extern crate ethcore_devtools as devtools;
|
||||
|
||||
mod endpoint;
|
||||
mod apps;
|
||||
@ -87,11 +89,22 @@ use ethcore_rpc::Extendable;
|
||||
|
||||
static DAPPS_DOMAIN : &'static str = ".parity";
|
||||
|
||||
/// Indicates sync status
|
||||
pub trait SyncStatus: Send + Sync {
|
||||
/// Returns true if there is a major sync happening.
|
||||
fn is_major_syncing(&self) -> bool;
|
||||
}
|
||||
|
||||
impl<F> SyncStatus for F where F: Fn() -> bool + Send + Sync {
|
||||
fn is_major_syncing(&self) -> bool { self() }
|
||||
}
|
||||
|
||||
/// Webapps HTTP+RPC server build.
|
||||
pub struct ServerBuilder {
|
||||
dapps_path: String,
|
||||
handler: Arc<IoHandler>,
|
||||
registrar: Arc<ContractClient>,
|
||||
sync_status: Arc<SyncStatus>,
|
||||
}
|
||||
|
||||
impl Extendable for ServerBuilder {
|
||||
@ -107,9 +120,15 @@ impl ServerBuilder {
|
||||
dapps_path: dapps_path,
|
||||
handler: Arc::new(IoHandler::new()),
|
||||
registrar: registrar,
|
||||
sync_status: Arc::new(|| false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Change default sync status.
|
||||
pub fn with_sync_status(&mut self, status: Arc<SyncStatus>) {
|
||||
self.sync_status = status;
|
||||
}
|
||||
|
||||
/// Asynchronously start server with no authentication,
|
||||
/// returns result with `Server` handle on success or an error.
|
||||
pub fn start_unsecured_http(&self, addr: &SocketAddr, hosts: Option<Vec<String>>) -> Result<Server, ServerError> {
|
||||
@ -119,7 +138,8 @@ impl ServerBuilder {
|
||||
NoAuth,
|
||||
self.handler.clone(),
|
||||
self.dapps_path.clone(),
|
||||
self.registrar.clone()
|
||||
self.registrar.clone(),
|
||||
self.sync_status.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -132,7 +152,8 @@ impl ServerBuilder {
|
||||
HttpBasicAuth::single_user(username, password),
|
||||
self.handler.clone(),
|
||||
self.dapps_path.clone(),
|
||||
self.registrar.clone()
|
||||
self.registrar.clone(),
|
||||
self.sync_status.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -166,10 +187,11 @@ impl Server {
|
||||
handler: Arc<IoHandler>,
|
||||
dapps_path: String,
|
||||
registrar: Arc<ContractClient>,
|
||||
sync_status: Arc<SyncStatus>,
|
||||
) -> Result<Server, ServerError> {
|
||||
let panic_handler = Arc::new(Mutex::new(None));
|
||||
let authorization = Arc::new(authorization);
|
||||
let apps_fetcher = Arc::new(apps::fetcher::AppFetcher::new(apps::urlhint::URLHintContract::new(registrar)));
|
||||
let apps_fetcher = Arc::new(apps::fetcher::AppFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status));
|
||||
let endpoints = Arc::new(apps::all_endpoints(dapps_path));
|
||||
let special = Arc::new({
|
||||
let mut special = HashMap::new();
|
||||
|
@ -79,7 +79,7 @@ impl<T: WebApp> Endpoint for PageEndpoint<T> {
|
||||
app: BuiltinDapp::new(self.app.clone()),
|
||||
prefix: self.prefix.clone(),
|
||||
path: path,
|
||||
file: None,
|
||||
file: Default::default(),
|
||||
safe_to_embed: self.safe_to_embed,
|
||||
})
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ use hyper::net::HttpStream;
|
||||
use hyper::status::StatusCode;
|
||||
use hyper::{Decoder, Encoder, Next};
|
||||
use endpoint::EndpointPath;
|
||||
use handlers::ContentHandler;
|
||||
|
||||
/// Represents a file that can be sent to client.
|
||||
/// Implementation should keep track of bytes already sent internally.
|
||||
@ -48,6 +49,25 @@ pub trait Dapp: Send + 'static {
|
||||
fn file(&self, path: &str) -> Option<Self::DappFile>;
|
||||
}
|
||||
|
||||
/// Currently served by `PageHandler` file
|
||||
pub enum ServedFile<T: Dapp> {
|
||||
/// File from dapp
|
||||
File(T::DappFile),
|
||||
/// Error (404)
|
||||
Error(ContentHandler),
|
||||
}
|
||||
|
||||
impl<T: Dapp> Default for ServedFile<T> {
|
||||
fn default() -> Self {
|
||||
ServedFile::Error(ContentHandler::error(
|
||||
StatusCode::NotFound,
|
||||
"404 Not Found",
|
||||
"Requested dapp resource was not found.",
|
||||
None
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A handler for a single webapp.
|
||||
/// Resolves correct paths and serves as a plumbing code between
|
||||
/// hyper server and dapp.
|
||||
@ -55,7 +75,7 @@ pub struct PageHandler<T: Dapp> {
|
||||
/// A Dapp.
|
||||
pub app: T,
|
||||
/// File currently being served (or `None` if file does not exist).
|
||||
pub file: Option<T::DappFile>,
|
||||
pub file: ServedFile<T>,
|
||||
/// Optional prefix to strip from path.
|
||||
pub prefix: Option<String>,
|
||||
/// Requested path.
|
||||
@ -95,7 +115,7 @@ impl<T: Dapp> server::Handler<HttpStream> for PageHandler<T> {
|
||||
self.app.file(&self.extract_path(url.path()))
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
}.map_or_else(|| ServedFile::default(), |f| ServedFile::File(f));
|
||||
Next::write()
|
||||
}
|
||||
|
||||
@ -104,24 +124,26 @@ impl<T: Dapp> server::Handler<HttpStream> for PageHandler<T> {
|
||||
}
|
||||
|
||||
fn on_response(&mut self, res: &mut server::Response) -> Next {
|
||||
if let Some(ref f) = self.file {
|
||||
res.set_status(StatusCode::Ok);
|
||||
res.headers_mut().set(header::ContentType(f.content_type().parse().unwrap()));
|
||||
if !self.safe_to_embed {
|
||||
res.headers_mut().set_raw("X-Frame-Options", vec![b"SAMEORIGIN".to_vec()]);
|
||||
match self.file {
|
||||
ServedFile::File(ref f) => {
|
||||
res.set_status(StatusCode::Ok);
|
||||
res.headers_mut().set(header::ContentType(f.content_type().parse().unwrap()));
|
||||
if !self.safe_to_embed {
|
||||
res.headers_mut().set_raw("X-Frame-Options", vec![b"SAMEORIGIN".to_vec()]);
|
||||
}
|
||||
Next::write()
|
||||
},
|
||||
ServedFile::Error(ref mut handler) => {
|
||||
handler.on_response(res)
|
||||
}
|
||||
Next::write()
|
||||
} else {
|
||||
res.set_status(StatusCode::NotFound);
|
||||
Next::write()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
|
||||
match self.file {
|
||||
None => Next::end(),
|
||||
Some(ref f) if f.is_drained() => Next::end(),
|
||||
Some(ref mut f) => match encoder.write(f.next_chunk()) {
|
||||
ServedFile::Error(ref mut handler) => handler.on_response_writable(encoder),
|
||||
ServedFile::File(ref f) if f.is_drained() => Next::end(),
|
||||
ServedFile::File(ref mut f) => match encoder.write(f.next_chunk()) {
|
||||
Ok(bytes) => {
|
||||
f.bytes_written(bytes);
|
||||
Next::write()
|
||||
@ -190,7 +212,7 @@ fn should_extract_path_with_appid() {
|
||||
port: 8080,
|
||||
using_dapps_domains: true,
|
||||
},
|
||||
file: None,
|
||||
file: Default::default(),
|
||||
safe_to_embed: true,
|
||||
};
|
||||
|
||||
|
@ -49,7 +49,7 @@ impl Endpoint for LocalPageEndpoint {
|
||||
app: LocalDapp::new(self.path.clone()),
|
||||
prefix: None,
|
||||
path: path,
|
||||
file: None,
|
||||
file: Default::default(),
|
||||
safe_to_embed: false,
|
||||
})
|
||||
}
|
||||
|
@ -55,10 +55,11 @@ impl Authorization for HttpBasicAuth {
|
||||
|
||||
match auth {
|
||||
Access::Denied => {
|
||||
Authorized::No(Box::new(ContentHandler::new(
|
||||
Authorized::No(Box::new(ContentHandler::error(
|
||||
status::StatusCode::Unauthorized,
|
||||
"<h1>Unauthorized</h1>".into(),
|
||||
"text/html".into(),
|
||||
"Unauthorized",
|
||||
"You need to provide valid credentials to access this page.",
|
||||
None
|
||||
)))
|
||||
},
|
||||
Access::AuthRequired => {
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
|
||||
use DAPPS_DOMAIN;
|
||||
use hyper::{server, header};
|
||||
use hyper::{server, header, StatusCode};
|
||||
use hyper::net::HttpStream;
|
||||
|
||||
use jsonrpc_http_server::{is_host_header_valid};
|
||||
@ -38,11 +38,9 @@ pub fn is_valid(request: &server::Request<HttpStream>, allowed_hosts: &[String],
|
||||
}
|
||||
|
||||
pub fn host_invalid_response() -> Box<server::Handler<HttpStream> + Send> {
|
||||
Box::new(ContentHandler::forbidden(
|
||||
r#"
|
||||
<h1>Request with disallowed <code>Host</code> header has been blocked.</h1>
|
||||
<p>Check the URL in your browser address bar.</p>
|
||||
"#.into(),
|
||||
"text/html".into()
|
||||
Box::new(ContentHandler::error(StatusCode::Forbidden,
|
||||
"Current Host Is Disallowed",
|
||||
"You are trying to access your node using incorrect address.",
|
||||
Some("Use allowed URL or specify different <code>hosts</code> CLI options.")
|
||||
))
|
||||
}
|
||||
|
@ -24,12 +24,12 @@ use DAPPS_DOMAIN;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use url::{Url, Host};
|
||||
use hyper::{self, server, Next, Encoder, Decoder, Control};
|
||||
use hyper::{self, server, Next, Encoder, Decoder, Control, StatusCode};
|
||||
use hyper::net::HttpStream;
|
||||
use apps;
|
||||
use apps::fetcher::AppFetcher;
|
||||
use endpoint::{Endpoint, Endpoints, EndpointPath};
|
||||
use handlers::{Redirection, extract_url};
|
||||
use handlers::{Redirection, extract_url, ContentHandler};
|
||||
use self::auth::{Authorization, Authorized};
|
||||
|
||||
/// Special endpoints are accessible on every domain (every dapp)
|
||||
@ -55,9 +55,16 @@ pub struct Router<A: Authorization + 'static> {
|
||||
impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
|
||||
|
||||
fn on_request(&mut self, req: server::Request<HttpStream>) -> Next {
|
||||
|
||||
// Choose proper handler depending on path / domain
|
||||
let url = extract_url(&req);
|
||||
let endpoint = extract_endpoint(&url);
|
||||
let is_utils = endpoint.1 == SpecialEndpoint::Utils;
|
||||
|
||||
// Validate Host header
|
||||
if let Some(ref hosts) = self.allowed_hosts {
|
||||
if !host_validation::is_valid(&req, hosts, self.endpoints.keys().cloned().collect()) {
|
||||
let is_valid = is_utils || host_validation::is_valid(&req, hosts, self.endpoints.keys().cloned().collect());
|
||||
if !is_valid {
|
||||
self.handler = host_validation::host_invalid_response();
|
||||
return self.handler.on_request(req);
|
||||
}
|
||||
@ -70,11 +77,7 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
|
||||
return self.handler.on_request(req);
|
||||
}
|
||||
|
||||
// Choose proper handler depending on path / domain
|
||||
let url = extract_url(&req);
|
||||
let endpoint = extract_endpoint(&url);
|
||||
let control = self.control.take().expect("on_request is called only once; control is always defined at start; qed");
|
||||
|
||||
self.handler = match endpoint {
|
||||
// First check special endpoints
|
||||
(ref path, ref endpoint) if self.special.contains_key(endpoint) => {
|
||||
@ -91,7 +94,12 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
|
||||
// Redirection to main page (maybe 404 instead?)
|
||||
(Some(ref path), _) if *req.method() == hyper::method::Method::Get => {
|
||||
let address = apps::redirection_address(path.using_dapps_domains, self.main_page);
|
||||
Redirection::new(address.as_str())
|
||||
Box::new(ContentHandler::error(
|
||||
StatusCode::NotFound,
|
||||
"404 Not Found",
|
||||
"Requested content was not found.",
|
||||
Some(&format!("Go back to the <a href=\"{}\">Home Page</a>.", address))
|
||||
))
|
||||
},
|
||||
// Redirect any GET request to home.
|
||||
_ if *req.method() == hyper::method::Method::Get => {
|
||||
|
@ -57,7 +57,7 @@ fn should_serve_apps() {
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||
assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json");
|
||||
assert!(response.body.contains("Parity Home Screen"));
|
||||
assert!(response.body.contains("Parity Home Screen"), response.body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -54,7 +54,7 @@ fn should_reject_on_invalid_auth() {
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 401 Unauthorized".to_owned());
|
||||
assert_eq!(response.body, "15\n<h1>Unauthorized</h1>\n0\n\n".to_owned());
|
||||
assert!(response.body.contains("Unauthorized"), response.body);
|
||||
assert_eq!(response.headers_raw.contains("WWW-Authenticate"), false);
|
||||
}
|
||||
|
||||
|
38
dapps/src/tests/fetch.rs
Normal file
38
dapps/src/tests/fetch.rs
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2015, 2016 Ethcore (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 tests::helpers::{serve_with_registrar, request};
|
||||
|
||||
#[test]
|
||||
fn should_resolve_dapp() {
|
||||
// given
|
||||
let (server, registrar) = serve_with_registrar();
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
"\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\
|
||||
Connection: close\r\n\
|
||||
\r\n\
|
||||
"
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
||||
assert_eq!(registrar.calls.lock().len(), 2);
|
||||
}
|
||||
|
@ -15,16 +15,15 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::env;
|
||||
use std::io::{Read, Write};
|
||||
use std::str::{self, Lines};
|
||||
use std::str;
|
||||
use std::sync::Arc;
|
||||
use std::net::TcpStream;
|
||||
use rustc_serialize::hex::{ToHex, FromHex};
|
||||
|
||||
use ServerBuilder;
|
||||
use Server;
|
||||
use apps::urlhint::ContractClient;
|
||||
use util::{Bytes, Address, Mutex, ToPretty};
|
||||
use devtools::http_client;
|
||||
|
||||
const REGISTRAR: &'static str = "8e4e9b13d4b45cb0befc93c3061b1408f67316b2";
|
||||
const URLHINT: &'static str = "deadbeefcafe0000000000000000000000000000";
|
||||
@ -59,65 +58,37 @@ impl ContractClient for FakeRegistrar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serve_hosts(hosts: Option<Vec<String>>) -> Server {
|
||||
pub fn init_server(hosts: Option<Vec<String>>) -> (Server, Arc<FakeRegistrar>) {
|
||||
let registrar = Arc::new(FakeRegistrar::new());
|
||||
let mut dapps_path = env::temp_dir();
|
||||
dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading");
|
||||
let builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar);
|
||||
builder.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), hosts).unwrap()
|
||||
let builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar.clone());
|
||||
(
|
||||
builder.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), hosts).unwrap(),
|
||||
registrar,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn serve_with_auth(user: &str, pass: &str) -> Server {
|
||||
let registrar = Arc::new(FakeRegistrar::new());
|
||||
let builder = ServerBuilder::new(env::temp_dir().to_str().unwrap().into(), registrar);
|
||||
let mut dapps_path = env::temp_dir();
|
||||
dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading");
|
||||
let builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar);
|
||||
builder.start_basic_auth_http(&"127.0.0.1:0".parse().unwrap(), None, user, pass).unwrap()
|
||||
}
|
||||
|
||||
pub fn serve_hosts(hosts: Option<Vec<String>>) -> Server {
|
||||
init_server(hosts).0
|
||||
}
|
||||
|
||||
pub fn serve_with_registrar() -> (Server, Arc<FakeRegistrar>) {
|
||||
init_server(None)
|
||||
}
|
||||
|
||||
pub fn serve() -> Server {
|
||||
serve_hosts(None)
|
||||
init_server(None).0
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
pub status: String,
|
||||
pub headers: Vec<String>,
|
||||
pub headers_raw: String,
|
||||
pub body: String,
|
||||
pub fn request(server: Server, request: &str) -> http_client::Response {
|
||||
http_client::request(server.addr(), request)
|
||||
}
|
||||
|
||||
pub fn read_block(lines: &mut Lines, all: bool) -> String {
|
||||
let mut block = String::new();
|
||||
loop {
|
||||
let line = lines.next();
|
||||
match line {
|
||||
None => break,
|
||||
Some("") if !all => break,
|
||||
Some(v) => {
|
||||
block.push_str(v);
|
||||
block.push_str("\n");
|
||||
},
|
||||
}
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
pub fn request(server: Server, request: &str) -> Response {
|
||||
let mut req = TcpStream::connect(server.addr()).unwrap();
|
||||
req.write_all(request.as_bytes()).unwrap();
|
||||
|
||||
let mut response = String::new();
|
||||
req.read_to_string(&mut response).unwrap();
|
||||
|
||||
let mut lines = response.lines();
|
||||
let status = lines.next().unwrap().to_owned();
|
||||
let headers_raw = read_block(&mut lines, false);
|
||||
let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect();
|
||||
let body = read_block(&mut lines, true);
|
||||
|
||||
Response {
|
||||
status: status,
|
||||
headers: headers,
|
||||
headers_raw: headers_raw,
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ mod helpers;
|
||||
|
||||
mod api;
|
||||
mod authorization;
|
||||
mod fetch;
|
||||
mod redirection;
|
||||
mod validation;
|
||||
|
||||
|
@ -57,7 +57,7 @@ fn should_redirect_to_home_when_trailing_slash_is_missing() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_redirect_to_home_on_invalid_dapp() {
|
||||
fn should_display_404_on_invalid_dapp() {
|
||||
// given
|
||||
let server = serve();
|
||||
|
||||
@ -72,12 +72,12 @@ fn should_redirect_to_home_on_invalid_dapp() {
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned());
|
||||
assert_eq!(response.headers.get(0).unwrap(), "Location: /home/");
|
||||
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
||||
assert!(response.body.contains("href=\"/home/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_redirect_to_home_on_invalid_dapp_with_domain() {
|
||||
fn should_display_404_on_invalid_dapp_with_domain() {
|
||||
// given
|
||||
let server = serve();
|
||||
|
||||
@ -92,8 +92,8 @@ fn should_redirect_to_home_on_invalid_dapp_with_domain() {
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned());
|
||||
assert_eq!(response.headers.get(0).unwrap(), "Location: http://home.parity/");
|
||||
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
||||
assert!(response.body.contains("href=\"http://home.parity/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -34,7 +34,7 @@ fn should_reject_invalid_host() {
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
|
||||
assert_eq!(response.body, "85\n\n\t\t<h1>Request with disallowed <code>Host</code> header has been blocked.</h1>\n\t\t<p>Check the URL in your browser address bar.</p>\n\t\t\n0\n\n".to_owned());
|
||||
assert!(response.body.contains("Current Host Is Disallowed"), response.body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -77,3 +77,24 @@ fn should_serve_dapps_domains() {
|
||||
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
// NOTE [todr] This is required for error pages to be styled properly.
|
||||
fn should_allow_parity_utils_even_on_invalid_domain() {
|
||||
// given
|
||||
let server = serve_hosts(Some(vec!["localhost:8080".into()]));
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
"\
|
||||
GET /parity-utils/styles.css HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:8080\r\n\
|
||||
Connection: close\r\n\
|
||||
\r\n\
|
||||
{}
|
||||
"
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||
}
|
||||
|
||||
|
64
devtools/src/http_client.rs
Normal file
64
devtools/src/http_client.rs
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2015, 2016 Ethcore (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::io::{Read, Write};
|
||||
use std::str::{self, Lines};
|
||||
use std::net::{TcpStream, SocketAddr};
|
||||
|
||||
pub struct Response {
|
||||
pub status: String,
|
||||
pub headers: Vec<String>,
|
||||
pub headers_raw: String,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
pub fn read_block(lines: &mut Lines, all: bool) -> String {
|
||||
let mut block = String::new();
|
||||
loop {
|
||||
let line = lines.next();
|
||||
match line {
|
||||
None => break,
|
||||
Some("") if !all => break,
|
||||
Some(v) => {
|
||||
block.push_str(v);
|
||||
block.push_str("\n");
|
||||
},
|
||||
}
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
pub fn request(address: &SocketAddr, request: &str) -> Response {
|
||||
let mut req = TcpStream::connect(address).unwrap();
|
||||
req.write_all(request.as_bytes()).unwrap();
|
||||
|
||||
let mut response = String::new();
|
||||
req.read_to_string(&mut response).unwrap();
|
||||
|
||||
let mut lines = response.lines();
|
||||
let status = lines.next().unwrap().to_owned();
|
||||
let headers_raw = read_block(&mut lines, false);
|
||||
let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect();
|
||||
let body = read_block(&mut lines, true);
|
||||
|
||||
Response {
|
||||
status: status,
|
||||
headers: headers,
|
||||
headers_raw: headers_raw,
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ extern crate rand;
|
||||
mod random_path;
|
||||
mod test_socket;
|
||||
mod stop_guard;
|
||||
pub mod http_client;
|
||||
|
||||
pub use random_path::*;
|
||||
pub use test_socket::*;
|
||||
|
@ -18,6 +18,7 @@ use std::sync::Arc;
|
||||
use io::PanicHandler;
|
||||
use rpc_apis;
|
||||
use ethcore::client::Client;
|
||||
use ethsync::SyncProvider;
|
||||
use helpers::replace_home;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
@ -49,6 +50,7 @@ pub struct Dependencies {
|
||||
pub panic_handler: Arc<PanicHandler>,
|
||||
pub apis: Arc<rpc_apis::Dependencies>,
|
||||
pub client: Arc<Client>,
|
||||
pub sync: Arc<SyncProvider>,
|
||||
}
|
||||
|
||||
pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<WebappServer>, String> {
|
||||
@ -117,9 +119,12 @@ mod server {
|
||||
) -> Result<WebappServer, String> {
|
||||
use ethcore_dapps as dapps;
|
||||
|
||||
let server = dapps::ServerBuilder::new(dapps_path, Arc::new(Registrar {
|
||||
client: deps.client.clone(),
|
||||
}));
|
||||
let mut server = dapps::ServerBuilder::new(
|
||||
dapps_path,
|
||||
Arc::new(Registrar { client: deps.client.clone() })
|
||||
);
|
||||
let sync = deps.sync.clone();
|
||||
server.with_sync_status(Arc::new(move || sync.status().is_major_syncing()));
|
||||
let server = rpc_apis::setup_rpc(server, deps.apis.clone(), rpc_apis::ApiSet::UnsafeContext);
|
||||
let start_result = match auth {
|
||||
None => {
|
||||
|
@ -224,6 +224,7 @@ pub fn execute(cmd: RunCmd) -> Result<(), String> {
|
||||
panic_handler: panic_handler.clone(),
|
||||
apis: deps_for_rpc_apis.clone(),
|
||||
client: client.clone(),
|
||||
sync: sync_provider.clone(),
|
||||
};
|
||||
|
||||
// start dapps server
|
||||
|
@ -19,6 +19,7 @@ ws = { git = "https://github.com/ethcore/ws-rs.git", branch = "mio-upstream-stab
|
||||
ethcore-util = { path = "../util" }
|
||||
ethcore-io = { path = "../util/io" }
|
||||
ethcore-rpc = { path = "../rpc" }
|
||||
ethcore-devtools = { path = "../devtools" }
|
||||
parity-dapps-signer = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4", optional = true}
|
||||
|
||||
clippy = { version = "0.0.85", optional = true}
|
||||
|
@ -54,15 +54,13 @@ extern crate jsonrpc_core;
|
||||
extern crate ws;
|
||||
#[cfg(feature = "ui")]
|
||||
extern crate parity_dapps_signer as signer;
|
||||
#[cfg(test)]
|
||||
extern crate ethcore_devtools as devtools;
|
||||
|
||||
mod authcode_store;
|
||||
mod ws_server;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use authcode_store::*;
|
||||
pub use ws_server::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn should_work() {}
|
||||
}
|
||||
|
81
signer/src/tests/mod.rs
Normal file
81
signer/src/tests/mod.rs
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2015, 2016 Ethcore (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::env;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
use devtools::http_client;
|
||||
use rpc::ConfirmationsQueue;
|
||||
use rand;
|
||||
|
||||
use ServerBuilder;
|
||||
use Server;
|
||||
|
||||
pub fn serve() -> Server {
|
||||
let queue = Arc::new(ConfirmationsQueue::default());
|
||||
let builder = ServerBuilder::new(queue, env::temp_dir());
|
||||
let port = 35000 + rand::random::<usize>() % 10000;
|
||||
let res = builder.start(format!("127.0.0.1:{}", port).parse().unwrap()).unwrap();
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
res
|
||||
}
|
||||
|
||||
pub fn request(server: Server, request: &str) -> http_client::Response {
|
||||
http_client::request(server.addr(), request)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_reject_invalid_host() {
|
||||
// given
|
||||
let server = serve();
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
"\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: test:8180\r\n\
|
||||
Connection: close\r\n\
|
||||
\r\n\
|
||||
{}
|
||||
"
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
||||
assert!(response.body.contains("URL Blocked"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_serve_styles_even_on_disallowed_domain() {
|
||||
// given
|
||||
let server = serve();
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
"\
|
||||
GET /styles.css HTTP/1.1\r\n\
|
||||
Host: test:8180\r\n\
|
||||
Connection: close\r\n\
|
||||
\r\n\
|
||||
{}
|
||||
"
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||
}
|
||||
|
21
signer/src/ws_server/error_tpl.html
Normal file
21
signer/src/ws_server/error_tpl.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
{meta}
|
||||
<title>{title}</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="parity-navbar"></div>
|
||||
<div class="parity-box">
|
||||
<h1>{title}</h1>
|
||||
<h3>{message}</h3>
|
||||
<p><code>{details}</code></p>
|
||||
</div>
|
||||
<div class="parity-status">
|
||||
<small>{version}</small>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -93,9 +93,15 @@ pub struct Server {
|
||||
broadcaster_handle: Option<thread::JoinHandle<()>>,
|
||||
queue: Arc<ConfirmationsQueue>,
|
||||
panic_handler: Arc<PanicHandler>,
|
||||
addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Returns the address this server is listening on
|
||||
pub fn addr(&self) -> &SocketAddr {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// Starts a new `WebSocket` server in separate thread.
|
||||
/// Returns a `Server` handle which closes the server when droped.
|
||||
fn start(addr: SocketAddr, handler: Arc<IoHandler>, queue: Arc<ConfirmationsQueue>, authcodes_path: PathBuf, skip_origin_validation: bool) -> Result<Server, ServerError> {
|
||||
@ -121,7 +127,7 @@ impl Server {
|
||||
// Spawn a thread with event loop
|
||||
let handle = thread::spawn(move || {
|
||||
ph.catch_panic(move || {
|
||||
match ws.listen(addr).map_err(ServerError::from) {
|
||||
match ws.listen(addr.clone()).map_err(ServerError::from) {
|
||||
Err(ServerError::IoError(io)) => die(format!(
|
||||
"Signer: Could not start listening on specified address. Make sure that no other instance is running on Signer's port. Details: {:?}",
|
||||
io
|
||||
@ -158,6 +164,7 @@ impl Server {
|
||||
broadcaster_handle: Some(broadcaster_handle),
|
||||
queue: queue,
|
||||
panic_handler: panic_handler,
|
||||
addr: addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ use std::path::{PathBuf, Path};
|
||||
use std::sync::Arc;
|
||||
use std::str::FromStr;
|
||||
use jsonrpc_core::IoHandler;
|
||||
use util::{H256, Mutex};
|
||||
use util::{H256, Mutex, version};
|
||||
|
||||
#[cfg(feature = "ui")]
|
||||
mod signer {
|
||||
@ -107,21 +107,32 @@ impl ws::Handler for Session {
|
||||
fn on_request(&mut self, req: &ws::Request) -> ws::Result<(ws::Response)> {
|
||||
let origin = req.header("origin").or_else(|| req.header("Origin")).map(|x| &x[..]);
|
||||
let host = req.header("host").or_else(|| req.header("Host")).map(|x| &x[..]);
|
||||
// Styles file is allowed for error pages to display nicely.
|
||||
let is_styles_file = req.resource() == "/styles.css";
|
||||
|
||||
// Check request origin and host header.
|
||||
if !self.skip_origin_validation {
|
||||
if !origin_is_allowed(&self.self_origin, origin) && !(origin.is_none() && origin_is_allowed(&self.self_origin, host)) {
|
||||
let is_valid = origin_is_allowed(&self.self_origin, origin) || (origin.is_none() && origin_is_allowed(&self.self_origin, host));
|
||||
let is_valid = is_styles_file || is_valid;
|
||||
|
||||
if !is_valid {
|
||||
warn!(target: "signer", "Blocked connection to Signer API from untrusted origin.");
|
||||
return Ok(ws::Response::forbidden(format!("You are not allowed to access system ui. Use: http://{}", self.self_origin)));
|
||||
return Ok(error(
|
||||
ErrorType::Forbidden,
|
||||
"URL Blocked",
|
||||
"You are not allowed to access Trusted Signer using this URL.",
|
||||
Some(&format!("Use: http://{}", self.self_origin)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Detect if it's a websocket request.
|
||||
if req.header("sec-websocket-key").is_some() {
|
||||
// Detect if it's a websocket request
|
||||
// (styles file skips origin validation, so make sure to prevent WS connections on this resource)
|
||||
if req.header("sec-websocket-key").is_some() && !is_styles_file {
|
||||
// Check authorization
|
||||
if !auth_is_valid(&self.authcodes_path, req.protocols()) {
|
||||
info!(target: "signer", "Unauthorized connection to Signer API blocked.");
|
||||
return Ok(ws::Response::forbidden("You are not authorized.".into()));
|
||||
return Ok(error(ErrorType::Forbidden, "Not Authorized", "Request to this API was not authorized.", None));
|
||||
}
|
||||
|
||||
let protocols = req.protocols().expect("Existence checked by authorization.");
|
||||
@ -137,7 +148,7 @@ impl ws::Handler for Session {
|
||||
Ok(signer::handle(req.resource())
|
||||
.map_or_else(
|
||||
// return 404 not found
|
||||
|| add_headers(ws::Response::not_found("Not found".into()), "text/plain"),
|
||||
|| error(ErrorType::NotFound, "Not found", "Requested file was not found.", None),
|
||||
// or serve the file
|
||||
|f| add_headers(ws::Response::ok(f.content.into()), &f.mime)
|
||||
))
|
||||
@ -189,3 +200,24 @@ impl ws::Factory for Factory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ErrorType {
|
||||
NotFound,
|
||||
Forbidden,
|
||||
}
|
||||
|
||||
fn error(error: ErrorType, title: &str, message: &str, details: Option<&str>) -> ws::Response {
|
||||
let content = format!(
|
||||
include_str!("./error_tpl.html"),
|
||||
title=title,
|
||||
meta="",
|
||||
message=message,
|
||||
details=details.unwrap_or(""),
|
||||
version=version(),
|
||||
);
|
||||
let res = match error {
|
||||
ErrorType::NotFound => ws::Response::not_found(content),
|
||||
ErrorType::Forbidden => ws::Response::forbidden(content),
|
||||
};
|
||||
add_headers(res, "text/html")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user