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:
Tomasz Drwięga
2016-09-05 19:40:51 +02:00
51 changed files with 1122 additions and 363 deletions

View File

@@ -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" }

View File

@@ -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);

View File

@@ -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
View 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>

View File

@@ -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()));

View File

@@ -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,

View File

@@ -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())
},

View File

@@ -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();

View File

@@ -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,
})
}

View File

@@ -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,
};

View File

@@ -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,
})
}

View File

@@ -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 => {

View File

@@ -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.")
))
}

View File

@@ -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 => {

View File

@@ -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]

View File

@@ -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
View 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);
}

View File

@@ -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,
}
}

View File

@@ -20,6 +20,7 @@ mod helpers;
mod api;
mod authorization;
mod fetch;
mod redirection;
mod validation;

View File

@@ -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]

View File

@@ -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());
}