Merge branch 'master' into dapps-content
Conflicts: dapps/src/apps/fetcher.rs dapps/src/apps/urlhint.rs dapps/src/handlers/client/mod.rs dapps/src/handlers/fetch.rs dapps/src/lib.rs dapps/src/page/local.rs
This commit is contained in:
@@ -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" }
|
||||
|
||||
@@ -60,7 +60,7 @@ impl ContentCache {
|
||||
ContentStatus::Fetching(ref abort) => {
|
||||
trace!(target: "dapps", "Aborting {} because of limit.", entry.0);
|
||||
// Mark as aborted
|
||||
abort.store(true, Ordering::Relaxed);
|
||||
abort.store(true, Ordering::SeqCst);
|
||||
},
|
||||
ContentStatus::Ready(ref endpoint) => {
|
||||
trace!(target: "dapps", "Removing {} because of limit.", entry.0);
|
||||
|
||||
@@ -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;
|
||||
@@ -45,6 +46,7 @@ pub struct ContentFetcher<R: URLHint = URLHintContract> {
|
||||
dapps_path: PathBuf,
|
||||
resolver: R,
|
||||
cache: Arc<Mutex<ContentCache>>,
|
||||
sync: Arc<SyncStatus>,
|
||||
}
|
||||
|
||||
impl<R: URLHint> Drop for ContentFetcher<R> {
|
||||
@@ -56,13 +58,14 @@ impl<R: URLHint> Drop for ContentFetcher<R> {
|
||||
|
||||
impl<R: URLHint> ContentFetcher<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());
|
||||
|
||||
ContentFetcher {
|
||||
dapps_path: dapps_path,
|
||||
resolver: resolver,
|
||||
sync: sync_status,
|
||||
cache: Arc::new(Mutex::new(ContentCache::default())),
|
||||
}
|
||||
}
|
||||
@@ -74,14 +77,20 @@ impl<R: URLHint> ContentFetcher<R> {
|
||||
|
||||
pub fn contains(&self, content_id: &str) -> bool {
|
||||
let mut cache = self.cache.lock();
|
||||
match cache.get(content_id) {
|
||||
// Check if we already have the app
|
||||
Some(_) => true,
|
||||
// fallback to resolver
|
||||
None => match content_id.from_hex() {
|
||||
Ok(content_id) => self.resolver.resolve(content_id).is_some(),
|
||||
_ => false,
|
||||
},
|
||||
// Check if we already have the app
|
||||
if cache.get(content_id).is_some() {
|
||||
return true;
|
||||
}
|
||||
// fallback to resolver
|
||||
if let Ok(content_id) = content_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(content_id).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +98,15 @@ impl<R: URLHint> ContentFetcher<R> {
|
||||
let mut cache = self.cache.lock();
|
||||
let content_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 = cache.get(&content_id);
|
||||
match status {
|
||||
@@ -98,46 +116,59 @@ impl<R: URLHint> ContentFetcher<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 = content_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 content_hex = content_id.from_hex().expect("to_handler is called only when `contains` returns true.");
|
||||
let content = self.resolver.resolve(content_hex);
|
||||
let abort = Arc::new(AtomicBool::new(false));
|
||||
|
||||
(Some(ContentStatus::Fetching(abort.clone())), match app {
|
||||
URLHintResult::Dapp(dapp) => Box::new(ContentFetcherHandler::new(
|
||||
dapp.url(),
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
DappInstaller {
|
||||
id: content_id.clone(),
|
||||
dapps_path: self.dapps_path.clone(),
|
||||
cache: self.cache.clone(),
|
||||
}
|
||||
)) as Box<Handler>,
|
||||
URLHintResult::Content(content) => Box::new(ContentFetcherHandler::new(
|
||||
content.url,
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
ContentInstaller {
|
||||
id: content_id.clone(),
|
||||
mime: content.mime,
|
||||
content_path: self.dapps_path.clone(),
|
||||
cache: self.cache.clone(),
|
||||
}
|
||||
)) as Box<Handler>,
|
||||
})
|
||||
match content {
|
||||
Some(URLHintResult::Dapp(dapp)) => (
|
||||
Some(ContentStatus::Fetching(abort.clone())),
|
||||
Box::new(ContentFetcherHandler::new(
|
||||
dapp.url(),
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
DappInstaller {
|
||||
id: content_id.clone(),
|
||||
dapps_path: self.dapps_path.clone(),
|
||||
cache: self.cache.clone(),
|
||||
})) as Box<Handler>
|
||||
),
|
||||
Some(URLHintResult::Content(content)) => (
|
||||
Some(ContentStatus::Fetching(abort.clone())),
|
||||
Box::new(ContentFetcherHandler::new(
|
||||
content.url,
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
ContentInstaller {
|
||||
id: content_id.clone(),
|
||||
mime: content.mime,
|
||||
content_path: self.dapps_path.clone(),
|
||||
cache: self.cache.clone(),
|
||||
}
|
||||
)) as Box<Handler>,
|
||||
),
|
||||
None => {
|
||||
// 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>)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -155,7 +186,7 @@ impl<R: URLHint> ContentFetcher<R> {
|
||||
pub enum ValidationError {
|
||||
Io(io::Error),
|
||||
Zip(zip::result::ZipError),
|
||||
InvalidDappId,
|
||||
InvalidContentId,
|
||||
ManifestNotFound,
|
||||
ManifestSerialization(String),
|
||||
HashMismatch { expected: H256, got: H256, },
|
||||
@@ -166,7 +197,7 @@ impl fmt::Display for ValidationError {
|
||||
match *self {
|
||||
ValidationError::Io(ref io) => write!(f, "Unexpected IO error occured: {:?}", io),
|
||||
ValidationError::Zip(ref zip) => write!(f, "Unable to read ZIP archive: {:?}", zip),
|
||||
ValidationError::InvalidDappId => write!(f, "Dapp ID is invalid. It should be 32 bytes hash of content."),
|
||||
ValidationError::InvalidContentId => write!(f, "ID is invalid. It should be 26 bits keccak hash of content."),
|
||||
ValidationError::ManifestNotFound => write!(f, "Downloaded Dapp bundle did not contain valid manifest.json file."),
|
||||
ValidationError::ManifestSerialization(ref err) => {
|
||||
write!(f, "There was an error during Dapp Manifest serialization: {:?}", err)
|
||||
@@ -277,7 +308,7 @@ impl ContentValidator for DappInstaller {
|
||||
trace!(target: "dapps", "Opening dapp bundle at {:?}", app_path);
|
||||
let mut file_reader = io::BufReader::new(try!(fs::File::open(app_path)));
|
||||
let hash = try!(sha3(&mut file_reader));
|
||||
let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidDappId));
|
||||
let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidContentId));
|
||||
if id != hash {
|
||||
return Err(ValidationError::HashMismatch {
|
||||
expected: id,
|
||||
@@ -350,6 +381,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;
|
||||
@@ -368,7 +400,7 @@ mod tests {
|
||||
fn should_true_if_contains_the_app() {
|
||||
// given
|
||||
let path = env::temp_dir();
|
||||
let fetcher = ContentFetcher::new(FakeResolver);
|
||||
let fetcher = ContentFetcher::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>
|
||||
@@ -94,7 +94,7 @@ impl Fetch {
|
||||
|
||||
impl Fetch {
|
||||
fn is_aborted(&self) -> bool {
|
||||
self.abort.load(Ordering::Relaxed)
|
||||
self.abort.load(Ordering::SeqCst)
|
||||
}
|
||||
fn mark_aborted(&mut self) -> Next {
|
||||
self.result = Some(Err(Error::Aborted.into()));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
//! Hyper Server Handler that fetches a file during a request (proxy).
|
||||
|
||||
use std::{fs, fmt};
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -120,16 +120,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 };
|
||||
@@ -146,10 +150,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 content within {} seconds.", FETCH_TIMEOUT),
|
||||
None
|
||||
);
|
||||
Self::close_client(&mut self.client);
|
||||
(Some(FetchState::Error(timeout)), Next::write())
|
||||
},
|
||||
@@ -159,28 +165,33 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
|
||||
match rec {
|
||||
// Unpack and validate
|
||||
Ok(Ok(path)) => {
|
||||
trace!(target: "dapps", "Fetching dapp finished. Starting validation.");
|
||||
trace!(target: "dapps", "Fetching content finished. Starting validation.");
|
||||
Self::close_client(&mut self.client);
|
||||
// Unpack and verify
|
||||
let state = match self.installer.validate_and_install(path.clone()) {
|
||||
Err(e) => {
|
||||
trace!(target: "dapps", "Error while validating dapp: {:?}", e);
|
||||
FetchState::Error(ContentHandler::html(
|
||||
trace!(target: "dapps", "Error while validating content: {:?}", e);
|
||||
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 content.",
|
||||
Some(&format!("{:?}", e))
|
||||
))
|
||||
},
|
||||
Ok(result) => FetchState::Done(result)
|
||||
};
|
||||
// Remove temporary zip file
|
||||
let _ = fs::remove_file(path);
|
||||
// TODO [todr] Uncomment me
|
||||
// let _ = fs::remove_file(path);
|
||||
(Some(state), Next::write())
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
warn!(target: "dapps", "Unable to fetch content: {:?}", 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 content.",
|
||||
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 content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar)));
|
||||
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::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,
|
||||
};
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ impl Endpoint for LocalPageEndpoint {
|
||||
app: LocalSingleFile { path: self.path.clone(), mime: mime.clone() },
|
||||
prefix: None,
|
||||
path: path,
|
||||
file: None,
|
||||
file: Default::default(),
|
||||
safe_to_embed: false,
|
||||
})
|
||||
} else {
|
||||
@@ -68,7 +68,7 @@ impl Endpoint for LocalPageEndpoint {
|
||||
app: LocalDapp { path: 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::ContentFetcher;
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user