Update jsonrpc dependencies and rewrite dapps to futures. (#6522)

* Bump version.

* Fix RPC crate.

* Fix BoxFuture in crates.

* Compiles and passes tests!

* Get rid of .boxed()

* Fixing issues with the UI.

* Remove minihttp. Support threads.

* Reimplement files serving to do it in chunks.

* Increase chunk size.

* Remove some unecessary copying.

* Fix tests.

* Fix stratum warning and ipfs todo.

* Switch to proper branch of jsonrpc.

* Update Cargo.lock.

* Update docs.

* Include dapps-glue in workspace.

* fixed merge artifacts

* Fix test compilation.
This commit is contained in:
Tomasz Drwięga
2017-10-05 12:35:01 +02:00
committed by Arkadiy Paronyan
parent 492da38d67
commit e8b418ca03
118 changed files with 2090 additions and 2908 deletions

View File

@@ -16,144 +16,82 @@
use std::sync::Arc;
use hyper::{server, net, Decoder, Encoder, Next, Control};
use hyper::method::Method;
use hyper::status::StatusCode;
use hyper::{Method, StatusCode};
use api::{response, types};
use api::response;
use apps::fetcher::Fetcher;
use handlers::{self, extract_url};
use endpoint::{Endpoint, Handler, EndpointPath};
use node_health::{NodeHealth, HealthStatus, Health};
use parity_reactor::Remote;
use endpoint::{Endpoint, Request, Response, EndpointPath};
use futures::{future, Future};
use node_health::{NodeHealth, HealthStatus};
#[derive(Clone)]
pub struct RestApi {
fetcher: Arc<Fetcher>,
health: NodeHealth,
remote: Remote,
}
impl Endpoint for RestApi {
fn respond(&self, mut path: EndpointPath, req: Request) -> Response {
if let Method::Options = *req.method() {
return Box::new(future::ok(response::empty()));
}
let endpoint = path.app_params.get(0).map(String::to_owned);
let hash = path.app_params.get(1).map(String::to_owned);
// at this point path.app_id contains 'api', adjust it to the hash properly, otherwise
// we will try and retrieve 'api' as the hash when doing the /api/content route
if let Some(ref hash) = hash {
path.app_id = hash.to_owned();
}
trace!(target: "dapps", "Handling /api request: {:?}/{:?}", endpoint, hash);
match endpoint.as_ref().map(String::as_str) {
Some("ping") => Box::new(future::ok(response::ping(req))),
Some("health") => self.health(),
Some("content") => self.resolve_content(hash.as_ref().map(String::as_str), path, req),
_ => Box::new(future::ok(response::not_found())),
}
}
}
impl RestApi {
pub fn new(
fetcher: Arc<Fetcher>,
health: NodeHealth,
remote: Remote,
) -> Box<Endpoint> {
Box::new(RestApi {
fetcher,
health,
remote,
})
}
}
impl Endpoint for RestApi {
fn to_async_handler(&self, path: EndpointPath, control: Control) -> Box<Handler> {
Box::new(RestApiRouter::new((*self).clone(), path, control))
}
}
struct RestApiRouter {
api: RestApi,
path: Option<EndpointPath>,
control: Option<Control>,
handler: Box<Handler>,
}
impl RestApiRouter {
fn new(api: RestApi, path: EndpointPath, control: Control) -> Self {
RestApiRouter {
path: Some(path),
control: Some(control),
api: api,
handler: Box::new(response::as_json_error(StatusCode::NotFound, &types::ApiError {
code: "404".into(),
title: "Not Found".into(),
detail: "Resource you requested has not been found.".into(),
})),
}
}
fn resolve_content(&self, hash: Option<&str>, path: EndpointPath, control: Control) -> Option<Box<Handler>> {
fn resolve_content(&self, hash: Option<&str>, path: EndpointPath, req: Request) -> Response {
trace!(target: "dapps", "Resolving content: {:?} from path: {:?}", hash, path);
match hash {
Some(hash) if self.api.fetcher.contains(hash) => {
Some(self.api.fetcher.to_async_handler(path, control))
Some(hash) if self.fetcher.contains(hash) => {
self.fetcher.respond(path, req)
},
_ => None
_ => Box::new(future::ok(response::not_found())),
}
}
fn health(&self, control: Control) -> Box<Handler> {
let map = move |health: Result<Result<Health, ()>, ()>| {
let status = match health {
Ok(Ok(ref health)) => {
if [&health.peers.status, &health.sync.status].iter().any(|x| *x != &HealthStatus::Ok) {
StatusCode::PreconditionFailed // HTTP 412
} else {
StatusCode::Ok // HTTP 200
}
},
_ => StatusCode::ServiceUnavailable, // HTTP 503
};
fn health(&self) -> Response {
Box::new(self.health.health()
.then(|health| {
let status = match health {
Ok(ref health) => {
if [&health.peers.status, &health.sync.status].iter().any(|x| *x != &HealthStatus::Ok) {
StatusCode::PreconditionFailed // HTTP 412
} else {
StatusCode::Ok // HTTP 200
}
},
_ => StatusCode::ServiceUnavailable, // HTTP 503
};
response::as_json(status, &health)
};
let health = self.api.health.health();
let remote = self.api.remote.clone();
Box::new(handlers::AsyncHandler::new(health, map, remote, control))
}
}
impl server::Handler<net::HttpStream> for RestApiRouter {
fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next {
if let Method::Options = *request.method() {
self.handler = response::empty();
return Next::write();
}
// TODO [ToDr] Consider using `path.app_params` instead
let url = extract_url(&request);
if url.is_none() {
// Just return 404 if we can't parse URL
return Next::write();
}
let url = url.expect("Check for None early-exists above; qed");
let mut path = self.path.take().expect("on_request called only once, and path is always defined in new; qed");
let control = self.control.take().expect("on_request called only once, and control is always defined in new; qed");
let endpoint = url.path.get(1).map(|v| v.as_str());
let hash = url.path.get(2).map(|v| v.as_str());
// at this point path.app_id contains 'api', adjust it to the hash properly, otherwise
// we will try and retrieve 'api' as the hash when doing the /api/content route
if let Some(ref hash) = hash { path.app_id = hash.clone().to_owned() }
let handler = endpoint.and_then(|v| match v {
"ping" => Some(response::ping()),
"health" => Some(self.health(control)),
"content" => self.resolve_content(hash, path, control),
_ => None
});
// Overwrite default
if let Some(h) = handler {
self.handler = h;
}
self.handler.on_request(request)
}
fn on_request_readable(&mut self, decoder: &mut Decoder<net::HttpStream>) -> Next {
self.handler.on_request_readable(decoder)
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
self.handler.on_response(res)
}
fn on_response_writable(&mut self, encoder: &mut Encoder<net::HttpStream>) -> Next {
self.handler.on_response_writable(encoder)
Ok(response::as_json(status, &health).into())
})
)
}
}

View File

@@ -16,27 +16,28 @@
use serde::Serialize;
use serde_json;
use hyper::status::StatusCode;
use hyper::{self, mime, StatusCode};
use endpoint::Handler;
use handlers::{ContentHandler, EchoHandler};
pub fn empty() -> Box<Handler> {
Box::new(ContentHandler::ok("".into(), mime!(Text/Plain)))
pub fn empty() -> hyper::Response {
ContentHandler::ok("".into(), mime::TEXT_PLAIN).into()
}
pub fn as_json<T: Serialize>(status: StatusCode, val: &T) -> ContentHandler {
pub fn as_json<T: Serialize>(status: StatusCode, val: &T) -> hyper::Response {
let json = serde_json::to_string(val)
.expect("serialization to string is infallible; qed");
ContentHandler::new(status, json, mime!(Application/Json))
ContentHandler::new(status, json, mime::APPLICATION_JSON).into()
}
pub fn as_json_error<T: Serialize>(status: StatusCode, val: &T) -> ContentHandler {
let json = serde_json::to_string(val)
.expect("serialization to string is infallible; qed");
ContentHandler::new(status, json, mime!(Application/Json))
pub fn ping(req: hyper::Request) -> hyper::Response {
EchoHandler::new(req).into()
}
pub fn ping() -> Box<Handler> {
Box::new(EchoHandler::default())
pub fn not_found() -> hyper::Response {
as_json(StatusCode::NotFound, &::api::types::ApiError {
code: "404".into(),
title: "Not Found".into(),
detail: "Resource you requested has not been found.".into(),
})
}

View File

@@ -19,12 +19,12 @@
use std::fs;
use linked_hash_map::LinkedHashMap;
use page::LocalPageEndpoint;
use page::local;
use handlers::FetchControl;
pub enum ContentStatus {
Fetching(FetchControl),
Ready(LocalPageEndpoint),
Ready(local::Dapp),
}
#[derive(Default)]

View File

@@ -18,16 +18,17 @@ use zip;
use std::{fs, fmt};
use std::io::{self, Read, Write};
use std::path::PathBuf;
use fetch::{self, Mime};
use hash::keccak_buffer;
use bigint::hash::H256;
use fetch::{self, Mime};
use futures_cpupool::CpuPool;
use hash::keccak_buffer;
use page::{LocalPageEndpoint, PageCache};
use handlers::{ContentValidator, ValidatorResponse};
use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest, serialize_manifest, Manifest};
use handlers::{ContentValidator, ValidatorResponse};
use page::{local, PageCache};
use Embeddable;
type OnDone = Box<Fn(Option<LocalPageEndpoint>) + Send>;
type OnDone = Box<Fn(Option<local::Dapp>) + Send>;
fn write_response_and_check_hash(
id: &str,
@@ -75,15 +76,17 @@ pub struct Content {
mime: Mime,
content_path: PathBuf,
on_done: OnDone,
pool: CpuPool,
}
impl Content {
pub fn new(id: String, mime: Mime, content_path: PathBuf, on_done: OnDone) -> Self {
pub fn new(id: String, mime: Mime, content_path: PathBuf, on_done: OnDone, pool: CpuPool) -> Self {
Content {
id: id,
mime: mime,
content_path: content_path,
on_done: on_done,
id,
mime,
content_path,
on_done,
pool,
}
}
}
@@ -91,12 +94,15 @@ impl Content {
impl ContentValidator for Content {
type Error = ValidationError;
fn validate_and_install(&self, response: fetch::Response) -> Result<ValidatorResponse, ValidationError> {
let validate = |content_path: PathBuf| {
fn validate_and_install(self, response: fetch::Response) -> Result<ValidatorResponse, ValidationError> {
let pool = self.pool;
let id = self.id.clone();
let mime = self.mime;
let validate = move |content_path: PathBuf| {
// Create dir
let (_, content_path) = write_response_and_check_hash(self.id.as_str(), content_path.clone(), self.id.as_str(), response)?;
let (_, content_path) = write_response_and_check_hash(&id, content_path, &id, response)?;
Ok(LocalPageEndpoint::single_file(content_path, self.mime.clone(), PageCache::Enabled))
Ok(local::Dapp::single_file(pool, content_path, mime, PageCache::Enabled))
};
// Prepare path for a file
@@ -118,15 +124,17 @@ pub struct Dapp {
dapps_path: PathBuf,
on_done: OnDone,
embeddable_on: Embeddable,
pool: CpuPool,
}
impl Dapp {
pub fn new(id: String, dapps_path: PathBuf, on_done: OnDone, embeddable_on: Embeddable) -> Self {
pub fn new(id: String, dapps_path: PathBuf, on_done: OnDone, embeddable_on: Embeddable, pool: CpuPool) -> Self {
Dapp {
id,
dapps_path,
on_done,
embeddable_on,
pool,
}
}
@@ -158,16 +166,19 @@ impl Dapp {
impl ContentValidator for Dapp {
type Error = ValidationError;
fn validate_and_install(&self, response: fetch::Response) -> Result<ValidatorResponse, ValidationError> {
let validate = |dapp_path: PathBuf| {
let (file, zip_path) = write_response_and_check_hash(self.id.as_str(), dapp_path.clone(), &format!("{}.zip", self.id), response)?;
fn validate_and_install(self, response: fetch::Response) -> Result<ValidatorResponse, ValidationError> {
let id = self.id.clone();
let pool = self.pool;
let embeddable_on = self.embeddable_on;
let validate = move |dapp_path: PathBuf| {
let (file, zip_path) = write_response_and_check_hash(&id, dapp_path.clone(), &format!("{}.zip", id), response)?;
trace!(target: "dapps", "Opening dapp bundle at {:?}", zip_path);
// Unpack archive
let mut zip = zip::ZipArchive::new(file)?;
// First find manifest file
let (mut manifest, manifest_dir) = Self::find_manifest(&mut zip)?;
// Overwrite id to match hash
manifest.id = self.id.clone();
manifest.id = id;
// Unpack zip
for i in 0..zip.len() {
@@ -198,7 +209,7 @@ impl ContentValidator for Dapp {
let mut manifest_file = fs::File::create(manifest_path)?;
manifest_file.write_all(manifest_str.as_bytes())?;
// Create endpoint
let endpoint = LocalPageEndpoint::new(dapp_path, manifest.clone().into(), PageCache::Enabled, self.embeddable_on.clone());
let endpoint = local::Dapp::new(pool, dapp_path, manifest.into(), PageCache::Enabled, embeddable_on);
Ok(endpoint)
};

View File

@@ -24,27 +24,25 @@ use std::{fs, env};
use std::path::PathBuf;
use std::sync::Arc;
use rustc_hex::FromHex;
use futures::{future, Future};
use futures_cpupool::CpuPool;
use fetch::{Client as FetchClient, Fetch};
use hash_fetch::urlhint::{URLHintContract, URLHint, URLHintResult};
use parity_reactor::Remote;
use hyper;
use hyper::status::StatusCode;
use hyper::StatusCode;
use {Embeddable, SyncStatus, random_filename};
use parking_lot::Mutex;
use page::LocalPageEndpoint;
use page::local;
use handlers::{ContentHandler, ContentFetcherHandler};
use endpoint::{Endpoint, EndpointPath, Handler};
use endpoint::{self, Endpoint, EndpointPath};
use apps::cache::{ContentCache, ContentStatus};
/// Limit of cached dapps/content
const MAX_CACHED_DAPPS: usize = 20;
pub trait Fetcher: Send + Sync + 'static {
pub trait Fetcher: Endpoint + 'static {
fn contains(&self, content_id: &str) -> bool;
fn to_async_handler(&self, path: EndpointPath, control: hyper::Control) -> Box<Handler>;
}
pub struct ContentFetcher<F: Fetch = FetchClient, R: URLHint + 'static = URLHintContract> {
@@ -53,8 +51,8 @@ pub struct ContentFetcher<F: Fetch = FetchClient, R: URLHint + 'static = URLHint
cache: Arc<Mutex<ContentCache>>,
sync: Arc<SyncStatus>,
embeddable_on: Embeddable,
remote: Remote,
fetch: F,
pool: CpuPool,
only_content: bool,
}
@@ -66,24 +64,23 @@ impl<R: URLHint + 'static, F: Fetch> Drop for ContentFetcher<F, R> {
}
impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
pub fn new(
resolver: R,
sync_status: Arc<SyncStatus>,
remote: Remote,
sync: Arc<SyncStatus>,
fetch: F,
pool: CpuPool,
) -> Self {
let mut cache_path = env::temp_dir();
cache_path.push(random_filename());
ContentFetcher {
cache_path: cache_path,
resolver: resolver,
sync: sync_status,
cache_path,
resolver,
sync,
cache: Arc::new(Mutex::new(ContentCache::default())),
embeddable_on: None,
remote: remote,
fetch: fetch,
fetch,
pool,
only_content: true,
}
}
@@ -98,24 +95,34 @@ impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
self
}
fn still_syncing(embeddable: Embeddable) -> Box<Handler> {
Box::new(ContentHandler::error(
fn not_found(embeddable: Embeddable) -> endpoint::Response {
Box::new(future::ok(ContentHandler::error(
StatusCode::NotFound,
"Resource Not Found",
"Requested resource was not found.",
None,
embeddable,
).into()))
}
fn still_syncing(embeddable: Embeddable) -> endpoint::Response {
Box::new(future::ok(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>"),
embeddable,
))
).into()))
}
fn dapps_disabled(address: Embeddable) -> Box<Handler> {
Box::new(ContentHandler::error(
fn dapps_disabled(address: Embeddable) -> endpoint::Response {
Box::new(future::ok(ContentHandler::error(
StatusCode::ServiceUnavailable,
"Network Dapps Not Available",
"This interface doesn't support network dapps for security reasons.",
None,
address,
))
).into()))
}
#[cfg(test)]
@@ -126,8 +133,6 @@ impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
// resolve contract call synchronously.
// TODO: port to futures-based hyper and make it all async.
fn resolve(&self, content_id: Vec<u8>) -> Option<URLHintResult> {
use futures::Future;
self.resolver.resolve(content_id)
.wait()
.unwrap_or_else(|e| { warn!("Error resolving content-id: {}", e); None })
@@ -151,8 +156,10 @@ impl<R: URLHint + 'static, F: Fetch> Fetcher for ContentFetcher<F, R> {
false
}
}
}
fn to_async_handler(&self, path: EndpointPath, control: hyper::Control) -> Box<Handler> {
impl<R: URLHint + 'static, F: Fetch> Endpoint for ContentFetcher<F, R> {
fn respond(&self, path: EndpointPath, req: endpoint::Request) -> endpoint::Response {
let mut cache = self.cache.lock();
let content_id = path.app_id.clone();
@@ -161,12 +168,12 @@ impl<R: URLHint + 'static, F: Fetch> Fetcher for ContentFetcher<F, R> {
match status {
// Just serve the content
Some(&mut ContentStatus::Ready(ref endpoint)) => {
(None, endpoint.to_async_handler(path, control))
(None, endpoint.to_response(&path))
},
// Content is already being fetched
Some(&mut ContentStatus::Fetching(ref fetch_control)) if !fetch_control.is_deadline_reached() => {
trace!(target: "dapps", "Content fetching in progress. Waiting...");
(None, fetch_control.to_async_handler(path, control))
(None, fetch_control.to_response(path))
},
// We need to start fetching the content
_ => {
@@ -176,7 +183,7 @@ impl<R: URLHint + 'static, F: Fetch> Fetcher for ContentFetcher<F, R> {
let cache = self.cache.clone();
let id = content_id.clone();
let on_done = move |result: Option<LocalPageEndpoint>| {
let on_done = move |result: Option<local::Dapp>| {
let mut cache = cache.lock();
match result {
Some(endpoint) => cache.insert(id.clone(), ContentStatus::Ready(endpoint)),
@@ -195,39 +202,39 @@ impl<R: URLHint + 'static, F: Fetch> Fetcher for ContentFetcher<F, R> {
},
Some(URLHintResult::Dapp(dapp)) => {
let handler = ContentFetcherHandler::new(
dapp.url(),
req.method(),
&dapp.url(),
path,
control,
installers::Dapp::new(
content_id.clone(),
self.cache_path.clone(),
Box::new(on_done),
self.embeddable_on.clone(),
self.pool.clone(),
),
self.embeddable_on.clone(),
self.remote.clone(),
self.fetch.clone(),
);
(Some(ContentStatus::Fetching(handler.fetch_control())), Box::new(handler) as Box<Handler>)
(Some(ContentStatus::Fetching(handler.fetch_control())), Box::new(handler) as endpoint::Response)
},
Some(URLHintResult::Content(content)) => {
let handler = ContentFetcherHandler::new(
content.url,
req.method(),
&content.url,
path,
control,
installers::Content::new(
content_id.clone(),
content.mime,
self.cache_path.clone(),
Box::new(on_done),
self.pool.clone(),
),
self.embeddable_on.clone(),
self.remote.clone(),
self.fetch.clone(),
);
(Some(ContentStatus::Fetching(handler.fetch_control())), Box::new(handler) as Box<Handler>)
(Some(ContentStatus::Fetching(handler.fetch_control())), Box::new(handler) as endpoint::Response)
},
None if self.sync.is_major_importing() => {
(None, Self::still_syncing(self.embeddable_on.clone()))
@@ -235,13 +242,7 @@ impl<R: URLHint + 'static, F: Fetch> Fetcher for ContentFetcher<F, R> {
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,
self.embeddable_on.clone(),
)) as Box<Handler>)
(None, Self::not_found(self.embeddable_on.clone()))
},
}
},
@@ -263,13 +264,12 @@ mod tests {
use std::sync::Arc;
use bytes::Bytes;
use fetch::{Fetch, Client};
use futures::{future, Future, BoxFuture};
use hash_fetch::urlhint::{URLHint, URLHintResult};
use parity_reactor::Remote;
use futures::future;
use hash_fetch::urlhint::{URLHint, URLHintResult, BoxFuture};
use apps::cache::ContentStatus;
use endpoint::EndpointInfo;
use page::LocalPageEndpoint;
use page::local;
use super::{ContentFetcher, Fetcher};
use {SyncStatus};
@@ -277,7 +277,7 @@ mod tests {
struct FakeResolver;
impl URLHint for FakeResolver {
fn resolve(&self, _id: Bytes) -> BoxFuture<Option<URLHintResult>, String> {
future::ok(None).boxed()
Box::new(future::ok(None))
}
}
@@ -291,10 +291,16 @@ mod tests {
#[test]
fn should_true_if_contains_the_app() {
// given
let pool = ::futures_cpupool::CpuPool::new(1);
let path = env::temp_dir();
let fetcher = ContentFetcher::new(FakeResolver, Arc::new(FakeSync(false)), Remote::new_sync(), Client::new().unwrap())
.allow_dapps(true);
let handler = LocalPageEndpoint::new(path, EndpointInfo {
let fetcher = ContentFetcher::new(
FakeResolver,
Arc::new(FakeSync(false)),
Client::new().unwrap(),
pool.clone(),
).allow_dapps(true);
let handler = local::Dapp::new(pool, path, EndpointInfo {
name: "fake".into(),
description: "".into(),
version: "".into(),

View File

@@ -19,9 +19,11 @@ use std::io;
use std::io::Read;
use std::fs;
use std::path::{Path, PathBuf};
use page::{LocalPageEndpoint, PageCache};
use endpoint::{Endpoint, EndpointInfo};
use futures_cpupool::CpuPool;
use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest};
use endpoint::{Endpoint, EndpointInfo};
use page::{local, PageCache};
use Embeddable;
struct LocalDapp {
@@ -61,14 +63,14 @@ fn read_manifest(name: &str, mut path: PathBuf) -> EndpointInfo {
/// Returns Dapp Id and Local Dapp Endpoint for given filesystem path.
/// Parses the path to extract last component (for name).
/// `None` is returned when path is invalid or non-existent.
pub fn local_endpoint<P: AsRef<Path>>(path: P, embeddable: Embeddable) -> Option<(String, Box<LocalPageEndpoint>)> {
pub fn local_endpoint<P: AsRef<Path>>(path: P, embeddable: Embeddable, pool: CpuPool) -> Option<(String, Box<local::Dapp>)> {
let path = path.as_ref().to_owned();
path.canonicalize().ok().and_then(|path| {
let name = path.file_name().and_then(|name| name.to_str());
name.map(|name| {
let dapp = local_dapp(name.into(), path.clone());
(dapp.id, Box::new(LocalPageEndpoint::new(
dapp.path, dapp.info, PageCache::Disabled, embeddable.clone())
(dapp.id, Box::new(local::Dapp::new(
pool.clone(), dapp.path, dapp.info, PageCache::Disabled, embeddable.clone())
))
})
})
@@ -86,13 +88,13 @@ fn local_dapp(name: String, path: PathBuf) -> LocalDapp {
}
/// Returns endpoints for Local Dapps found for given filesystem path.
/// Scans the directory and collects `LocalPageEndpoints`.
pub fn local_endpoints<P: AsRef<Path>>(dapps_path: P, embeddable: Embeddable) -> BTreeMap<String, Box<Endpoint>> {
/// Scans the directory and collects `local::Dapp`.
pub fn local_endpoints<P: AsRef<Path>>(dapps_path: P, embeddable: Embeddable, pool: CpuPool) -> BTreeMap<String, Box<Endpoint>> {
let mut pages = BTreeMap::<String, Box<Endpoint>>::new();
for dapp in local_dapps(dapps_path.as_ref()) {
pages.insert(
dapp.id,
Box::new(LocalPageEndpoint::new(dapp.path, dapp.info, PageCache::Disabled, embeddable.clone()))
Box::new(local::Dapp::new(pool.clone(), dapp.path, dapp.info, PageCache::Disabled, embeddable.clone()))
);
}
pages

View File

@@ -18,12 +18,12 @@ use std::path::PathBuf;
use std::sync::Arc;
use endpoint::{Endpoints, Endpoint};
use page::PageEndpoint;
use futures_cpupool::CpuPool;
use page;
use proxypac::ProxyPac;
use web::Web;
use fetch::Fetch;
use parity_dapps::WebApp;
use parity_reactor::Remote;
use parity_ui;
use {WebProxyTokens, ParentFrameSettings};
@@ -43,12 +43,12 @@ pub const UTILS_PATH: &'static str = "parity-utils";
pub const WEB_PATH: &'static str = "web";
pub const URL_REFERER: &'static str = "__referer=";
pub fn utils() -> Box<Endpoint> {
Box::new(PageEndpoint::with_prefix(parity_ui::App::default(), UTILS_PATH.to_owned()))
pub fn utils(pool: CpuPool) -> Box<Endpoint> {
Box::new(page::builtin::Dapp::new(pool, parity_ui::App::default()))
}
pub fn ui() -> Box<Endpoint> {
Box::new(PageEndpoint::with_fallback_to_index(parity_ui::App::default()))
pub fn ui(pool: CpuPool) -> Box<Endpoint> {
Box::new(page::builtin::Dapp::with_fallback_to_index(pool, parity_ui::App::default()))
}
pub fn ui_redirection(embeddable: Option<ParentFrameSettings>) -> Box<Endpoint> {
@@ -61,14 +61,14 @@ pub fn all_endpoints<F: Fetch>(
dapps_domain: &str,
embeddable: Option<ParentFrameSettings>,
web_proxy_tokens: Arc<WebProxyTokens>,
remote: Remote,
fetch: F,
pool: CpuPool,
) -> (Vec<String>, Endpoints) {
// fetch fs dapps at first to avoid overwriting builtins
let mut pages = fs::local_endpoints(dapps_path.clone(), embeddable.clone());
let mut pages = fs::local_endpoints(dapps_path.clone(), embeddable.clone(), pool.clone());
let local_endpoints: Vec<String> = pages.keys().cloned().collect();
for path in extra_dapps {
if let Some((id, endpoint)) = fs::local_endpoint(path.clone(), embeddable.clone()) {
if let Some((id, endpoint)) = fs::local_endpoint(path.clone(), embeddable.clone(), pool.clone()) {
pages.insert(id, endpoint);
} else {
warn!(target: "dapps", "Ignoring invalid dapp at {}", path.display());
@@ -76,17 +76,17 @@ pub fn all_endpoints<F: Fetch>(
}
// NOTE [ToDr] Dapps will be currently embeded on 8180
insert::<parity_ui::App>(&mut pages, "ui", Embeddable::Yes(embeddable.clone()));
insert::<parity_ui::App>(&mut pages, "ui", Embeddable::Yes(embeddable.clone()), pool.clone());
pages.insert("proxy".into(), ProxyPac::boxed(embeddable.clone(), dapps_domain.to_owned()));
pages.insert(WEB_PATH.into(), Web::boxed(embeddable.clone(), web_proxy_tokens.clone(), remote.clone(), fetch.clone()));
pages.insert(WEB_PATH.into(), Web::boxed(embeddable.clone(), web_proxy_tokens.clone(), fetch.clone()));
(local_endpoints, pages)
}
fn insert<T : WebApp + Default + 'static>(pages: &mut Endpoints, id: &str, embed_at: Embeddable) {
fn insert<T : WebApp + Default + 'static>(pages: &mut Endpoints, id: &str, embed_at: Embeddable, pool: CpuPool) {
pages.insert(id.to_owned(), Box::new(match embed_at {
Embeddable::Yes(address) => PageEndpoint::new_safe_to_embed(T::default(), address),
Embeddable::No => PageEndpoint::new(T::default()),
Embeddable::Yes(address) => page::builtin::Dapp::new_safe_to_embed(pool, T::default(), address),
Embeddable::No => page::builtin::Dapp::new(pool, T::default()),
}));
}

View File

@@ -16,9 +16,10 @@
//! UI redirections
use hyper::{Control, StatusCode};
use hyper::StatusCode;
use futures::future;
use endpoint::{Endpoint, Handler, EndpointPath};
use endpoint::{Endpoint, Request, Response, EndpointPath};
use {handlers, Embeddable};
/// Redirection to UI server.
@@ -37,19 +38,20 @@ impl Redirection {
}
impl Endpoint for Redirection {
fn to_async_handler(&self, _path: EndpointPath, _control: Control) -> Box<Handler> {
if let Some(ref frame) = self.embeddable_on {
fn respond(&self, _path: EndpointPath, req: Request) -> Response {
Box::new(future::ok(if let Some(ref frame) = self.embeddable_on {
trace!(target: "dapps", "Redirecting to signer interface.");
handlers::Redirection::boxed(&format!("http://{}:{}", &frame.host, frame.port))
let protocol = req.uri().scheme().unwrap_or("http");
handlers::Redirection::new(format!("{}://{}:{}", protocol, &frame.host, frame.port)).into()
} else {
trace!(target: "dapps", "Signer disabled, returning 404.");
Box::new(handlers::ContentHandler::error(
handlers::ContentHandler::error(
StatusCode::NotFound,
"404 Not Found",
"Your homepage is not available when Trusted Signer is disabled.",
Some("You can still access dapps by writing a correct address, though. Re-enable Signer to get your homepage back."),
None,
))
}
).into()
}))
}
}

View File

@@ -18,17 +18,25 @@
use std::collections::BTreeMap;
use hyper::{self, server, net};
use jsonrpc_core::BoxFuture;
use hyper;
#[derive(Debug, PartialEq, Default, Clone)]
pub struct EndpointPath {
pub app_id: String,
pub app_params: Vec<String>,
pub query: Option<String>,
pub host: String,
pub port: u16,
pub using_dapps_domains: bool,
}
impl EndpointPath {
pub fn has_no_params(&self) -> bool {
self.app_params.is_empty() || self.app_params.iter().all(|x| x.is_empty())
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct EndpointInfo {
pub name: String,
@@ -39,16 +47,11 @@ pub struct EndpointInfo {
}
pub type Endpoints = BTreeMap<String, Box<Endpoint>>;
pub type Handler = server::Handler<net::HttpStream> + Send;
pub type Response = BoxFuture<hyper::Response, hyper::Error>;
pub type Request = hyper::Request;
pub trait Endpoint : Send + Sync {
fn info(&self) -> Option<&EndpointInfo> { None }
fn to_handler(&self, _path: EndpointPath) -> Box<Handler> {
panic!("This Endpoint is asynchronous and requires Control object.");
}
fn to_async_handler(&self, path: EndpointPath, _control: hyper::Control) -> Box<Handler> {
self.to_handler(path)
}
fn respond(&self, path: EndpointPath, req: Request) -> Response;
}

View File

@@ -1,112 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Async Content Handler
//! Temporary solution until we switch to future-based server.
//! Wraps a future and converts it to hyper::server::Handler;
use std::{mem, time};
use std::sync::mpsc;
use futures::Future;
use hyper::{server, Decoder, Encoder, Next, Control};
use hyper::net::HttpStream;
use handlers::ContentHandler;
use parity_reactor::Remote;
const TIMEOUT_SECS: u64 = 15;
enum State<F, T, M> {
Initial(F, M, Remote, Control),
Waiting(mpsc::Receiver<Result<T, ()>>, M),
Done(ContentHandler),
Invalid,
}
pub struct AsyncHandler<F, T, M> {
state: State<F, T, M>,
}
impl<F, T, M> AsyncHandler<F, T, M> {
pub fn new(future: F, map: M, remote: Remote, control: Control) -> Self {
AsyncHandler {
state: State::Initial(future, map, remote, control),
}
}
}
impl<F, T, E, M> server::Handler<HttpStream> for AsyncHandler<F, Result<T, E>, M> where
F: Future<Item=T, Error=E> + Send + 'static,
M: FnOnce(Result<Result<T, E>, ()>) -> ContentHandler,
T: Send + 'static,
E: Send + 'static,
{
fn on_request(&mut self, _request: server::Request<HttpStream>) -> Next {
if let State::Initial(future, map, remote, control) = mem::replace(&mut self.state, State::Invalid) {
let (tx, rx) = mpsc::sync_channel(1);
let control2 = control.clone();
let tx2 = tx.clone();
remote.spawn_with_timeout(move || future.then(move |result| {
// Send a result (ignore errors if the connection was dropped)
let _ = tx.send(Ok(result));
// Resume handler
let _ = control.ready(Next::read());
Ok(())
}), time::Duration::from_secs(TIMEOUT_SECS), move || {
// Notify about error
let _ = tx2.send(Err(()));
// Resume handler
let _ = control2.ready(Next::read());
});
self.state = State::Waiting(rx, map);
}
Next::wait()
}
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next {
if let State::Waiting(rx, map) = mem::replace(&mut self.state, State::Invalid) {
match rx.try_recv() {
Ok(result) => {
self.state = State::Done(map(result));
},
Err(err) => {
warn!("Resuming handler in incorrect state: {:?}", err);
}
}
}
Next::write()
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
if let State::Done(ref mut handler) = self.state {
handler.on_response(res)
} else {
Next::end()
}
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
if let State::Done(ref mut handler) = self.state {
handler.on_response_writable(encoder)
} else {
Next::end()
}
}
}

View File

@@ -16,32 +16,29 @@
//! Simple Content Handler
use hyper::{header, server, Decoder, Encoder, Next};
use hyper::net::HttpStream;
use hyper::mime::Mime;
use hyper::status::StatusCode;
use hyper::{self, mime, header};
use hyper::StatusCode;
use util::version;
use handlers::add_security_headers;
use Embeddable;
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct ContentHandler {
code: StatusCode,
content: String,
mimetype: Mime,
write_pos: usize,
mimetype: mime::Mime,
safe_to_embed_on: Embeddable,
}
impl ContentHandler {
pub fn ok(content: String, mimetype: Mime) -> Self {
pub fn ok(content: String, mimetype: mime::Mime) -> Self {
Self::new(StatusCode::Ok, content, mimetype)
}
pub fn html(code: StatusCode, content: String, embeddable_on: Embeddable) -> Self {
Self::new_embeddable(code, content, mime!(Text/Html), embeddable_on)
Self::new_embeddable(code, content, mime::TEXT_HTML, embeddable_on)
}
pub fn error(
@@ -60,57 +57,32 @@ impl ContentHandler {
), embeddable_on)
}
pub fn new(code: StatusCode, content: String, mimetype: Mime) -> Self {
pub fn new(code: StatusCode, content: String, mimetype: mime::Mime) -> Self {
Self::new_embeddable(code, content, mimetype, None)
}
pub fn new_embeddable(
code: StatusCode,
content: String,
mimetype: Mime,
mimetype: mime::Mime,
safe_to_embed_on: Embeddable,
) -> Self {
ContentHandler {
code,
content,
mimetype,
write_pos: 0,
safe_to_embed_on,
}
}
}
impl server::Handler<HttpStream> for ContentHandler {
fn on_request(&mut self, _request: server::Request<HttpStream>) -> Next {
Next::write()
}
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next {
Next::write()
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
res.set_status(self.code);
res.headers_mut().set(header::ContentType(self.mimetype.clone()));
add_security_headers(&mut res.headers_mut(), self.safe_to_embed_on.take());
Next::write()
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
let bytes = self.content.as_bytes();
if self.write_pos == bytes.len() {
return Next::end();
}
match encoder.write(&bytes[self.write_pos..]) {
Ok(bytes) => {
self.write_pos += bytes;
Next::write()
},
Err(e) => match e.kind() {
::std::io::ErrorKind::WouldBlock => Next::write(),
_ => Next::end()
},
}
impl Into<hyper::Response> for ContentHandler {
fn into(self) -> hyper::Response {
let mut res = hyper::Response::new()
.with_status(self.code)
.with_header(header::ContentType(self.mimetype))
.with_body(self.content);
add_security_headers(&mut res.headers_mut(), self.safe_to_embed_on);
res
}
}

View File

@@ -16,45 +16,31 @@
//! Echo Handler
use std::io::Read;
use hyper::{server, Decoder, Encoder, Next};
use hyper::net::HttpStream;
use super::ContentHandler;
use hyper::{self, header};
#[derive(Default)]
use handlers::add_security_headers;
#[derive(Debug)]
pub struct EchoHandler {
content: String,
handler: Option<ContentHandler>,
request: hyper::Request,
}
impl server::Handler<HttpStream> for EchoHandler {
fn on_request(&mut self, _: server::Request<HttpStream>) -> Next {
Next::read()
}
fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
match decoder.read_to_string(&mut self.content) {
Ok(0) => {
self.handler = Some(ContentHandler::ok(self.content.clone(), mime!(Application/Json)));
Next::write()
},
Ok(_) => Next::read(),
Err(e) => match e.kind() {
::std::io::ErrorKind::WouldBlock => Next::read(),
_ => Next::end(),
}
impl EchoHandler {
pub fn new(request: hyper::Request) -> Self {
EchoHandler {
request,
}
}
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
self.handler.as_mut()
.expect("handler always set in on_request, which is before now; qed")
.on_response(res)
}
impl Into<hyper::Response> for EchoHandler {
fn into(self) -> hyper::Response {
let content_type = self.request.headers().get().cloned();
let mut res = hyper::Response::new()
.with_header(content_type.unwrap_or(header::ContentType::json()))
.with_body(self.request.body());
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
self.handler.as_mut()
.expect("handler always set in on_request, which is before now; qed")
.on_response_writable(encoder)
add_security_headers(res.headers_mut(), None);
res
}
}

View File

@@ -16,57 +16,39 @@
//! Hyper Server Handler that fetches a file during a request (proxy).
use std::fmt;
use std::sync::{mpsc, Arc};
use std::{fmt, mem};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Instant, Duration};
use fetch::{self, Fetch};
use futures::Future;
use parity_reactor::Remote;
use futures::sync::oneshot;
use futures::{self, Future};
use hyper::{self, Method, StatusCode};
use jsonrpc_core::BoxFuture;
use parking_lot::Mutex;
use hyper::{server, Decoder, Encoder, Next, Method, Control};
use hyper::net::HttpStream;
use hyper::uri::RequestUri;
use hyper::status::StatusCode;
use endpoint::EndpointPath;
use endpoint::{self, EndpointPath};
use handlers::{ContentHandler, StreamingHandler};
use page::{LocalPageEndpoint, PageHandlerWaiting};
use page::local;
use {Embeddable};
const FETCH_TIMEOUT: u64 = 300;
pub enum ValidatorResponse {
Local(LocalPageEndpoint),
Local(local::Dapp),
Streaming(StreamingHandler<fetch::Response>),
}
pub trait ContentValidator: Send + 'static {
pub trait ContentValidator: Sized + Send + 'static {
type Error: fmt::Debug + fmt::Display;
fn validate_and_install(&self, fetch::Response) -> Result<ValidatorResponse, Self::Error>;
fn validate_and_install(self, fetch::Response) -> Result<ValidatorResponse, Self::Error>;
}
enum FetchState {
Waiting,
NotStarted(String),
Error(ContentHandler),
InProgress(mpsc::Receiver<FetchState>),
Streaming(StreamingHandler<fetch::Response>),
Done(LocalPageEndpoint, Box<PageHandlerWaiting>),
}
enum WaitResult {
Error(ContentHandler),
Done(LocalPageEndpoint),
NonAwaitable,
}
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct FetchControl {
abort: Arc<AtomicBool>,
listeners: Arc<Mutex<Vec<(Control, mpsc::Sender<WaitResult>)>>>,
listeners: Arc<Mutex<Vec<oneshot::Sender<WaitResult>>>>,
deadline: Instant,
}
@@ -81,14 +63,30 @@ impl Default for FetchControl {
}
impl FetchControl {
pub fn is_deadline_reached(&self) -> bool {
self.deadline < Instant::now()
}
pub fn abort(&self) {
self.abort.store(true, Ordering::SeqCst);
}
pub fn to_response(&self, path: EndpointPath) -> endpoint::Response {
let (tx, receiver) = oneshot::channel();
self.listeners.lock().push(tx);
Box::new(WaitingHandler {
path,
state: WaitState::Waiting(receiver),
})
}
fn notify<F: Fn() -> WaitResult>(&self, status: F) {
let mut listeners = self.listeners.lock();
for (control, sender) in listeners.drain(..) {
for sender in listeners.drain(..) {
trace!(target: "dapps", "Resuming request waiting for content...");
if let Err(e) = sender.send(status()) {
trace!(target: "dapps", "Waiting listener notification failed: {:?}", e);
} else {
let _ = control.ready(Next::read());
if let Err(_) = sender.send(status()) {
trace!(target: "dapps", "Waiting listener notification failed.");
}
}
}
@@ -98,92 +96,79 @@ impl FetchControl {
FetchState::Error(ref handler) => self.notify(|| WaitResult::Error(handler.clone())),
FetchState::Done(ref endpoint, _) => self.notify(|| WaitResult::Done(endpoint.clone())),
FetchState::Streaming(_) => self.notify(|| WaitResult::NonAwaitable),
FetchState::NotStarted(_) | FetchState::InProgress(_) | FetchState::Waiting => {},
FetchState::InProgress(_) => {},
FetchState::Empty => {},
}
}
}
pub fn is_deadline_reached(&self) -> bool {
self.deadline < Instant::now()
}
pub fn abort(&self) {
self.abort.store(true, Ordering::SeqCst);
}
enum WaitState {
Waiting(oneshot::Receiver<WaitResult>),
Done(endpoint::Response),
}
pub fn to_async_handler(&self, path: EndpointPath, control: Control) -> Box<server::Handler<HttpStream> + Send> {
let (tx, rx) = mpsc::channel();
self.listeners.lock().push((control, tx));
Box::new(WaitingHandler {
receiver: rx,
state: FetchState::Waiting,
uri: RequestUri::default(),
path: path,
})
}
#[derive(Debug)]
enum WaitResult {
Error(ContentHandler),
Done(local::Dapp),
NonAwaitable,
}
pub struct WaitingHandler {
receiver: mpsc::Receiver<WaitResult>,
state: FetchState,
uri: RequestUri,
path: EndpointPath,
state: WaitState,
}
impl server::Handler<HttpStream> for WaitingHandler {
fn on_request(&mut self, request: server::Request<HttpStream>) -> Next {
self.uri = request.uri().clone();
Next::wait()
}
impl Future for WaitingHandler {
type Item = hyper::Response;
type Error = hyper::Error;
fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
let result = self.receiver.try_recv().ok();
self.state = match result {
Some(WaitResult::Error(handler)) => FetchState::Error(handler),
Some(WaitResult::Done(endpoint)) => {
let mut page_handler = endpoint.to_page_handler(self.path.clone());
page_handler.set_uri(&self.uri);
FetchState::Done(endpoint, page_handler)
},
_ => {
warn!("A result for waiting request was not received.");
FetchState::Waiting
},
};
fn poll(&mut self) -> futures::Poll<Self::Item, Self::Error> {
loop {
let new_state = match self.state {
WaitState::Waiting(ref mut receiver) => {
let result = try_ready!(receiver.poll().map_err(|_| hyper::Error::Timeout));
match self.state {
FetchState::Done(_, ref mut handler) => handler.on_request_readable(decoder),
FetchState::Streaming(ref mut handler) => handler.on_request_readable(decoder),
FetchState::Error(ref mut handler) => handler.on_request_readable(decoder),
_ => Next::write(),
}
}
match result {
WaitResult::Error(handler) => {
return Ok(futures::Async::Ready(handler.into()));
},
WaitResult::NonAwaitable => {
let errors = Errors { embeddable_on: None };
return Ok(futures::Async::Ready(errors.streaming().into()));
},
WaitResult::Done(endpoint) => {
WaitState::Done(endpoint.to_response(&self.path).into())
},
}
},
WaitState::Done(ref mut response) => {
return response.poll()
},
};
fn on_response(&mut self, res: &mut server::Response) -> Next {
match self.state {
FetchState::Done(_, ref mut handler) => handler.on_response(res),
FetchState::Streaming(ref mut handler) => handler.on_response(res),
FetchState::Error(ref mut handler) => handler.on_response(res),
_ => Next::end(),
}
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
match self.state {
FetchState::Done(_, ref mut handler) => handler.on_response_writable(encoder),
FetchState::Streaming(ref mut handler) => handler.on_response_writable(encoder),
FetchState::Error(ref mut handler) => handler.on_response_writable(encoder),
_ => Next::end(),
self.state = new_state;
}
}
}
#[derive(Clone)]
#[derive(Debug, Clone)]
struct Errors {
embeddable_on: Embeddable,
}
impl Errors {
fn streaming(&self) -> ContentHandler {
ContentHandler::error(
StatusCode::BadGateway,
"Streaming Error",
"This content is being streamed in other place.",
None,
self.embeddable_on.clone(),
)
}
fn download_error<E: fmt::Debug>(&self, e: E) -> ContentHandler {
ContentHandler::error(
StatusCode::BadGateway,
@@ -225,67 +210,102 @@ impl Errors {
}
}
pub struct ContentFetcherHandler<H: ContentValidator, F: Fetch> {
enum FetchState {
Error(ContentHandler),
InProgress(BoxFuture<FetchState, ()>),
Streaming(hyper::Response),
Done(local::Dapp, endpoint::Response),
Empty,
}
impl fmt::Debug for FetchState {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
use self::FetchState::*;
write!(fmt, "FetchState(")?;
match *self {
Error(ref error) => write!(fmt, "error: {:?}", error),
InProgress(_) => write!(fmt, "in progress"),
Streaming(ref res) => write!(fmt, "streaming: {:?}", res),
Done(ref endpoint, _) => write!(fmt, "done: {:?}", endpoint),
Empty => write!(fmt, "?"),
}?;
write!(fmt, ")")
}
}
#[derive(Debug)]
pub struct ContentFetcherHandler {
fetch_control: FetchControl,
control: Control,
remote: Remote,
status: FetchState,
fetch: F,
installer: Option<H>,
path: EndpointPath,
errors: Errors,
}
impl<H: ContentValidator, F: Fetch> ContentFetcherHandler<H, F> {
pub fn new(
url: String,
path: EndpointPath,
control: Control,
installer: H,
embeddable_on: Embeddable,
remote: Remote,
fetch: F,
) -> Self {
ContentFetcherHandler {
fetch_control: FetchControl::default(),
control,
remote,
fetch,
status: FetchState::NotStarted(url),
installer: Some(installer),
path,
errors: Errors {
embeddable_on,
},
}
}
impl ContentFetcherHandler {
pub fn fetch_control(&self) -> FetchControl {
self.fetch_control.clone()
}
fn fetch_content(&self, uri: RequestUri, url: &str, installer: H) -> mpsc::Receiver<FetchState> {
let (tx, rx) = mpsc::channel();
let abort = self.fetch_control.abort.clone();
pub fn new<H: ContentValidator, F: Fetch>(
method: &hyper::Method,
url: &str,
path: EndpointPath,
installer: H,
embeddable_on: Embeddable,
fetch: F,
) -> Self {
let fetch_control = FetchControl::default();
let errors = Errors { embeddable_on };
let path = self.path.clone();
let tx2 = tx.clone();
let control = self.control.clone();
let errors = self.errors.clone();
// Validation of method
let status = match *method {
// Start fetching content
Method::Get => {
trace!(target: "dapps", "Fetching content from: {:?}", url);
FetchState::InProgress(Self::fetch_content(
fetch,
url,
fetch_control.abort.clone(),
path,
errors.clone(),
installer,
))
},
// or return error
_ => FetchState::Error(errors.method_not_allowed()),
};
let future = self.fetch.fetch_with_abort(url, abort.into()).then(move |result| {
ContentFetcherHandler {
fetch_control,
status,
errors,
}
}
fn fetch_content<H: ContentValidator, F: Fetch>(
fetch: F,
url: &str,
abort: Arc<AtomicBool>,
path: EndpointPath,
errors: Errors,
installer: H,
) -> BoxFuture<FetchState, ()> {
// Start fetching the content
let fetch2 = fetch.clone();
let future = fetch.fetch_with_abort(url, abort.into()).then(move |result| {
trace!(target: "dapps", "Fetching content finished. Starting validation: {:?}", result);
let new_state = match result {
Ok(match result {
Ok(response) => match installer.validate_and_install(response) {
Ok(ValidatorResponse::Local(endpoint)) => {
trace!(target: "dapps", "Validation OK. Returning response.");
let mut handler = endpoint.to_page_handler(path);
handler.set_uri(&uri);
FetchState::Done(endpoint, handler)
let response = endpoint.to_response(&path);
FetchState::Done(endpoint, response)
},
Ok(ValidatorResponse::Streaming(handler)) => {
Ok(ValidatorResponse::Streaming(stream)) => {
trace!(target: "dapps", "Validation OK. Streaming response.");
FetchState::Streaming(handler)
let (reading, response) = stream.into_response();
fetch2.process_and_forget(reading);
FetchState::Streaming(response)
},
Err(e) => {
trace!(target: "dapps", "Error while validating content: {:?}", e);
@@ -296,100 +316,55 @@ impl<H: ContentValidator, F: Fetch> ContentFetcherHandler<H, F> {
warn!(target: "dapps", "Unable to fetch content: {:?}", e);
FetchState::Error(errors.download_error(e))
},
};
// Content may be resolved when the connection is already dropped.
let _ = tx2.send(new_state);
// Ignoring control errors
let _ = control.ready(Next::read());
Ok(()) as Result<(), ()>
})
});
// make sure to run within fetch thread pool.
let future = self.fetch.process(future);
// spawn to event loop
let control = self.control.clone();
let errors = self.errors.clone();
self.remote.spawn_with_timeout(|| future, Duration::from_secs(FETCH_TIMEOUT), move || {
// Notify about the timeout
let _ = tx.send(FetchState::Error(errors.timeout_error()));
// Ignoring control errors
let _ = control.ready(Next::read());
});
rx
fetch.process(future)
}
}
impl<H: ContentValidator, F: Fetch> server::Handler<HttpStream> for ContentFetcherHandler<H, F> {
fn on_request(&mut self, request: server::Request<HttpStream>) -> Next {
let status = if let FetchState::NotStarted(ref url) = self.status {
let uri = request.uri().clone();
let installer = self.installer.take().expect("Installer always set initialy; installer used only in on_request; on_request invoked only once; qed");
impl Future for ContentFetcherHandler {
type Item = hyper::Response;
type Error = hyper::Error;
Some(match *request.method() {
// Start fetching content
Method::Get => {
trace!(target: "dapps", "Fetching content from: {:?}", url);
let receiver = self.fetch_content(uri, url, installer);
FetchState::InProgress(receiver)
fn poll(&mut self) -> futures::Poll<Self::Item, Self::Error> {
loop {
trace!(target: "dapps", "Polling status: {:?}", self.status);
self.status = match mem::replace(&mut self.status, FetchState::Empty) {
FetchState::Error(error) => {
return Ok(futures::Async::Ready(error.into()));
},
// or return error
_ => FetchState::Error(self.errors.method_not_allowed()),
})
} else { None };
FetchState::Streaming(response) => {
return Ok(futures::Async::Ready(response));
},
any => any,
};
if let Some(status) = status {
let status = match self.status {
// Request may time out
FetchState::InProgress(_) if self.fetch_control.is_deadline_reached() => {
trace!(target: "dapps", "Fetching dapp failed because of timeout.");
FetchState::Error(self.errors.timeout_error())
},
FetchState::InProgress(ref mut receiver) => {
// Check if there is a response
trace!(target: "dapps", "Polling streaming response.");
try_ready!(receiver.poll().map_err(|err| {
warn!(target: "dapps", "Error while fetching response: {:?}", err);
hyper::Error::Timeout
}))
},
FetchState::Done(_, ref mut response) => {
return response.poll()
},
FetchState::Empty => panic!("Future polled twice."),
_ => unreachable!(),
};
trace!(target: "dapps", "New status: {:?}", status);
self.fetch_control.set_status(&status);
self.status = status;
}
Next::read()
}
fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
let (status, next) = match self.status {
// Request may time out
FetchState::InProgress(_) if self.fetch_control.is_deadline_reached() => {
trace!(target: "dapps", "Fetching dapp failed because of timeout.");
(Some(FetchState::Error(self.errors.timeout_error())), Next::write())
},
FetchState::InProgress(ref receiver) => {
// Check if there is an answer
let rec = receiver.try_recv();
match rec {
// just return the new state
Ok(state) => (Some(state), Next::write()),
// wait some more
_ => (None, Next::wait())
}
},
FetchState::Error(ref mut handler) => (None, handler.on_request_readable(decoder)),
_ => (None, Next::write()),
};
if let Some(status) = status {
self.fetch_control.set_status(&status);
self.status = status;
}
next
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
match self.status {
FetchState::Done(_, ref mut handler) => handler.on_response(res),
FetchState::Streaming(ref mut handler) => handler.on_response(res),
FetchState::Error(ref mut handler) => handler.on_response(res),
_ => Next::end(),
}
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
match self.status {
FetchState::Done(_, ref mut handler) => handler.on_response_writable(encoder),
FetchState::Streaming(ref mut handler) => handler.on_response_writable(encoder),
FetchState::Error(ref mut handler) => handler.on_response_writable(encoder),
_ => Next::end(),
}
}
}

View File

@@ -16,80 +16,79 @@
//! Hyper handlers implementations.
mod async;
mod content;
mod echo;
mod fetch;
mod reader;
mod redirect;
mod streaming;
pub use self::async::AsyncHandler;
pub use self::content::ContentHandler;
pub use self::echo::EchoHandler;
pub use self::fetch::{ContentFetcherHandler, ContentValidator, FetchControl, ValidatorResponse};
pub use self::reader::Reader;
pub use self::redirect::Redirection;
pub use self::streaming::StreamingHandler;
use std::iter;
use itertools::Itertools;
use url::Url;
use hyper::{server, header, net, uri};
use hyper::header;
use {apps, address, Embeddable};
/// Adds security-related headers to the Response.
pub fn add_security_headers(headers: &mut header::Headers, embeddable_on: Embeddable) {
headers.set_raw("X-XSS-Protection", vec![b"1; mode=block".to_vec()]);
headers.set_raw("X-Content-Type-Options", vec![b"nosniff".to_vec()]);
headers.set_raw("X-XSS-Protection", "1; mode=block");
headers.set_raw("X-Content-Type-Options", "nosniff");
// Embedding header:
if let None = embeddable_on {
headers.set_raw("X-Frame-Options", vec![b"SAMEORIGIN".to_vec()]);
headers.set_raw("X-Frame-Options", "SAMEORIGIN");
}
// Content Security Policy headers
headers.set_raw("Content-Security-Policy", vec![
headers.set_raw("Content-Security-Policy", String::new()
// Allow connecting to WS servers and HTTP(S) servers.
// We could be more restrictive and allow only RPC server URL.
b"connect-src http: https: ws: wss:;".to_vec(),
+ "connect-src http: https: ws: wss:;"
// Allow framing any content from HTTP(S).
// Again we could only allow embedding from RPC server URL.
// (deprecated)
b"frame-src 'self' http: https:;".to_vec(),
+ "frame-src 'self' http: https:;"
// Allow framing and web workers from HTTP(S).
b"child-src 'self' http: https:;".to_vec(),
+ "child-src 'self' http: https:;"
// We allow data: blob: and HTTP(s) images.
// We could get rid of wildcarding HTTP and only allow RPC server URL.
// (http required for local dapps icons)
b"img-src 'self' 'unsafe-inline' data: blob: http: https:;".to_vec(),
+ "img-src 'self' 'unsafe-inline' data: blob: http: https:;"
// Allow style from data: blob: and HTTPS.
b"style-src 'self' 'unsafe-inline' data: blob: https:;".to_vec(),
+ "style-src 'self' 'unsafe-inline' data: blob: https:;"
// Allow fonts from data: and HTTPS.
b"font-src 'self' data: https:;".to_vec(),
+ "font-src 'self' data: https:;"
// Allow inline scripts and scripts eval (webpack/jsconsole)
{
+ {
let script_src = embeddable_on.as_ref()
.map(|e| e.extra_script_src.iter()
.map(|&(ref host, port)| address(host, port))
.join(" ")
).unwrap_or_default();
format!(
&format!(
"script-src 'self' 'unsafe-inline' 'unsafe-eval' {};",
script_src
).into_bytes()
},
)
}
// Same restrictions as script-src with additional
// blob: that is required for camera access (worker)
b"worker-src 'self' 'unsafe-inline' 'unsafe-eval' https: blob:;".to_vec(),
+ "worker-src 'self' 'unsafe-inline' 'unsafe-eval' https: blob:;"
// Restrict everything else to the same origin.
b"default-src 'self';".to_vec(),
+ "default-src 'self';"
// Run in sandbox mode (although it's not fully safe since we allow same-origin and script)
b"sandbox allow-same-origin allow-forms allow-modals allow-popups allow-presentation allow-scripts;".to_vec(),
+ "sandbox allow-same-origin allow-forms allow-modals allow-popups allow-presentation allow-scripts;"
// Disallow subitting forms from any dapps
b"form-action 'none';".to_vec(),
+ "form-action 'none';"
// Never allow mixed content
b"block-all-mixed-content;".to_vec(),
+ "block-all-mixed-content;"
// Specify if the site can be embedded.
match embeddable_on {
+ &match embeddable_on {
Some(ref embed) => {
let std = address(&embed.host, embed.port);
let proxy = format!("{}.{}", apps::HOME_PAGE, embed.dapps_domain);
@@ -112,43 +111,6 @@ pub fn add_security_headers(headers: &mut header::Headers, embeddable_on: Embedd
format!("frame-ancestors {};", ancestors)
},
None => format!("frame-ancestors 'self';"),
}.into_bytes(),
]);
}
/// Extracts URL part from the Request.
pub fn extract_url(req: &server::Request<net::HttpStream>) -> Option<Url> {
convert_uri_to_url(req.uri(), req.headers().get::<header::Host>())
}
/// Extracts URL given URI and Host header.
pub fn convert_uri_to_url(uri: &uri::RequestUri, host: Option<&header::Host>) -> Option<Url> {
match *uri {
uri::RequestUri::AbsoluteUri(ref url) => {
match Url::from_generic_url(url.clone()) {
Ok(url) => Some(url),
_ => None,
}
},
uri::RequestUri::AbsolutePath { ref path, ref query } => {
let query = match *query {
Some(ref query) => format!("?{}", query),
None => "".into(),
};
// Attempt to prepend the Host header (mandatory in HTTP/1.1)
let url_string = match host {
Some(ref host) => {
format!("http://{}:{}{}{}", host.hostname, host.port.unwrap_or(80), path, query)
},
None => return None,
};
match Url::parse(&url_string) {
Ok(url) => Some(url),
_ => None,
}
},
_ => None,
}
}
);
}

View File

@@ -0,0 +1,73 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! A chunk-producing io::Read wrapper.
use std::io::{self, Read};
use futures::{self, sink, Sink, Future};
use futures::sync::mpsc;
use hyper;
type Sender = mpsc::Sender<Result<hyper::Chunk, hyper::Error>>;
const MAX_CHUNK_SIZE: usize = 32 * 1024;
/// A Reader is essentially a stream of `hyper::Chunks`.
/// The chunks are read from given `io::Read` instance.
///
/// Unfortunately `hyper` doesn't allow you to pass `Stream`
/// directly to the response, so you need to create
/// a `Body::pair()` and send over chunks using `sink::Send`.
/// Also `Chunks` need to take `Vec` by value, so we need
/// to allocate it for each chunk being sent.
pub struct Reader<R: io::Read> {
buffer: [u8; MAX_CHUNK_SIZE],
content: io::BufReader<R>,
sending: sink::Send<Sender>,
}
impl<R: io::Read> Reader<R> {
pub fn pair(content: R, initial: Vec<u8>) -> (Self, hyper::Body) {
let (tx, rx) = hyper::Body::pair();
let reader = Reader {
buffer: [0; MAX_CHUNK_SIZE],
content: io::BufReader::new(content),
sending: tx.send(Ok(initial.into())),
};
(reader, rx)
}
}
impl<R: io::Read> Future for Reader<R> {
type Item = ();
type Error = ();
fn poll(&mut self) -> futures::Poll<Self::Item, Self::Error> {
loop {
let next = try_ready!(self.sending.poll().map_err(|err| {
warn!(target: "dapps", "Unable to send next chunk: {:?}", err);
}));
self.sending = match self.content.read(&mut self.buffer) {
Ok(0) => return Ok(futures::Async::Ready(())),
Ok(read) => next.send(Ok(self.buffer[..read].to_vec().into())),
Err(err) => next.send(Err(hyper::Error::Io(err))),
}
}
}
}

View File

@@ -16,9 +16,7 @@
//! HTTP Redirection hyper handler
use hyper::{header, server, Decoder, Encoder, Next};
use hyper::net::HttpStream;
use hyper::status::StatusCode;
use hyper::{self, header, StatusCode};
#[derive(Clone)]
pub struct Redirection {
@@ -26,36 +24,18 @@ pub struct Redirection {
}
impl Redirection {
pub fn new(url: &str) -> Self {
pub fn new<T: Into<String>>(url: T) -> Self {
Redirection {
to_url: url.to_owned()
to_url: url.into()
}
}
pub fn boxed(url: &str) -> Box<Self> {
Box::new(Self::new(url))
}
}
impl server::Handler<HttpStream> for Redirection {
fn on_request(&mut self, _request: server::Request<HttpStream>) -> Next {
Next::write()
}
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next {
Next::write()
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
impl Into<hyper::Response> for Redirection {
fn into(self) -> hyper::Response {
// Don't use `MovedPermanently` here to prevent browser from caching the redirections.
res.set_status(StatusCode::Found);
res.headers_mut().set(header::Location(self.to_url.to_owned()));
Next::write()
}
fn on_response_writable(&mut self, _encoder: &mut Encoder<HttpStream>) -> Next {
Next::end()
hyper::Response::new()
.with_status(StatusCode::Found)
.with_header(header::Location::new(self.to_url))
}
}

View File

@@ -16,87 +16,43 @@
//! Content Stream Response
use std::io::{self, Read};
use std::io;
use hyper::{self, header, mime, StatusCode};
use hyper::{header, server, Decoder, Encoder, Next};
use hyper::net::HttpStream;
use hyper::mime::Mime;
use hyper::status::StatusCode;
use handlers::add_security_headers;
use handlers::{add_security_headers, Reader};
use Embeddable;
const BUFFER_SIZE: usize = 1024;
pub struct StreamingHandler<R: io::Read> {
buffer: [u8; BUFFER_SIZE],
buffer_leftover: usize,
pub struct StreamingHandler<R> {
initial: Vec<u8>,
content: R,
status: StatusCode,
content: io::BufReader<R>,
mimetype: Mime,
mimetype: mime::Mime,
safe_to_embed_on: Embeddable,
}
impl<R: io::Read> StreamingHandler<R> {
pub fn new(content: R, status: StatusCode, mimetype: Mime, embeddable_on: Embeddable) -> Self {
pub fn new(content: R, status: StatusCode, mimetype: mime::Mime, safe_to_embed_on: Embeddable) -> Self {
StreamingHandler {
buffer: [0; BUFFER_SIZE],
buffer_leftover: 0,
status: status,
content: io::BufReader::new(content),
mimetype: mimetype,
safe_to_embed_on: embeddable_on,
initial: Vec::new(),
content,
status,
mimetype,
safe_to_embed_on,
}
}
pub fn set_initial_content(&mut self, content: &str) {
assert_eq!(self.buffer_leftover, 0);
let bytes = content.as_bytes();
self.buffer_leftover = bytes.len();
self.buffer[0..self.buffer_leftover].copy_from_slice(bytes);
}
}
impl<R: io::Read> server::Handler<HttpStream> for StreamingHandler<R> {
fn on_request(&mut self, _request: server::Request<HttpStream>) -> Next {
Next::write()
}
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next {
Next::write()
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
res.set_status(self.status);
res.headers_mut().set(header::ContentType(self.mimetype.clone()));
add_security_headers(&mut res.headers_mut(), self.safe_to_embed_on.take());
Next::write()
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
fn handle_error(e: io::Error) -> Next {
match e.kind() {
::std::io::ErrorKind::WouldBlock => Next::write(),
_ => Next::end(),
}
}
let write_pos = self.buffer_leftover;
match self.content.read(&mut self.buffer[write_pos..]) {
Err(e) => handle_error(e),
Ok(read) => match encoder.write(&self.buffer[..write_pos + read]) {
Err(e) => handle_error(e),
Ok(0) => Next::end(),
Ok(wrote) => {
self.buffer_leftover = write_pos + read - wrote;
if self.buffer_leftover > 0 {
for i in self.buffer_leftover..write_pos + read {
self.buffer.swap(i, i - self.buffer_leftover);
}
}
Next::write()
},
},
}
self.initial = content.as_bytes().to_vec();
}
pub fn into_response(self) -> (Reader<R>, hyper::Response) {
let (reader, body) = Reader::pair(self.content, self.initial);
let mut res = hyper::Response::new()
.with_status(self.status)
.with_header(header::ContentType(self.mimetype))
.with_body(body);
add_security_headers(&mut res.headers_mut(), self.safe_to_embed_on);
(reader, res)
}
}

View File

@@ -20,7 +20,7 @@
#![cfg_attr(feature="nightly", plugin(clippy))]
extern crate base32;
extern crate futures;
extern crate futures_cpupool;
extern crate itertools;
extern crate linked_hash_map;
extern crate mime_guess;
@@ -29,9 +29,7 @@ extern crate rand;
extern crate rustc_hex;
extern crate serde;
extern crate serde_json;
extern crate time;
extern crate unicase;
extern crate url as url_lib;
extern crate zip;
extern crate jsonrpc_core;
@@ -44,14 +42,13 @@ extern crate fetch;
extern crate node_health;
extern crate parity_dapps_glue as parity_dapps;
extern crate parity_hash_fetch as hash_fetch;
extern crate parity_reactor;
extern crate parity_ui;
extern crate hash;
#[macro_use]
extern crate log;
extern crate futures;
#[macro_use]
extern crate mime;
extern crate log;
#[macro_use]
extern crate serde_derive;
@@ -59,6 +56,8 @@ extern crate serde_derive;
extern crate ethcore_devtools as devtools;
#[cfg(test)]
extern crate env_logger;
#[cfg(test)]
extern crate parity_reactor;
mod endpoint;
mod apps;
@@ -67,7 +66,6 @@ mod router;
mod handlers;
mod api;
mod proxypac;
mod url;
mod web;
#[cfg(test)]
mod tests;
@@ -76,13 +74,12 @@ use std::collections::HashMap;
use std::mem;
use std::path::PathBuf;
use std::sync::Arc;
use parking_lot::RwLock;
use futures_cpupool::CpuPool;
use jsonrpc_http_server::{self as http, hyper, Origin};
use parking_lot::RwLock;
use fetch::Fetch;
use node_health::NodeHealth;
use parity_reactor::Remote;
pub use hash_fetch::urlhint::ContractClient;
pub use node_health::SyncStatus;
@@ -105,6 +102,7 @@ pub struct Endpoints {
endpoints: Arc<RwLock<endpoint::Endpoints>>,
dapps_path: PathBuf,
embeddable: Option<ParentFrameSettings>,
pool: Option<CpuPool>,
}
impl Endpoints {
@@ -117,7 +115,11 @@ impl Endpoints {
/// Check for any changes in the local dapps folder and update.
pub fn refresh_local_dapps(&self) {
let new_local = apps::fs::local_endpoints(&self.dapps_path, self.embeddable.clone());
let pool = match self.pool.as_ref() {
None => return,
Some(pool) => pool,
};
let new_local = apps::fs::local_endpoints(&self.dapps_path, self.embeddable.clone(), pool.clone());
let old_local = mem::replace(&mut *self.local_endpoints.write(), new_local.keys().cloned().collect());
let (_, to_remove): (_, Vec<_>) = old_local
.into_iter()
@@ -151,8 +153,8 @@ impl Middleware {
/// Creates new middleware for UI server.
pub fn ui<F: Fetch>(
pool: CpuPool,
health: NodeHealth,
remote: Remote,
dapps_domain: &str,
registrar: Arc<ContractClient>,
sync_status: Arc<SyncStatus>,
@@ -161,16 +163,16 @@ impl Middleware {
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(
hash_fetch::urlhint::URLHintContract::new(registrar),
sync_status.clone(),
remote.clone(),
fetch.clone(),
pool.clone(),
).embeddable_on(None).allow_dapps(false));
let special = {
let mut special = special_endpoints(
pool.clone(),
health,
content_fetcher.clone(),
remote.clone(),
);
special.insert(router::SpecialEndpoint::Home, Some(apps::ui()));
special.insert(router::SpecialEndpoint::Home, Some(apps::ui(pool.clone())));
special
};
let router = router::Router::new(
@@ -189,8 +191,8 @@ impl Middleware {
/// Creates new Dapps server middleware.
pub fn dapps<F: Fetch>(
pool: CpuPool,
health: NodeHealth,
remote: Remote,
ui_address: Option<(String, u16)>,
extra_embed_on: Vec<(String, u16)>,
extra_script_src: Vec<(String, u16)>,
@@ -206,8 +208,8 @@ impl Middleware {
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(
hash_fetch::urlhint::URLHintContract::new(registrar),
sync_status.clone(),
remote.clone(),
fetch.clone(),
pool.clone(),
).embeddable_on(embeddable.clone()).allow_dapps(true));
let (local_endpoints, endpoints) = apps::all_endpoints(
dapps_path.clone(),
@@ -215,21 +217,22 @@ impl Middleware {
dapps_domain,
embeddable.clone(),
web_proxy_tokens,
remote.clone(),
fetch.clone(),
pool.clone(),
);
let endpoints = Endpoints {
endpoints: Arc::new(RwLock::new(endpoints)),
dapps_path,
local_endpoints: Arc::new(RwLock::new(local_endpoints)),
embeddable: embeddable.clone(),
pool: Some(pool.clone()),
};
let special = {
let mut special = special_endpoints(
pool.clone(),
health,
content_fetcher.clone(),
remote.clone(),
);
special.insert(
router::SpecialEndpoint::Home,
@@ -254,23 +257,22 @@ impl Middleware {
}
impl http::RequestMiddleware for Middleware {
fn on_request(&self, req: &hyper::server::Request<hyper::net::HttpStream>, control: &hyper::Control) -> http::RequestMiddlewareAction {
self.router.on_request(req, control)
fn on_request(&self, req: hyper::Request) -> http::RequestMiddlewareAction {
self.router.on_request(req)
}
}
fn special_endpoints(
pool: CpuPool,
health: NodeHealth,
content_fetcher: Arc<apps::fetcher::Fetcher>,
remote: Remote,
) -> HashMap<router::SpecialEndpoint, Option<Box<endpoint::Endpoint>>> {
let mut special = HashMap::new();
special.insert(router::SpecialEndpoint::Rpc, None);
special.insert(router::SpecialEndpoint::Utils, Some(apps::utils()));
special.insert(router::SpecialEndpoint::Utils, Some(apps::utils(pool)));
special.insert(router::SpecialEndpoint::Api, Some(api::RestApi::new(
content_fetcher,
health,
remote,
)));
special
}

View File

@@ -14,71 +14,62 @@
// 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;
use futures::future;
use futures_cpupool::CpuPool;
use hyper::mime::{self, Mime};
use itertools::Itertools;
use parity_dapps::{WebApp, Info};
use endpoint::{Endpoint, EndpointInfo, EndpointPath, Request, Response};
use page::{handler, PageCache};
use std::sync::Arc;
use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler};
use parity_dapps::{WebApp, File, Info};
use Embeddable;
pub struct PageEndpoint<T : WebApp + 'static> {
pub struct Dapp<T: WebApp + 'static> {
/// futures cpu pool
pool: CpuPool,
/// Content of the files
pub app: Arc<T>,
/// Prefix to strip from the path (when `None` deducted from `app_id`)
pub prefix: Option<String>,
app: T,
/// Safe to be loaded in frame by other origin. (use wisely!)
safe_to_embed_on: Embeddable,
info: EndpointInfo,
fallback_to_index_html: bool,
}
impl<T: WebApp + 'static> PageEndpoint<T> {
/// Creates new `PageEndpoint` for builtin (compile time) Dapp.
pub fn new(app: T) -> Self {
impl<T: WebApp + 'static> Dapp<T> {
/// Creates new `Dapp` for builtin (compile time) Dapp.
pub fn new(pool: CpuPool, app: T) -> Self {
let info = app.info();
PageEndpoint {
app: Arc::new(app),
prefix: None,
Dapp {
pool,
app,
safe_to_embed_on: None,
info: EndpointInfo::from(info),
fallback_to_index_html: false,
}
}
/// Creates a new `PageEndpoint` for builtin (compile time) Dapp.
/// Creates a new `Dapp` for builtin (compile time) Dapp.
/// Instead of returning 404 this endpoint will always server index.html.
pub fn with_fallback_to_index(app: T) -> Self {
pub fn with_fallback_to_index(pool: CpuPool, app: T) -> Self {
let info = app.info();
PageEndpoint {
app: Arc::new(app),
prefix: None,
Dapp {
pool,
app,
safe_to_embed_on: None,
info: EndpointInfo::from(info),
fallback_to_index_html: true,
}
}
/// Create new `PageEndpoint` and specify prefix that should be removed before looking for a file.
/// It's used only for special endpoints (i.e. `/parity-utils/`)
/// So `/parity-utils/inject.js` will be resolved to `/inject.js` is prefix is set.
pub fn with_prefix(app: T, prefix: String) -> Self {
let info = app.info();
PageEndpoint {
app: Arc::new(app),
prefix: Some(prefix),
safe_to_embed_on: None,
info: EndpointInfo::from(info),
fallback_to_index_html: false,
}
}
/// Creates new `PageEndpoint` which can be safely used in iframe
/// Creates new `Dapp` which can be safely used in iframe
/// even from different origin. It might be dangerous (clickjacking).
/// Use wisely!
pub fn new_safe_to_embed(app: T, address: Embeddable) -> Self {
pub fn new_safe_to_embed(pool: CpuPool, app: T, address: Embeddable) -> Self {
let info = app.info();
PageEndpoint {
app: Arc::new(app),
prefix: None,
Dapp {
pool,
app,
safe_to_embed_on: address,
info: EndpointInfo::from(info),
fallback_to_index_html: false,
@@ -86,21 +77,51 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
}
}
impl<T: WebApp> Endpoint for PageEndpoint<T> {
impl<T: WebApp> Endpoint for Dapp<T> {
fn info(&self) -> Option<&EndpointInfo> {
Some(&self.info)
}
fn to_handler(&self, path: EndpointPath) -> Box<Handler> {
Box::new(handler::PageHandler {
app: BuiltinDapp::new(self.app.clone(), self.fallback_to_index_html),
prefix: self.prefix.clone(),
path: path,
file: handler::ServedFile::new(self.safe_to_embed_on.clone()),
fn respond(&self, path: EndpointPath, _req: Request) -> Response {
trace!(target: "dapps", "Builtin file path: {:?}", path);
let file_path = if path.has_no_params() {
"index.html".to_owned()
} else {
path.app_params.into_iter().filter(|x| !x.is_empty()).join("/")
};
trace!(target: "dapps", "Builtin file: {:?}", file_path);
let file = {
let file = |path| self.app.file(path).map(|file| {
let content_type = match file.content_type.parse() {
Ok(mime) => mime,
Err(_) => {
warn!(target: "dapps", "invalid MIME type: {}", file.content_type);
mime::TEXT_HTML
},
};
BuiltinFile {
content_type,
content: io::Cursor::new(file.content),
}
});
let res = file(&file_path);
if self.fallback_to_index_html {
res.or_else(|| file("index.html"))
} else {
res
}
};
let (reader, response) = handler::PageHandler {
file,
cache: PageCache::Disabled,
safe_to_embed_on: self.safe_to_embed_on.clone(),
})
}.into_response();
self.pool.spawn(reader).forget();
Box::new(future::ok(response))
}
}
@@ -116,66 +137,20 @@ impl From<Info> for EndpointInfo {
}
}
struct BuiltinDapp<T: WebApp + 'static> {
app: Arc<T>,
fallback_to_index_html: bool,
struct BuiltinFile {
content_type: Mime,
content: io::Cursor<&'static [u8]>,
}
impl<T: WebApp + 'static> BuiltinDapp<T> {
fn new(app: Arc<T>, fallback_to_index_html: bool) -> Self {
BuiltinDapp {
app: app,
fallback_to_index_html: fallback_to_index_html,
}
}
}
impl<T: WebApp + 'static> handler::Dapp for BuiltinDapp<T> {
type DappFile = BuiltinDappFile<T>;
fn file(&self, path: &str) -> Option<Self::DappFile> {
let file = |path| self.app.file(path).map(|_| {
BuiltinDappFile {
app: self.app.clone(),
path: path.into(),
write_pos: 0,
}
});
let res = file(path);
if self.fallback_to_index_html {
res.or_else(|| file("index.html"))
} else {
res
}
}
}
struct BuiltinDappFile<T: WebApp + 'static> {
app: Arc<T>,
path: String,
write_pos: usize,
}
impl<T: WebApp + 'static> BuiltinDappFile<T> {
fn file(&self) -> &File {
self.app.file(&self.path).expect("Check is done when structure is created.")
}
}
impl<T: WebApp + 'static> handler::DappFile for BuiltinDappFile<T> {
fn content_type(&self) -> &str {
self.file().content_type
}
fn is_drained(&self) -> bool {
self.write_pos == self.file().content.len()
}
fn next_chunk(&mut self) -> &[u8] {
&self.file().content[self.write_pos..]
}
fn bytes_written(&mut self, bytes: usize) {
self.write_pos += bytes;
impl handler::DappFile for BuiltinFile {
type Reader = io::Cursor<&'static [u8]>;
fn content_type(&self) -> &Mime {
&self.content_type
}
fn into_reader(self) -> Self::Reader {
self.content
}
}

View File

@@ -14,61 +14,25 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use time::{self, Duration};
use std::io;
use std::time::{Duration, SystemTime};
use hyper::{self, header, StatusCode};
use hyper::mime::Mime;
use hyper::header;
use hyper::server;
use hyper::uri::RequestUri;
use hyper::net::HttpStream;
use hyper::status::StatusCode;
use hyper::{Decoder, Encoder, Next};
use endpoint::EndpointPath;
use handlers::{ContentHandler, add_security_headers};
use handlers::{Reader, ContentHandler, add_security_headers};
use {Embeddable};
/// Represents a file that can be sent to client.
/// Implementation should keep track of bytes already sent internally.
pub trait DappFile: Send {
pub trait DappFile {
/// A reader type returned by this file.
type Reader: io::Read;
/// Returns a content-type of this file.
fn content_type(&self) -> &str;
fn content_type(&self) -> &Mime;
/// Checks if all bytes from that file were written.
fn is_drained(&self) -> bool;
/// Fetch next chunk to write to the client.
fn next_chunk(&mut self) -> &[u8];
/// How many files have been written to the client.
fn bytes_written(&mut self, bytes: usize);
}
/// Dapp as a (dynamic) set of files.
pub trait Dapp: Send + 'static {
/// File type
type DappFile: DappFile;
/// Returns file under given path.
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> ServedFile<T> {
pub fn new(embeddable_on: Embeddable) -> Self {
ServedFile::Error(ContentHandler::error(
StatusCode::NotFound,
"404 Not Found",
"Requested dapp resource was not found.",
None,
embeddable_on,
))
}
/// Convert this file into io::Read instance.
fn into_reader(self) -> Self::Reader where Self: Sized;
}
/// Defines what cache headers should be appended to returned resources.
@@ -84,194 +48,55 @@ impl Default for PageCache {
}
}
/// A generic type for `PageHandler` allowing to set the URL.
/// Used by dapps fetching to set the URL after the content was downloaded.
pub trait PageHandlerWaiting: server::Handler<HttpStream> + Send {
fn set_uri(&mut self, uri: &RequestUri);
}
/// A handler for a single webapp.
/// Resolves correct paths and serves as a plumbing code between
/// hyper server and dapp.
pub struct PageHandler<T: Dapp> {
/// A Dapp.
pub app: T,
pub struct PageHandler<T: DappFile> {
/// File currently being served
pub file: ServedFile<T>,
/// Optional prefix to strip from path.
pub prefix: Option<String>,
/// Requested path.
pub path: EndpointPath,
pub file: Option<T>,
/// Flag indicating if the file can be safely embeded (put in iframe).
pub safe_to_embed_on: Embeddable,
/// Cache settings for this page.
pub cache: PageCache,
}
impl<T: Dapp> PageHandlerWaiting for PageHandler<T> {
fn set_uri(&mut self, uri: &RequestUri) {
trace!(target: "dapps", "Setting URI: {:?}", uri);
self.file = match *uri {
RequestUri::AbsolutePath { ref path, .. } => {
self.app.file(&self.extract_path(path))
},
RequestUri::AbsoluteUri(ref url) => {
self.app.file(&self.extract_path(url.path()))
},
_ => None,
}.map_or_else(|| ServedFile::new(self.safe_to_embed_on.clone()), |f| ServedFile::File(f));
}
}
impl<T: DappFile> PageHandler<T> {
pub fn into_response(self) -> (Option<Reader<T::Reader>>, hyper::Response) {
let file = match self.file {
None => return (None, ContentHandler::error(
StatusCode::NotFound,
"File not found",
"Requested file has not been found.",
None,
self.safe_to_embed_on,
).into()),
Some(file) => file,
};
impl<T: Dapp> PageHandler<T> {
fn extract_path(&self, path: &str) -> String {
let app_id = &self.path.app_id;
let prefix = "/".to_owned() + self.prefix.as_ref().unwrap_or(app_id);
let prefix_with_slash = prefix.clone() + "/";
let query_pos = path.find('?').unwrap_or_else(|| path.len());
let mut res = hyper::Response::new()
.with_status(StatusCode::Ok);
// Index file support
match path == "/" || path == &prefix || path == &prefix_with_slash {
true => "index.html".to_owned(),
false => if path.starts_with(&prefix_with_slash) {
path[prefix_with_slash.len()..query_pos].to_owned()
} else if path.starts_with("/") {
path[1..query_pos].to_owned()
} else {
path[0..query_pos].to_owned()
// headers
{
let mut headers = res.headers_mut();
if let PageCache::Enabled = self.cache {
let validity_secs = 365u32 * 24 * 3600;
let validity = Duration::from_secs(validity_secs as u64);
headers.set(header::CacheControl(vec![
header::CacheDirective::Public,
header::CacheDirective::MaxAge(validity_secs),
]));
headers.set(header::Expires(header::HttpDate::from(SystemTime::now() + validity)));
}
headers.set(header::ContentType(file.content_type().to_owned()));
add_security_headers(&mut headers, self.safe_to_embed_on);
}
let (reader, body) = Reader::pair(file.into_reader(), Vec::new());
res.set_body(body);
(Some(reader), res)
}
}
impl<T: Dapp> server::Handler<HttpStream> for PageHandler<T> {
fn on_request(&mut self, req: server::Request<HttpStream>) -> Next {
self.set_uri(req.uri());
Next::write()
}
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next {
Next::write()
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
match self.file {
ServedFile::File(ref f) => {
res.set_status(StatusCode::Ok);
if let PageCache::Enabled = self.cache {
let mut headers = res.headers_mut();
let validity = Duration::days(365);
headers.set(header::CacheControl(vec![
header::CacheDirective::Public,
header::CacheDirective::MaxAge(validity.num_seconds() as u32),
]));
headers.set(header::Expires(header::HttpDate(time::now() + validity)));
}
match f.content_type().parse() {
Ok(mime) => res.headers_mut().set(header::ContentType(mime)),
Err(()) => debug!(target: "dapps", "invalid MIME type: {}", f.content_type()),
}
// Security headers:
add_security_headers(&mut res.headers_mut(), self.safe_to_embed_on.take());
Next::write()
},
ServedFile::Error(ref mut handler) => {
handler.on_response(res)
}
}
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
match self.file {
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()
},
Err(e) => match e.kind() {
::std::io::ErrorKind::WouldBlock => Next::write(),
_ => Next::end(),
},
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
pub struct TestWebAppFile;
impl DappFile for TestWebAppFile {
fn content_type(&self) -> &str {
unimplemented!()
}
fn is_drained(&self) -> bool {
unimplemented!()
}
fn next_chunk(&mut self) -> &[u8] {
unimplemented!()
}
fn bytes_written(&mut self, _bytes: usize) {
unimplemented!()
}
}
#[derive(Default)]
pub struct TestWebapp;
impl Dapp for TestWebapp {
type DappFile = TestWebAppFile;
fn file(&self, _path: &str) -> Option<Self::DappFile> {
None
}
}
}
#[test]
fn should_extract_path_with_appid() {
// given
let path1 = "/";
let path2= "/test.css";
let path3 = "/app/myfile.txt";
let path4 = "/app/myfile.txt?query=123";
let page_handler = PageHandler {
app: test::TestWebapp,
prefix: None,
path: EndpointPath {
app_id: "app".to_owned(),
app_params: vec![],
host: "".to_owned(),
port: 8080,
using_dapps_domains: true,
},
file: ServedFile::new(None),
cache: Default::default(),
safe_to_embed_on: None,
};
// when
let res1 = page_handler.extract_path(path1);
let res2 = page_handler.extract_path(path2);
let res3 = page_handler.extract_path(path3);
let res4 = page_handler.extract_path(path4);
// then
assert_eq!(&res1, "index.html");
assert_eq!(&res2, "test.css");
assert_eq!(&res3, "myfile.txt");
assert_eq!(&res4, "myfile.txt");
}

View File

@@ -15,16 +15,18 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use mime_guess;
use std::io::{Seek, Read, SeekFrom};
use std::fs;
use std::{fs, fmt};
use std::path::{Path, PathBuf};
use page::handler::{self, PageCache, PageHandlerWaiting};
use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler};
use mime::Mime;
use futures::{future};
use futures_cpupool::CpuPool;
use page::handler::{self, PageCache};
use endpoint::{Endpoint, EndpointInfo, EndpointPath, Request, Response};
use hyper::mime::Mime;
use Embeddable;
#[derive(Debug, Clone)]
pub struct LocalPageEndpoint {
#[derive(Clone)]
pub struct Dapp {
pool: CpuPool,
path: PathBuf,
mime: Option<Mime>,
info: Option<EndpointInfo>,
@@ -32,23 +34,37 @@ pub struct LocalPageEndpoint {
embeddable_on: Embeddable,
}
impl LocalPageEndpoint {
pub fn new(path: PathBuf, info: EndpointInfo, cache: PageCache, embeddable_on: Embeddable) -> Self {
LocalPageEndpoint {
path: path,
impl fmt::Debug for Dapp {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("Dapp")
.field("path", &self.path)
.field("mime", &self.mime)
.field("info", &self.info)
.field("cache", &self.cache)
.field("embeddable_on", &self.embeddable_on)
.finish()
}
}
impl Dapp {
pub fn new(pool: CpuPool, path: PathBuf, info: EndpointInfo, cache: PageCache, embeddable_on: Embeddable) -> Self {
Dapp {
pool,
path,
mime: None,
info: Some(info),
cache: cache,
embeddable_on: embeddable_on,
cache,
embeddable_on,
}
}
pub fn single_file(path: PathBuf, mime: Mime, cache: PageCache) -> Self {
LocalPageEndpoint {
path: path,
pub fn single_file(pool: CpuPool, path: PathBuf, mime: Mime, cache: PageCache) -> Self {
Dapp {
pool,
path,
mime: Some(mime),
info: None,
cache: cache,
cache,
embeddable_on: None,
}
}
@@ -57,125 +73,75 @@ impl LocalPageEndpoint {
self.path.clone()
}
fn page_handler_with_mime(&self, path: EndpointPath, mime: &Mime) -> handler::PageHandler<LocalSingleFile> {
handler::PageHandler {
app: LocalSingleFile { path: self.path.clone(), mime: format!("{}", mime) },
prefix: None,
path: path,
file: handler::ServedFile::new(None),
safe_to_embed_on: self.embeddable_on.clone(),
cache: self.cache,
}
}
fn page_handler(&self, path: EndpointPath) -> handler::PageHandler<LocalDapp> {
handler::PageHandler {
app: LocalDapp { path: self.path.clone() },
prefix: None,
path: path,
file: handler::ServedFile::new(None),
safe_to_embed_on: self.embeddable_on.clone(),
cache: self.cache,
}
}
pub fn to_page_handler(&self, path: EndpointPath) -> Box<PageHandlerWaiting> {
fn get_file(&self, path: &EndpointPath) -> Option<LocalFile> {
if let Some(ref mime) = self.mime {
Box::new(self.page_handler_with_mime(path, mime))
} else {
Box::new(self.page_handler(path))
return LocalFile::from_path(&self.path, mime.to_owned());
}
let mut file_path = self.path.to_owned();
if path.has_no_params() {
file_path.push("index.html");
} else {
for part in &path.app_params {
file_path.push(part);
}
}
let mime = mime_guess::guess_mime_type(&file_path);
LocalFile::from_path(&file_path, mime)
}
pub fn to_response(&self, path: &EndpointPath) -> Response {
let (reader, response) = handler::PageHandler {
file: self.get_file(path),
cache: self.cache,
safe_to_embed_on: self.embeddable_on.clone(),
}.into_response();
self.pool.spawn(reader).forget();
Box::new(future::ok(response))
}
}
impl Endpoint for LocalPageEndpoint {
impl Endpoint for Dapp {
fn info(&self) -> Option<&EndpointInfo> {
self.info.as_ref()
}
fn to_handler(&self, path: EndpointPath) -> Box<Handler> {
if let Some(ref mime) = self.mime {
Box::new(self.page_handler_with_mime(path, mime))
} else {
Box::new(self.page_handler(path))
}
}
}
struct LocalSingleFile {
path: PathBuf,
mime: String,
}
impl handler::Dapp for LocalSingleFile {
type DappFile = LocalFile;
fn file(&self, _path: &str) -> Option<Self::DappFile> {
LocalFile::from_path(&self.path, Some(&self.mime))
}
}
struct LocalDapp {
path: PathBuf,
}
impl handler::Dapp for LocalDapp {
type DappFile = LocalFile;
fn file(&self, file_path: &str) -> Option<Self::DappFile> {
let mut path = self.path.clone();
for part in file_path.split('/') {
path.push(part);
}
LocalFile::from_path(&path, None)
fn respond(&self, path: EndpointPath, _req: Request) -> Response {
self.to_response(&path)
}
}
struct LocalFile {
content_type: String,
buffer: [u8; 4096],
content_type: Mime,
file: fs::File,
len: u64,
pos: u64,
}
impl LocalFile {
fn from_path<P: AsRef<Path>>(path: P, mime: Option<&str>) -> Option<Self> {
fn from_path<P: AsRef<Path>>(path: P, content_type: Mime) -> Option<Self> {
trace!(target: "dapps", "Local file: {:?}", path.as_ref());
// Check if file exists
fs::File::open(&path).ok().map(|file| {
let content_type = mime.map(|mime| mime.to_owned())
.unwrap_or_else(|| mime_guess::guess_mime_type(path).to_string());
let len = file.metadata().ok().map_or(0, |meta| meta.len());
LocalFile {
content_type: content_type,
buffer: [0; 4096],
file: file,
pos: 0,
len: len,
content_type,
file,
}
})
}
}
impl handler::DappFile for LocalFile {
fn content_type(&self) -> &str {
type Reader = fs::File;
fn content_type(&self) -> &Mime {
&self.content_type
}
fn is_drained(&self) -> bool {
self.pos == self.len
}
fn next_chunk(&mut self) -> &[u8] {
let _ = self.file.seek(SeekFrom::Start(self.pos));
if let Ok(n) = self.file.read(&mut self.buffer) {
&self.buffer[0..n]
} else {
&self.buffer[0..0]
}
}
fn bytes_written(&mut self, bytes: usize) {
self.pos += bytes as u64;
fn into_reader(self) -> Self::Reader {
self.file
}
}

View File

@@ -15,11 +15,9 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
mod builtin;
mod local;
pub mod builtin;
pub mod local;
mod handler;
pub use self::local::LocalPageEndpoint;
pub use self::builtin::PageEndpoint;
pub use self::handler::{PageCache, PageHandlerWaiting};
pub use self::handler::PageCache;

View File

@@ -16,9 +16,11 @@
//! Serving ProxyPac file
use endpoint::{Endpoint, Handler, EndpointPath};
use handlers::ContentHandler;
use apps::HOME_PAGE;
use endpoint::{Endpoint, Request, Response, EndpointPath};
use futures::future;
use handlers::ContentHandler;
use hyper::mime;
use {address, Embeddable};
pub struct ProxyPac {
@@ -33,7 +35,7 @@ impl ProxyPac {
}
impl Endpoint for ProxyPac {
fn to_handler(&self, path: EndpointPath) -> Box<Handler> {
fn respond(&self, path: EndpointPath, _req: Request) -> Response {
let ui = self.embeddable
.as_ref()
.map(|ref parent| address(&parent.host, parent.port))
@@ -57,7 +59,9 @@ function FindProxyForURL(url, host) {{
"#,
HOME_PAGE, self.dapps_domain, path.host, path.port, ui);
Box::new(ContentHandler::ok(content, mime!(Application/Javascript)))
Box::new(future::ok(
ContentHandler::ok(content, mime::TEXT_JAVASCRIPT).into()
))
}
}

View File

@@ -17,18 +17,16 @@
//! Router implementation
//! Dispatch requests to proper application.
use std::cmp;
use std::sync::Arc;
use std::collections::HashMap;
use url::{Url, Host};
use hyper::{self, server, header, Control};
use hyper::net::HttpStream;
use futures::future;
use hyper::{self, header, Uri};
use jsonrpc_http_server as http;
use apps;
use apps::fetcher::Fetcher;
use endpoint::{Endpoint, EndpointPath, Handler};
use endpoint::{self, Endpoint, EndpointPath};
use Endpoints;
use handlers;
use Embeddable;
@@ -43,6 +41,13 @@ pub enum SpecialEndpoint {
None,
}
enum Response {
Some(endpoint::Response),
None(hyper::Request),
}
/// An endpoint router.
/// Dispatches the request to particular Endpoint by requested uri/path.
pub struct Router {
endpoints: Option<Endpoints>,
fetch: Arc<Fetcher>,
@@ -52,11 +57,10 @@ pub struct Router {
}
impl Router {
fn resolve_request(&self, req: &server::Request<HttpStream>, control: Control, refresh_dapps: bool) -> (bool, Option<Box<Handler>>) {
fn resolve_request(&self, req: hyper::Request, refresh_dapps: bool) -> (bool, Response) {
// Choose proper handler depending on path / domain
let url = handlers::extract_url(req);
let endpoint = extract_endpoint(&url, &self.dapps_domain);
let referer = extract_referer_endpoint(req, &self.dapps_domain);
let endpoint = extract_endpoint(req.uri(), req.headers().get(), &self.dapps_domain);
let referer = extract_referer_endpoint(&req, &self.dapps_domain);
let is_utils = endpoint.1 == SpecialEndpoint::Utils;
let is_get_request = *req.method() == hyper::Method::Get;
let is_head_request = *req.method() == hyper::Method::Head;
@@ -64,47 +68,51 @@ impl Router {
.as_ref()
.map_or(false, |endpoints| endpoints.endpoints.read().contains_key(dapp));
trace!(target: "dapps", "Routing request to {:?}. Details: {:?}", url, req);
debug!(target: "dapps", "Handling endpoint request: {:?}", endpoint);
trace!(target: "dapps", "Routing request to {:?}. Details: {:?}", req.uri(), req);
debug!(target: "dapps", "Handling endpoint request: {:?}, referer: {:?}", endpoint, referer);
(is_utils, match (endpoint.0, endpoint.1, referer) {
// Handle invalid web requests that we can recover from
(ref path, SpecialEndpoint::None, Some((ref referer, ref referer_url)))
(ref path, SpecialEndpoint::None, Some(ref referer))
if referer.app_id == apps::WEB_PATH
&& has_dapp(apps::WEB_PATH)
&& !is_web_endpoint(path)
=>
{
trace!(target: "dapps", "Redirecting to correct web request: {:?}", referer_url);
let len = cmp::min(referer_url.path.len(), 2); // /web/<encoded>/
let base = referer_url.path[..len].join("/");
let requested = url.map(|u| u.path.join("/")).unwrap_or_default();
Some(handlers::Redirection::boxed(&format!("/{}/{}", base, requested)))
let token = referer.app_params.get(0).map(String::as_str).unwrap_or("");
let requested = req.uri().path();
let query = req.uri().query().map_or_else(String::new, |query| format!("?{}", query));
let redirect_url = format!("/{}/{}{}{}", apps::WEB_PATH, token, requested, query);
trace!(target: "dapps", "Redirecting to correct web request: {:?}", redirect_url);
Response::Some(Box::new(future::ok(
handlers::Redirection::new(redirect_url).into()
)))
},
// First check special endpoints
(ref path, ref endpoint, _) if self.special.contains_key(endpoint) => {
trace!(target: "dapps", "Resolving to special endpoint.");
self.special.get(endpoint)
.expect("special known to contain key; qed")
.as_ref()
.map(|special| special.to_async_handler(path.clone().unwrap_or_default(), control))
let special = self.special.get(endpoint).expect("special known to contain key; qed");
match *special {
Some(ref special) => Response::Some(special.respond(path.clone().unwrap_or_default(), req)),
None => Response::None(req),
}
},
// Then delegate to dapp
(Some(ref path), _, _) if has_dapp(&path.app_id) => {
trace!(target: "dapps", "Resolving to local/builtin dapp.");
Some(self.endpoints
Response::Some(self.endpoints
.as_ref()
.expect("endpoints known to be set; qed")
.endpoints
.read()
.get(&path.app_id)
.expect("endpoints known to contain key; qed")
.to_async_handler(path.clone(), control))
.respond(path.clone(), req))
},
// Try to resolve and fetch the dapp
(Some(ref path), _, _) if self.fetch.contains(&path.app_id) => {
trace!(target: "dapps", "Resolving to fetchable content.");
Some(self.fetch.to_async_handler(path.clone(), control))
Response::Some(self.fetch.respond(path.clone(), req))
},
// 404 for non-existent content (only if serving endpoints and not homepage)
(Some(ref path), _, _)
@@ -117,45 +125,50 @@ impl Router {
if refresh_dapps {
debug!(target: "dapps", "Refreshing dapps and re-trying.");
self.endpoints.as_ref().map(|endpoints| endpoints.refresh_local_dapps());
return self.resolve_request(req, control, false)
return self.resolve_request(req, false);
} else {
Some(Box::new(handlers::ContentHandler::error(
Response::Some(Box::new(future::ok(handlers::ContentHandler::error(
hyper::StatusCode::NotFound,
"404 Not Found",
"Requested content was not found.",
None,
self.embeddable_on.clone(),
)))
).into())))
}
},
// Any other GET|HEAD requests to home page.
_ if (is_get_request || is_head_request) && self.special.contains_key(&SpecialEndpoint::Home) => {
self.special.get(&SpecialEndpoint::Home)
.expect("special known to contain key; qed")
.as_ref()
.map(|special| special.to_async_handler(Default::default(), control))
let special = self.special.get(&SpecialEndpoint::Home).expect("special known to contain key; qed");
match *special {
Some(ref special) => {
let mut endpoint = EndpointPath::default();
endpoint.app_params = req.uri().path().split('/').map(str::to_owned).collect();
Response::Some(special.respond(endpoint, req))
},
None => Response::None(req),
}
},
// RPC by default
_ => {
trace!(target: "dapps", "Resolving to RPC call.");
None
Response::None(req)
}
})
}
}
impl http::RequestMiddleware for Router {
fn on_request(&self, req: &server::Request<HttpStream>, control: &Control) -> http::RequestMiddlewareAction {
let control = control.clone();
fn on_request(&self, req: hyper::Request) -> http::RequestMiddlewareAction {
let is_origin_set = req.headers().get::<header::Origin>().is_some();
let (is_utils, handler) = self.resolve_request(req, control, self.endpoints.is_some());
match handler {
Some(handler) => http::RequestMiddlewareAction::Respond {
let (is_utils, response) = self.resolve_request(req, self.endpoints.is_some());
match response {
Response::Some(response) => http::RequestMiddlewareAction::Respond {
should_validate_hosts: !is_utils,
handler: handler,
response,
},
None => http::RequestMiddlewareAction::Proceed {
Response::None(request) => http::RequestMiddlewareAction::Proceed {
should_continue_on_invalid_cors: !is_origin_set,
request,
},
}
}
@@ -186,41 +199,44 @@ fn is_web_endpoint(path: &Option<EndpointPath>) -> bool {
}
}
fn extract_referer_endpoint(req: &server::Request<HttpStream>, dapps_domain: &str) -> Option<(EndpointPath, Url)> {
fn extract_referer_endpoint(req: &hyper::Request, dapps_domain: &str) -> Option<EndpointPath> {
let referer = req.headers().get::<header::Referer>();
let url = referer.and_then(|referer| Url::parse(&referer.0).ok());
let url = referer.and_then(|referer| referer.parse().ok());
url.and_then(|url| {
let option = Some(url);
extract_url_referer_endpoint(&option, dapps_domain).or_else(|| {
extract_endpoint(&option, dapps_domain).0.map(|endpoint| (endpoint, option.expect("Just wrapped; qed")))
extract_url_referer_endpoint(&url, dapps_domain).or_else(|| {
extract_endpoint(&url, None, dapps_domain).0
})
})
}
fn extract_url_referer_endpoint(url: &Option<Url>, dapps_domain: &str) -> Option<(EndpointPath, Url)> {
let query = url.as_ref().and_then(|url| url.query.as_ref());
match (url, query) {
(&Some(ref url), Some(ref query)) if query.starts_with(apps::URL_REFERER) => {
let referer_url = format!("http://{}:{}/{}", url.host, url.port, &query[apps::URL_REFERER.len()..]);
fn extract_url_referer_endpoint(url: &Uri, dapps_domain: &str) -> Option<EndpointPath> {
let query = url.query();
match query {
Some(query) if query.starts_with(apps::URL_REFERER) => {
let scheme = url.scheme().unwrap_or("http");
let host = url.host().unwrap_or("unknown");
let port = default_port(url, None);
let referer_url = format!("{}://{}:{}/{}", scheme, host, port, &query[apps::URL_REFERER.len()..]);
debug!(target: "dapps", "Recovering referer from query parameter: {}", referer_url);
let referer_url = Url::parse(&referer_url).ok();
extract_endpoint(&referer_url, dapps_domain).0.map(|endpoint| {
(endpoint, referer_url.expect("Endpoint returned only when url `is_some`").clone())
})
if let Some(referer_url) = referer_url.parse().ok() {
extract_endpoint(&referer_url, None, dapps_domain).0
} else {
None
}
},
_ => None,
}
}
fn extract_endpoint(url: &Option<Url>, dapps_domain: &str) -> (Option<EndpointPath>, SpecialEndpoint) {
fn special_endpoint(url: &Url) -> SpecialEndpoint {
if url.path.len() <= 1 {
fn extract_endpoint(url: &Uri, extra_host: Option<&header::Host>, dapps_domain: &str) -> (Option<EndpointPath>, SpecialEndpoint) {
fn special_endpoint(path: &[&str]) -> SpecialEndpoint {
if path.len() <= 1 {
return SpecialEndpoint::None;
}
match url.path[0].as_ref() {
match path[0].as_ref() {
apps::RPC_PATH => SpecialEndpoint::Rpc,
apps::API_PATH => SpecialEndpoint::Api,
apps::UTILS_PATH => SpecialEndpoint::Utils,
@@ -229,114 +245,162 @@ fn extract_endpoint(url: &Option<Url>, dapps_domain: &str) -> (Option<EndpointPa
}
}
match *url {
Some(ref url) => match url.host {
Host::Domain(ref domain) if domain.ends_with(dapps_domain) => {
let id = &domain[0..(domain.len() - dapps_domain.len())];
let (id, params) = if let Some(split) = id.rfind('.') {
let (params, id) = id.split_at(split);
(id[1..].to_owned(), [params.to_owned()].into_iter().chain(&url.path).cloned().collect())
} else {
(id.to_owned(), url.path.clone())
};
let port = default_port(url, extra_host.as_ref().and_then(|h| h.port()));
let host = url.host().or_else(|| extra_host.as_ref().map(|h| h.hostname()));
let query = url.query().map(str::to_owned);
let mut path_segments = url.path().split('/').skip(1).collect::<Vec<_>>();
trace!(
target: "dapps",
"Extracting endpoint from: {:?} (dapps: {}). Got host {:?}:{} with path {:?}",
url, dapps_domain, host, port, path_segments
);
match host {
Some(host) if host.ends_with(dapps_domain) => {
let id = &host[0..(host.len() - dapps_domain.len())];
let special = special_endpoint(&path_segments);
(Some(EndpointPath {
app_id: id,
app_params: params,
host: domain.clone(),
port: url.port,
using_dapps_domains: true,
}), special_endpoint(url))
},
_ if url.path.len() > 1 => {
let id = url.path[0].to_owned();
(Some(EndpointPath {
app_id: id,
app_params: url.path[1..].to_vec(),
host: format!("{}", url.host),
port: url.port,
using_dapps_domains: false,
}), special_endpoint(url))
},
_ => (None, special_endpoint(url)),
// remove special endpoint id from params
if special != SpecialEndpoint::None {
path_segments.remove(0);
}
let (app_id, app_params) = if let Some(split) = id.rfind('.') {
let (params, id) = id.split_at(split);
path_segments.insert(0, params);
(id[1..].to_owned(), path_segments)
} else {
(id.to_owned(), path_segments)
};
(Some(EndpointPath {
app_id,
app_params: app_params.into_iter().map(Into::into).collect(),
query,
host: host.to_owned(),
port,
using_dapps_domains: true,
}), special)
},
_ => (None, SpecialEndpoint::None)
Some(host) if path_segments.len() > 1 => {
let special = special_endpoint(&path_segments);
let id = path_segments.remove(0);
(Some(EndpointPath {
app_id: id.to_owned(),
app_params: path_segments.into_iter().map(Into::into).collect(),
query,
host: host.to_owned(),
port,
using_dapps_domains: false,
}), special)
},
_ => (None, special_endpoint(&path_segments)),
}
}
#[test]
fn should_extract_endpoint() {
let dapps_domain = ".web3.site";
assert_eq!(extract_endpoint(&None, dapps_domain), (None, SpecialEndpoint::None));
// With path prefix
assert_eq!(
extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["index.html".to_owned()],
host: "localhost".to_owned(),
port: 8080,
using_dapps_domains: false,
}), SpecialEndpoint::None)
);
// With path prefix
assert_eq!(
extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "rpc".to_owned(),
app_params: vec!["".to_owned()],
host: "localhost".to_owned(),
port: 8080,
using_dapps_domains: false,
}), SpecialEndpoint::Rpc)
);
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/parity-utils/inject.js").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "parity-utils".into(), "inject.js".into()],
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Utils)
);
// By Subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://status.web3.site/test.html").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["test.html".to_owned()],
host: "status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::None)
);
// RPC by subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/rpc/").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "rpc".into(), "".into()],
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Rpc)
);
// API by subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/api/").ok(), dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "api".into(), "".into()],
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Api)
);
fn default_port(url: &Uri, extra_port: Option<u16>) -> u16 {
let scheme = url.scheme().unwrap_or("http");
url.port().or(extra_port).unwrap_or_else(|| match scheme {
"http" => 80,
"https" => 443,
_ => 80,
})
}
#[cfg(test)]
mod tests {
use super::{SpecialEndpoint, EndpointPath, extract_endpoint};
#[test]
fn should_extract_endpoint() {
let dapps_domain = ".web3.site";
// With path prefix
assert_eq!(
extract_endpoint(&"http://localhost:8080/status/index.html?q=1".parse().unwrap(), None, dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["index.html".to_owned()],
query: Some("q=1".into()),
host: "localhost".to_owned(),
port: 8080,
using_dapps_domains: false,
}), SpecialEndpoint::None)
);
// With path prefix
assert_eq!(
extract_endpoint(&"http://localhost:8080/rpc/".parse().unwrap(), None, dapps_domain),
(Some(EndpointPath {
app_id: "rpc".to_owned(),
app_params: vec!["".to_owned()],
query: None,
host: "localhost".to_owned(),
port: 8080,
using_dapps_domains: false,
}), SpecialEndpoint::Rpc)
);
assert_eq!(
extract_endpoint(&"http://my.status.web3.site/parity-utils/inject.js".parse().unwrap(), None, dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".into(), "inject.js".into()],
query: None,
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Utils)
);
assert_eq!(
extract_endpoint(&"http://my.status.web3.site/inject.js".parse().unwrap(), None, dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".into(), "inject.js".into()],
query: None,
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::None)
);
// By Subdomain
assert_eq!(
extract_endpoint(&"http://status.web3.site/test.html".parse().unwrap(), None, dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["test.html".to_owned()],
query: None,
host: "status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::None)
);
// RPC by subdomain
assert_eq!(
extract_endpoint(&"http://my.status.web3.site/rpc/".parse().unwrap(), None, dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".into(), "".into()],
query: None,
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Rpc)
);
// API by subdomain
assert_eq!(
extract_endpoint(&"http://my.status.web3.site/api/".parse().unwrap(), None, dapps_domain),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["my".into(), "".into()],
query: None,
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Api)
);
}
}

View File

@@ -49,6 +49,7 @@ fn should_handle_ping() {
"\
POST /api/ping HTTP/1.1\r\n\
Host: home.parity\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
\r\n\
{}

View File

@@ -18,7 +18,7 @@ use devtools::http_client;
use rustc_hex::FromHex;
use tests::helpers::{
serve_with_registrar, serve_with_registrar_and_sync, serve_with_fetch,
serve_with_registrar_and_fetch, serve_with_registrar_and_fetch_and_threads,
serve_with_registrar_and_fetch,
request, assert_security_headers_for_embed,
};
@@ -171,6 +171,8 @@ fn should_return_fetched_dapp_content() {
r#"18
<h1>Hello Gavcoin!</h1>
0
"#
);
@@ -257,7 +259,7 @@ fn should_not_request_content_twice() {
use std::thread;
// given
let (server, fetch, registrar) = serve_with_registrar_and_fetch_and_threads(true);
let (server, fetch, registrar) = serve_with_registrar_and_fetch();
let gavcoin = GAVCOIN_ICON.from_hex().unwrap();
registrar.set_result(
"2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e".parse().unwrap(),

View File

@@ -94,7 +94,7 @@ impl FakeFetch {
}
impl Fetch for FakeFetch {
type Result = futures::BoxFuture<fetch::Response, fetch::Error>;
type Result = Box<Future<Item = fetch::Response, Error = fetch::Error> + Send>;
fn new() -> Result<Self, fetch::Error> where Self: Sized {
Ok(FakeFetch::default())
@@ -117,6 +117,17 @@ impl Fetch for FakeFetch {
tx.send(fetch::Response::from_reader(cursor)).unwrap();
});
rx.map_err(|_| fetch::Error::Aborted).boxed()
Box::new(rx.map_err(|_| fetch::Error::Aborted))
}
fn process_and_forget<F, I, E>(&self, f: F) where
F: Future<Item=I, Error=E> + Send + 'static,
I: Send + 'static,
E: Send + 'static,
{
// Spawn the task in a separate thread.
thread::spawn(|| {
let _ = f.wait();
});
}
}

View File

@@ -22,12 +22,12 @@ use std::sync::Arc;
use env_logger::LogBuilder;
use jsonrpc_core::IoHandler;
use jsonrpc_http_server::{self as http, Host, DomainsValidation};
use parity_reactor::Remote;
use devtools::http_client;
use hash_fetch::urlhint::ContractClient;
use fetch::{Fetch, Client as FetchClient};
use node_health::{NodeHealth, TimeChecker, CpuPool};
use parity_reactor::Remote;
use {Middleware, SyncStatus, WebProxyTokens};
@@ -55,7 +55,7 @@ fn init_logger() {
}
}
pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (Server, Arc<FakeRegistrar>) where
pub fn init_server<F, B>(process: F, io: IoHandler) -> (Server, Arc<FakeRegistrar>) where
F: FnOnce(ServerBuilder) -> ServerBuilder<B>,
B: Fetch,
{
@@ -64,11 +64,9 @@ pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (Server,
let mut dapps_path = env::temp_dir();
dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading");
let server = process(ServerBuilder::new(
&dapps_path, registrar.clone(), remote,
))
.signer_address(Some(("127.0.0.1".into(), SIGNER_PORT)))
.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), io).unwrap();
let mut builder = ServerBuilder::new(&dapps_path, registrar.clone());
builder.signer_address = Some(("127.0.0.1".into(), SIGNER_PORT));
let server = process(builder).start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), io).unwrap();
(
server,
registrar,
@@ -76,34 +74,34 @@ pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (Server,
}
pub fn serve_with_rpc(io: IoHandler) -> Server {
init_server(|builder| builder, io, Remote::new_sync()).0
init_server(|builder| builder, io).0
}
pub fn serve_hosts(hosts: Option<Vec<String>>) -> Server {
let hosts = hosts.map(|hosts| hosts.into_iter().map(Into::into).collect());
init_server(|builder| builder.allowed_hosts(hosts.into()), Default::default(), Remote::new_sync()).0
init_server(|mut builder| {
builder.allowed_hosts = hosts.into();
builder
}, Default::default()).0
}
pub fn serve_with_registrar() -> (Server, Arc<FakeRegistrar>) {
init_server(|builder| builder, Default::default(), Remote::new_sync())
init_server(|builder| builder, Default::default())
}
pub fn serve_with_registrar_and_sync() -> (Server, Arc<FakeRegistrar>) {
init_server(|builder| {
builder.sync_status(Arc::new(FakeSync(true)))
}, Default::default(), Remote::new_sync())
init_server(|mut builder| {
builder.sync_status = Arc::new(FakeSync(true));
builder
}, Default::default())
}
pub fn serve_with_registrar_and_fetch() -> (Server, FakeFetch, Arc<FakeRegistrar>) {
serve_with_registrar_and_fetch_and_threads(false)
}
pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Server, FakeFetch, Arc<FakeRegistrar>) {
let fetch = FakeFetch::default();
let f = fetch.clone();
let (server, reg) = init_server(move |builder| {
builder.fetch(f.clone())
}, Default::default(), if multi_threaded { Remote::new_thread_per_future() } else { Remote::new_sync() });
}, Default::default());
(server, fetch, reg)
}
@@ -111,19 +109,25 @@ pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Serv
pub fn serve_with_fetch(web_token: &'static str, domain: &'static str) -> (Server, FakeFetch) {
let fetch = FakeFetch::default();
let f = fetch.clone();
let (server, _) = init_server(move |builder| {
builder
.fetch(f.clone())
.web_proxy_tokens(Arc::new(move |token| {
if &token == web_token { Some(domain.into()) } else { None }
}))
}, Default::default(), Remote::new_sync());
let (server, _) = init_server(move |mut builder| {
builder.web_proxy_tokens = Arc::new(move |token| {
if &token == web_token { Some(domain.into()) } else { None }
});
builder.fetch(f.clone())
}, Default::default());
(server, fetch)
}
pub fn serve() -> Server {
init_server(|builder| builder, Default::default(), Remote::new_sync()).0
init_server(|builder| builder, Default::default()).0
}
pub fn serve_ui() -> Server {
init_server(|mut builder| {
builder.serve_ui = true;
builder
}, Default::default()).0
}
pub fn request(server: Server, request: &str) -> http_client::Response {
@@ -146,13 +150,13 @@ pub struct ServerBuilder<T: Fetch = FetchClient> {
web_proxy_tokens: Arc<WebProxyTokens>,
signer_address: Option<(String, u16)>,
allowed_hosts: DomainsValidation<Host>,
remote: Remote,
fetch: Option<T>,
serve_ui: bool,
}
impl ServerBuilder {
/// Construct new dapps server
pub fn new<P: AsRef<Path>>(dapps_path: P, registrar: Arc<ContractClient>, remote: Remote) -> Self {
pub fn new<P: AsRef<Path>>(dapps_path: P, registrar: Arc<ContractClient>) -> Self {
ServerBuilder {
dapps_path: dapps_path.as_ref().to_owned(),
registrar: registrar,
@@ -160,8 +164,8 @@ impl ServerBuilder {
web_proxy_tokens: Arc::new(|_| None),
signer_address: None,
allowed_hosts: DomainsValidation::Disabled,
remote: remote,
fetch: None,
serve_ui: false,
}
}
}
@@ -176,37 +180,11 @@ impl<T: Fetch> ServerBuilder<T> {
web_proxy_tokens: self.web_proxy_tokens,
signer_address: self.signer_address,
allowed_hosts: self.allowed_hosts,
remote: self.remote,
fetch: Some(fetch),
serve_ui: self.serve_ui,
}
}
/// Change default sync status.
pub fn sync_status(mut self, status: Arc<SyncStatus>) -> Self {
self.sync_status = status;
self
}
/// Change default web proxy tokens validator.
pub fn web_proxy_tokens(mut self, tokens: Arc<WebProxyTokens>) -> Self {
self.web_proxy_tokens = tokens;
self
}
/// Change default signer port.
pub fn signer_address(mut self, signer_address: Option<(String, u16)>) -> Self {
self.signer_address = signer_address;
self
}
/// Change allowed hosts.
/// `None` - All hosts are allowed
/// `Some(whitelist)` - Allow only whitelisted hosts (+ listen address)
pub fn allowed_hosts(mut self, allowed_hosts: DomainsValidation<Host>) -> Self {
self.allowed_hosts = allowed_hosts;
self
}
/// Asynchronously start server with no authentication,
/// returns result with `Server` handle on success or an error.
pub fn start_unsecured_http(self, addr: &SocketAddr, io: IoHandler) -> Result<Server, http::Error> {
@@ -221,8 +199,9 @@ impl<T: Fetch> ServerBuilder<T> {
self.registrar,
self.sync_status,
self.web_proxy_tokens,
self.remote,
Remote::new_sync(),
fetch,
self.serve_ui,
)
}
@@ -254,26 +233,39 @@ impl Server {
web_proxy_tokens: Arc<WebProxyTokens>,
remote: Remote,
fetch: F,
serve_ui: bool,
) -> Result<Server, http::Error> {
let health = NodeHealth::new(
sync_status.clone(),
TimeChecker::new::<String>(&[], CpuPool::new(1)),
remote.clone(),
);
let middleware = Middleware::dapps(
health,
remote,
signer_address,
vec![],
vec![],
dapps_path,
extra_dapps,
DAPPS_DOMAIN.into(),
registrar,
sync_status,
web_proxy_tokens,
fetch,
);
let pool = ::futures_cpupool::CpuPool::new(1);
let middleware = if serve_ui {
Middleware::ui(
pool,
health,
DAPPS_DOMAIN.into(),
registrar,
sync_status,
fetch,
)
} else {
Middleware::dapps(
pool,
health,
signer_address,
vec![],
vec![],
dapps_path,
extra_dapps,
DAPPS_DOMAIN.into(),
registrar,
sync_status,
web_proxy_tokens,
fetch,
)
};
let mut allowed_hosts: Option<Vec<Host>> = allowed_hosts.into();
allowed_hosts.as_mut().map(|mut hosts| {
@@ -295,9 +287,7 @@ impl Server {
pub fn addr(&self) -> &SocketAddr {
self.server.as_ref()
.expect("server is always Some at the start; it's consumed only when object is dropped; qed")
.addrs()
.first()
.expect("You cannot start the server without binding to at least one address; qed")
.address()
}
}

View File

@@ -17,13 +17,13 @@
use std::str;
use std::sync::Arc;
use std::collections::HashMap;
use rustc_hex::FromHex;
use hash_fetch::urlhint::ContractClient;
use bigint::hash::H256;
use util::Address;
use bytes::{Bytes, ToPretty};
use hash_fetch::urlhint::{ContractClient, BoxFuture};
use parking_lot::Mutex;
use rustc_hex::FromHex;
use util::Address;
const REGISTRAR: &'static str = "8e4e9b13d4b45cb0befc93c3061b1408f67316b2";
const URLHINT: &'static str = "deadbeefcafe0000000000000000000000000000";
@@ -67,7 +67,7 @@ impl ContractClient for FakeRegistrar {
Ok(REGISTRAR.parse().unwrap())
}
fn call(&self, address: Address, data: Bytes) -> ::futures::BoxFuture<Bytes, String> {
fn call(&self, address: Address, data: Bytes) -> BoxFuture<Bytes, String> {
let call = (address.to_hex(), data.to_hex());
self.calls.lock().push(call.clone());
let res = self.responses.lock().get(&call).cloned().expect(&format!("No response for call: {:?}", call));

62
dapps/src/tests/home.rs Normal file
View File

@@ -0,0 +1,62 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use tests::helpers::{serve_ui, request, assert_security_headers};
#[test]
fn should_serve_home_js() {
// given
let server = serve_ui();
// when
let response = request(server,
"\
GET /inject.js HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Content-Type", "application/javascript");
assert_eq!(response.body.contains("function(){"), true, "Expected function in: {}", response.body);
assert_security_headers(&response.headers);
}
#[test]
fn should_serve_home() {
// given
let server = serve_ui();
// when
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Content-Type", "text/html");
assert_security_headers(&response.headers);
}

View File

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

View File

@@ -201,6 +201,7 @@ fn should_serve_utils() {
// then
response.assert_status("HTTP/1.1 200 OK");
assert_eq!(response.body.contains("function(){"), true);
response.assert_header("Content-Type", "application/javascript");
assert_eq!(response.body.contains("function(){"), true, "Expected function in: {}", response.body);
assert_security_headers(&response.headers);
}

View File

@@ -33,7 +33,7 @@ fn should_reject_invalid_host() {
);
// then
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
response.assert_status("HTTP/1.1 403 Forbidden");
assert!(response.body.contains("Provided Host header is not whitelisted."), response.body);
}
@@ -54,7 +54,7 @@ fn should_allow_valid_host() {
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
response.assert_status("HTTP/1.1 200 OK");
}
#[test]
@@ -74,7 +74,7 @@ fn should_serve_dapps_domains() {
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
response.assert_status("HTTP/1.1 200 OK");
}
#[test]
@@ -95,5 +95,5 @@ fn should_allow_parity_utils_even_on_invalid_domain() {
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
response.assert_status("HTTP/1.1 200 OK");
}

View File

@@ -1,150 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! HTTP/HTTPS URL type. Based on URL type from Iron library.
use url_lib::{self};
pub use url_lib::Host;
/// HTTP/HTTPS URL type for Iron.
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Url {
/// Raw url of url
pub raw: url_lib::Url,
/// The host field of the URL, probably a domain.
pub host: Host,
/// The connection port.
pub port: u16,
/// The URL path, the resource to be accessed.
///
/// A *non-empty* vector encoding the parts of the URL path.
/// Empty entries of `""` correspond to trailing slashes.
pub path: Vec<String>,
/// The URL query.
pub query: Option<String>,
/// The URL username field, from the userinfo section of the URL.
///
/// `None` if the `@` character was not part of the input OR
/// if a blank username was provided.
/// Otherwise, a non-empty string.
pub username: Option<String>,
/// The URL password field, from the userinfo section of the URL.
///
/// `None` if the `@` character was not part of the input OR
/// if a blank password was provided.
/// Otherwise, a non-empty string.
pub password: Option<String>,
}
impl Url {
/// Create a URL from a string.
///
/// The input must be a valid URL with a special scheme for this to succeed.
///
/// HTTP and HTTPS are special schemes.
///
/// See: http://url.spec.whatwg.org/#special-scheme
pub fn parse(input: &str) -> Result<Url, String> {
// Parse the string using rust-url, then convert.
match url_lib::Url::parse(input) {
Ok(raw_url) => Url::from_generic_url(raw_url),
Err(e) => Err(format!("{}", e))
}
}
/// Create a `Url` from a `rust-url` `Url`.
pub fn from_generic_url(raw_url: url_lib::Url) -> Result<Url, String> {
// Map empty usernames to None.
let username = match raw_url.username() {
"" => None,
username => Some(username.to_owned())
};
// Map empty passwords to None.
let password = match raw_url.password() {
Some(password) if !password.is_empty() => Some(password.to_owned()),
_ => None,
};
let port = raw_url.port_or_known_default().ok_or_else(|| format!("Unknown port for scheme: `{}`", raw_url.scheme()))?;
let host = raw_url.host().ok_or_else(|| "Valid host, because only data:, mailto: protocols does not have host.".to_owned())?.to_owned();
let path = raw_url.path_segments().ok_or_else(|| "Valid path segments. In HTTP we won't get cannot-be-a-base URLs".to_owned())?
.map(|part| part.to_owned()).collect();
let query = raw_url.query().map(|x| x.to_owned());
Ok(Url {
port: port,
host: host,
path: path,
query: query,
raw: raw_url,
username: username,
password: password,
})
}
}
#[cfg(test)]
mod test {
use super::Url;
#[test]
fn test_default_port() {
assert_eq!(Url::parse("http://example.com/wow").unwrap().port, 80u16);
assert_eq!(Url::parse("https://example.com/wow").unwrap().port, 443u16);
}
#[test]
fn test_explicit_port() {
assert_eq!(Url::parse("http://localhost:3097").unwrap().port, 3097u16);
}
#[test]
fn test_empty_username() {
assert!(Url::parse("http://@example.com").unwrap().username.is_none());
assert!(Url::parse("http://:password@example.com").unwrap().username.is_none());
}
#[test]
fn test_not_empty_username() {
let user = Url::parse("http://john:pass@example.com").unwrap().username;
assert_eq!(user.unwrap(), "john");
let user = Url::parse("http://john:@example.com").unwrap().username;
assert_eq!(user.unwrap(), "john");
}
#[test]
fn test_empty_password() {
assert!(Url::parse("http://michael@example.com").unwrap().password.is_none());
assert!(Url::parse("http://:@example.com").unwrap().password.is_none());
}
#[test]
fn test_not_empty_password() {
let pass = Url::parse("http://michael:pass@example.com").unwrap().password;
assert_eq!(pass.unwrap(), "pass");
let pass = Url::parse("http://:pass@example.com").unwrap().password;
assert_eq!(pass.unwrap(), "pass");
}
}

View File

@@ -17,26 +17,23 @@
//! Serving web-based content (proxying)
use std::sync::Arc;
use fetch::{self, Fetch};
use parity_reactor::Remote;
use base32;
use hyper::{self, server, net, Next, Encoder, Decoder};
use hyper::status::StatusCode;
use fetch::{self, Fetch};
use hyper::{mime, StatusCode};
use apps;
use endpoint::{Endpoint, Handler, EndpointPath};
use endpoint::{Endpoint, EndpointPath, Request, Response};
use futures::future;
use handlers::{
ContentFetcherHandler, ContentHandler, ContentValidator, ValidatorResponse,
StreamingHandler, extract_url,
StreamingHandler,
};
use url::Url;
use {Embeddable, WebProxyTokens};
pub struct Web<F> {
embeddable_on: Embeddable,
web_proxy_tokens: Arc<WebProxyTokens>,
remote: Remote,
fetch: F,
}
@@ -44,92 +41,27 @@ impl<F: Fetch> Web<F> {
pub fn boxed(
embeddable_on: Embeddable,
web_proxy_tokens: Arc<WebProxyTokens>,
remote: Remote,
fetch: F,
) -> Box<Endpoint> {
Box::new(Web {
embeddable_on,
web_proxy_tokens,
remote,
fetch,
})
}
}
impl<F: Fetch> Endpoint for Web<F> {
fn to_async_handler(&self, path: EndpointPath, control: hyper::Control) -> Box<Handler> {
Box::new(WebHandler {
control: control,
state: State::Initial,
path: path,
remote: self.remote.clone(),
fetch: self.fetch.clone(),
web_proxy_tokens: self.web_proxy_tokens.clone(),
embeddable_on: self.embeddable_on.clone(),
})
}
}
struct WebInstaller {
embeddable_on: Embeddable,
referer: String,
}
impl ContentValidator for WebInstaller {
type Error = String;
fn validate_and_install(&self, response: fetch::Response) -> Result<ValidatorResponse, String> {
let status = StatusCode::from_u16(response.status().to_u16());
let is_html = response.is_html();
let mime = response.content_type().unwrap_or(mime!(Text/Html));
let mut handler = StreamingHandler::new(
response,
status,
mime,
self.embeddable_on.clone(),
);
if is_html {
handler.set_initial_content(&format!(
r#"<script src="/{}/inject.js"></script><script>history.replaceState({{}}, "", "/?{}{}/{}")</script>"#,
apps::UTILS_PATH,
apps::URL_REFERER,
apps::WEB_PATH,
&self.referer,
));
}
Ok(ValidatorResponse::Streaming(handler))
}
}
enum State<F: Fetch> {
Initial,
Error(ContentHandler),
Fetching(ContentFetcherHandler<WebInstaller, F>),
}
struct WebHandler<F: Fetch> {
control: hyper::Control,
state: State<F>,
path: EndpointPath,
remote: Remote,
fetch: F,
web_proxy_tokens: Arc<WebProxyTokens>,
embeddable_on: Embeddable,
}
impl<F: Fetch> WebHandler<F> {
fn extract_target_url(&self, url: Option<Url>) -> Result<String, State<F>> {
let token_and_url = self.path.app_params.get(0)
fn extract_target_url(&self, path: &EndpointPath) -> Result<String, ContentHandler> {
let token_and_url = path.app_params.get(0)
.map(|encoded| encoded.replace('.', ""))
.and_then(|encoded| base32::decode(base32::Alphabet::Crockford, &encoded.to_uppercase()))
.and_then(|data| String::from_utf8(data).ok())
.ok_or_else(|| State::Error(ContentHandler::error(
.ok_or_else(|| ContentHandler::error(
StatusCode::BadRequest,
"Invalid parameter",
"Couldn't parse given parameter:",
self.path.app_params.get(0).map(String::as_str),
path.app_params.get(0).map(String::as_str),
self.embeddable_on.clone()
)))?;
))?;
let mut token_it = token_and_url.split('+');
let token = token_it.next();
@@ -139,9 +71,9 @@ impl<F: Fetch> WebHandler<F> {
let domain = match token.and_then(|token| self.web_proxy_tokens.domain(token)) {
Some(domain) => domain,
_ => {
return Err(State::Error(ContentHandler::error(
return Err(ContentHandler::error(
StatusCode::BadRequest, "Invalid Access Token", "Invalid or old web proxy access token supplied.", Some("Try refreshing the page."), self.embeddable_on.clone()
)));
));
}
};
@@ -149,95 +81,86 @@ impl<F: Fetch> WebHandler<F> {
let mut target_url = match target_url {
Some(url) if url.starts_with("http://") || url.starts_with("https://") => url.to_owned(),
_ => {
return Err(State::Error(ContentHandler::error(
return Err(ContentHandler::error(
StatusCode::BadRequest, "Invalid Protocol", "Invalid protocol used.", None, self.embeddable_on.clone()
)));
));
}
};
if !target_url.starts_with(&*domain) {
return Err(State::Error(ContentHandler::error(
return Err(ContentHandler::error(
StatusCode::BadRequest, "Invalid Domain", "Dapp attempted to access invalid domain.", Some(&target_url), self.embeddable_on.clone(),
)));
));
}
if !target_url.ends_with("/") {
target_url = format!("{}/", target_url);
}
// TODO [ToDr] Should just use `path.app_params`
let (path, query) = match (&url, self.path.using_dapps_domains) {
(&Some(ref url), true) => (&url.path[..], &url.query),
(&Some(ref url), false) => (&url.path[2..], &url.query),
_ => {
return Err(State::Error(ContentHandler::error(
StatusCode::BadRequest, "Invalid URL", "Couldn't parse URL", None, self.embeddable_on.clone()
)));
}
};
// Skip the token
let query = path.query.as_ref().map_or_else(String::new, |query| format!("?{}", query));
let path = path.app_params[1..].join("/");
let query = match *query {
Some(ref query) => format!("?{}", query),
None => "".into(),
};
Ok(format!("{}{}{}", target_url, path.join("/"), query))
Ok(format!("{}{}{}", target_url, path, query))
}
}
impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next {
let url = extract_url(&request);
impl<F: Fetch> Endpoint for Web<F> {
fn respond(&self, path: EndpointPath, req: Request) -> Response {
// First extract the URL (reject invalid URLs)
let target_url = match self.extract_target_url(url) {
let target_url = match self.extract_target_url(&path) {
Ok(url) => url,
Err(error) => {
self.state = error;
return Next::write();
Err(response) => {
return Box::new(future::ok(response.into()));
}
};
let mut handler = ContentFetcherHandler::new(
target_url,
self.path.clone(),
self.control.clone(),
let token = path.app_params.get(0)
.expect("`target_url` is valid; app_params is not empty;qed")
.to_owned();
Box::new(ContentFetcherHandler::new(
req.method(),
&target_url,
path,
WebInstaller {
embeddable_on: self.embeddable_on.clone(),
referer: self.path.app_params.get(0)
.expect("`target_url` is valid; app_params is not empty;qed")
.to_owned(),
token,
},
self.embeddable_on.clone(),
self.remote.clone(),
self.fetch.clone(),
);
let res = handler.on_request(request);
self.state = State::Fetching(handler);
res
}
fn on_request_readable(&mut self, decoder: &mut Decoder<net::HttpStream>) -> Next {
match self.state {
State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_request_readable(decoder),
State::Fetching(ref mut handler) => handler.on_request_readable(decoder),
}
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
match self.state {
State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_response(res),
State::Fetching(ref mut handler) => handler.on_response(res),
}
}
fn on_response_writable(&mut self, encoder: &mut Encoder<net::HttpStream>) -> Next {
match self.state {
State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_response_writable(encoder),
State::Fetching(ref mut handler) => handler.on_response_writable(encoder),
}
))
}
}
struct WebInstaller {
embeddable_on: Embeddable,
token: String,
}
impl ContentValidator for WebInstaller {
type Error = String;
fn validate_and_install(self, response: fetch::Response) -> Result<ValidatorResponse, String> {
let status = response.status();
let is_html = response.is_html();
let mime = response.content_type().unwrap_or(mime::TEXT_HTML);
let mut handler = StreamingHandler::new(
response,
status,
mime,
self.embeddable_on,
);
if is_html {
handler.set_initial_content(&format!(
r#"<script src="/{}/inject.js"></script><script>history.replaceState({{}}, "", "/?{}{}/{}")</script>"#,
apps::UTILS_PATH,
apps::URL_REFERER,
apps::WEB_PATH,
&self.token,
));
}
Ok(ValidatorResponse::Streaming(handler))
}
}