diff --git a/Cargo.lock b/Cargo.lock
index c81020f6620..788bf1c3772 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -272,6 +272,7 @@ dependencies = [
"jsonrpc-core 2.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-http-server 5.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-dapps 0.3.0 (git+https://github.com/ethcore/parity-dapps-rs.git)",
"parity-dapps-builtins 0.5.0 (git+https://github.com/ethcore/parity-dapps-builtins-rs.git)",
"parity-dapps-dao 0.3.0 (git+https://github.com/ethcore/parity-dapps-dao-rs.git)",
diff --git a/dapps/Cargo.toml b/dapps/Cargo.toml
index 219904b4c4f..f83e0ad1172 100644
--- a/dapps/Cargo.toml
+++ b/dapps/Cargo.toml
@@ -27,6 +27,7 @@ parity-dapps-builtins = { git = "https://github.com/ethcore/parity-dapps-builtin
parity-dapps-wallet = { git = "https://github.com/ethcore/parity-dapps-wallet-rs.git", version = "0.5.0", optional = true }
parity-dapps-dao = { git = "https://github.com/ethcore/parity-dapps-dao-rs.git", version = "0.3.0", optional = true }
parity-dapps-makerotc = { git = "https://github.com/ethcore/parity-dapps-makerotc-rs.git", version = "0.2.0", optional = true }
+mime_guess = { version = "1.6.1" }
clippy = { version = "0.0.69", optional = true}
[build-dependencies]
diff --git a/dapps/src/api/api.rs b/dapps/src/api/api.rs
index c460dcf209a..95b01d44226 100644
--- a/dapps/src/api/api.rs
+++ b/dapps/src/api/api.rs
@@ -15,7 +15,7 @@
// along with Parity. If not, see .
use std::sync::Arc;
-use endpoint::{Endpoint, Endpoints, Handler, EndpointPath};
+use endpoint::{Endpoint, Endpoints, EndpointInfo, Handler, EndpointPath};
use api::response::as_json;
@@ -23,8 +23,8 @@ pub struct RestApi {
endpoints: Arc,
}
-#[derive(Debug, PartialEq, Serialize)]
-struct App {
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub struct App {
pub id: String,
pub name: String,
pub description: String,
@@ -34,6 +34,19 @@ struct App {
pub icon_url: String,
}
+impl App {
+ fn from_info(id: &str, info: &EndpointInfo) -> Self {
+ App {
+ id: id.to_owned(),
+ name: info.name.to_owned(),
+ description: info.description.to_owned(),
+ version: info.version.to_owned(),
+ author: info.author.to_owned(),
+ icon_url: info.icon_url.to_owned(),
+ }
+ }
+}
+
impl RestApi {
pub fn new(endpoints: Arc) -> Box {
Box::new(RestApi {
@@ -43,14 +56,7 @@ impl RestApi {
fn list_apps(&self) -> Vec {
self.endpoints.iter().filter_map(|(ref k, ref e)| {
- e.info().map(|ref info| App {
- id: k.to_owned().clone(),
- name: info.name.to_owned(),
- description: info.description.to_owned(),
- version: info.version.to_owned(),
- author: info.author.to_owned(),
- icon_url: info.icon_url.to_owned(),
- })
+ e.info().map(|ref info| App::from_info(k, info))
}).collect()
}
}
diff --git a/dapps/src/api/mod.rs.in b/dapps/src/api/mod.rs.in
index 0eff6b3978b..a069c06b0a4 100644
--- a/dapps/src/api/mod.rs.in
+++ b/dapps/src/api/mod.rs.in
@@ -18,3 +18,4 @@ mod api;
mod response;
pub use self::api::RestApi;
+pub use self::api::App;
diff --git a/dapps/src/apps/fs.rs b/dapps/src/apps/fs.rs
new file mode 100644
index 00000000000..fa3b0ab4c1d
--- /dev/null
+++ b/dapps/src/apps/fs.rs
@@ -0,0 +1,116 @@
+// Copyright 2015, 2016 Ethcore (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+use serde_json;
+use std::io;
+use std::io::Read;
+use std::fs;
+use std::path::PathBuf;
+use page::LocalPageEndpoint;
+use endpoint::{Endpoints, EndpointInfo};
+use api::App;
+
+struct LocalDapp {
+ id: String,
+ path: PathBuf,
+ info: EndpointInfo,
+}
+
+fn local_dapps(dapps_path: String) -> Vec {
+ let files = fs::read_dir(dapps_path.as_str());
+ if let Err(e) = files {
+ warn!(target: "dapps", "Unable to load local dapps from: {}. Reason: {:?}", dapps_path, e);
+ return vec![];
+ }
+
+ let files = files.expect("Check is done earlier");
+ files.map(|dir| {
+ let entry = try!(dir);
+ let file_type = try!(entry.file_type());
+
+ // skip files
+ if file_type.is_file() {
+ return Err(io::Error::new(io::ErrorKind::NotFound, "Not a file"));
+ }
+
+ // take directory name and path
+ entry.file_name().into_string()
+ .map(|name| (name, entry.path()))
+ .map_err(|e| {
+ info!(target: "dapps", "Unable to load dapp: {:?}. Reason: {:?}", entry.path(), e);
+ io::Error::new(io::ErrorKind::NotFound, "Invalid name")
+ })
+ })
+ .filter_map(|m| {
+ if let Err(ref e) = m {
+ debug!(target: "dapps", "Ignoring local dapp: {:?}", e);
+ }
+ m.ok()
+ })
+ .map(|(name, path)| {
+ // try to get manifest file
+ let info = read_manifest(&name, path.clone());
+ LocalDapp {
+ id: name,
+ path: path,
+ info: info,
+ }
+ })
+ .collect()
+}
+
+fn read_manifest(name: &str, mut path: PathBuf) -> EndpointInfo {
+ path.push("manifest.json");
+
+ fs::File::open(path.clone())
+ .map_err(|e| format!("{:?}", e))
+ .and_then(|mut f| {
+ // Reat file
+ let mut s = String::new();
+ try!(f.read_to_string(&mut s).map_err(|e| format!("{:?}", e)));
+ // Try to deserialize manifest
+ serde_json::from_str::(&s).map_err(|e| format!("{:?}", e))
+ })
+ .map(|app| EndpointInfo {
+ name: app.name,
+ description: app.description,
+ version: app.version,
+ author: app.author,
+ icon_url: app.icon_url,
+ })
+ .unwrap_or_else(|e| {
+ warn!(target: "dapps", "Cannot read manifest file at: {:?}. Error: {:?}", path, e);
+
+ EndpointInfo {
+ name: name.into(),
+ description: name.into(),
+ version: "0.0.0".into(),
+ author: "?".into(),
+ icon_url: "icon.png".into(),
+ }
+ })
+}
+
+pub fn local_endpoints(dapps_path: String) -> Endpoints {
+ let mut pages = Endpoints::new();
+ for dapp in local_dapps(dapps_path) {
+ pages.insert(
+ dapp.id,
+ Box::new(LocalPageEndpoint::new(dapp.path, dapp.info))
+ );
+ }
+ pages
+}
diff --git a/dapps/src/apps.rs b/dapps/src/apps/mod.rs
similarity index 93%
rename from dapps/src/apps.rs
rename to dapps/src/apps/mod.rs
index 130b20fb98e..7f849cf6502 100644
--- a/dapps/src/apps.rs
+++ b/dapps/src/apps/mod.rs
@@ -19,10 +19,11 @@ use page::PageEndpoint;
use proxypac::ProxyPac;
use parity_dapps::WebApp;
+mod fs;
+
extern crate parity_dapps_status;
extern crate parity_dapps_builtins;
-
pub const DAPPS_DOMAIN : &'static str = ".parity";
pub const RPC_PATH : &'static str = "rpc";
pub const API_PATH : &'static str = "api";
@@ -36,22 +37,24 @@ pub fn utils() -> Box {
Box::new(PageEndpoint::with_prefix(parity_dapps_builtins::App::default(), UTILS_PATH.to_owned()))
}
-pub fn all_endpoints() -> Endpoints {
- let mut pages = Endpoints::new();
- pages.insert("proxy".into(), ProxyPac::boxed());
-
+pub fn all_endpoints(dapps_path: String) -> Endpoints {
+ // fetch fs dapps at first to avoid overwriting builtins
+ let mut pages = fs::local_endpoints(dapps_path);
// Home page needs to be safe embed
// because we use Cross-Origin LocalStorage.
// TODO [ToDr] Account naming should be moved to parity.
pages.insert("home".into(), Box::new(
PageEndpoint::new_safe_to_embed(parity_dapps_builtins::App::default())
));
+ pages.insert("proxy".into(), ProxyPac::boxed());
insert::(&mut pages, "status");
insert::(&mut pages, "parity");
+ // Optional dapps
wallet_page(&mut pages);
daodapp_page(&mut pages);
makerotc_page(&mut pages);
+
pages
}
diff --git a/dapps/src/endpoint.rs b/dapps/src/endpoint.rs
index 28ca6ea11e8..592bc7f8f33 100644
--- a/dapps/src/endpoint.rs
+++ b/dapps/src/endpoint.rs
@@ -30,17 +30,17 @@ pub struct EndpointPath {
pub port: u16,
}
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone)]
pub struct EndpointInfo {
- pub name: &'static str,
- pub description: &'static str,
- pub version: &'static str,
- pub author: &'static str,
- pub icon_url: &'static str,
+ pub name: String,
+ pub description: String,
+ pub version: String,
+ pub author: String,
+ pub icon_url: String,
}
pub trait Endpoint : Send + Sync {
- fn info(&self) -> Option { None }
+ fn info(&self) -> Option<&EndpointInfo> { None }
fn to_handler(&self, path: EndpointPath) -> Box>;
}
diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs
index 231e7b0801a..a7fbd5963d9 100644
--- a/dapps/src/lib.rs
+++ b/dapps/src/lib.rs
@@ -53,6 +53,7 @@ extern crate jsonrpc_core;
extern crate jsonrpc_http_server;
extern crate parity_dapps;
extern crate ethcore_rpc;
+extern crate mime_guess;
mod endpoint;
mod apps;
@@ -73,6 +74,7 @@ static DAPPS_DOMAIN : &'static str = ".parity";
/// Webapps HTTP+RPC server build.
pub struct ServerBuilder {
+ dapps_path: String,
handler: Arc,
}
@@ -84,8 +86,9 @@ impl Extendable for ServerBuilder {
impl ServerBuilder {
/// Construct new dapps server
- pub fn new() -> Self {
+ pub fn new(dapps_path: String) -> Self {
ServerBuilder {
+ dapps_path: dapps_path,
handler: Arc::new(IoHandler::new())
}
}
@@ -93,13 +96,13 @@ impl ServerBuilder {
/// Asynchronously start server with no authentication,
/// returns result with `Server` handle on success or an error.
pub fn start_unsecure_http(&self, addr: &SocketAddr) -> Result {
- Server::start_http(addr, NoAuth, self.handler.clone())
+ Server::start_http(addr, NoAuth, self.handler.clone(), self.dapps_path.clone())
}
/// Asynchronously start server with `HTTP Basic Authentication`,
/// return result with `Server` handle on success or an error.
pub fn start_basic_auth_http(&self, addr: &SocketAddr, username: &str, password: &str) -> Result {
- Server::start_http(addr, HttpBasicAuth::single_user(username, password), self.handler.clone())
+ Server::start_http(addr, HttpBasicAuth::single_user(username, password), self.handler.clone(), self.dapps_path.clone())
}
}
@@ -110,10 +113,10 @@ pub struct Server {
}
impl Server {
- fn start_http(addr: &SocketAddr, authorization: A, handler: Arc) -> Result {
+ fn start_http(addr: &SocketAddr, authorization: A, handler: Arc, dapps_path: String) -> Result {
let panic_handler = Arc::new(Mutex::new(None));
let authorization = Arc::new(authorization);
- let endpoints = Arc::new(apps::all_endpoints());
+ let endpoints = Arc::new(apps::all_endpoints(dapps_path));
let special = Arc::new({
let mut special = HashMap::new();
special.insert(router::SpecialEndpoint::Rpc, rpc::rpc(handler, panic_handler.clone()));
diff --git a/dapps/src/page/builtin.rs b/dapps/src/page/builtin.rs
new file mode 100644
index 00000000000..1c7ca32d493
--- /dev/null
+++ b/dapps/src/page/builtin.rs
@@ -0,0 +1,154 @@
+// Copyright 2015, 2016 Ethcore (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+use page::handler;
+use std::sync::Arc;
+use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler};
+use parity_dapps::{WebApp, File, Info};
+
+pub struct PageEndpoint {
+ /// Content of the files
+ pub app: Arc,
+ /// Prefix to strip from the path (when `None` deducted from `app_id`)
+ pub prefix: Option,
+ /// Safe to be loaded in frame by other origin. (use wisely!)
+ safe_to_embed: bool,
+ info: EndpointInfo,
+}
+
+impl PageEndpoint {
+ /// Creates new `PageEndpoint` for builtin (compile time) Dapp.
+ pub fn new(app: T) -> Self {
+ let info = app.info();
+ PageEndpoint {
+ app: Arc::new(app),
+ prefix: None,
+ safe_to_embed: false,
+ info: EndpointInfo::from(info),
+ }
+ }
+
+ /// 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: false,
+ info: EndpointInfo::from(info),
+ }
+ }
+
+ /// Creates new `PageEndpoint` 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) -> Self {
+ let info = app.info();
+ PageEndpoint {
+ app: Arc::new(app),
+ prefix: None,
+ safe_to_embed: true,
+ info: EndpointInfo::from(info),
+ }
+ }
+}
+
+impl Endpoint for PageEndpoint {
+
+ fn info(&self) -> Option<&EndpointInfo> {
+ Some(&self.info)
+ }
+
+ fn to_handler(&self, path: EndpointPath) -> Box {
+ Box::new(handler::PageHandler {
+ app: BuiltinDapp::new(self.app.clone()),
+ prefix: self.prefix.clone(),
+ path: path,
+ file: None,
+ safe_to_embed: self.safe_to_embed,
+ })
+ }
+}
+
+impl From for EndpointInfo {
+ fn from(info: Info) -> Self {
+ EndpointInfo {
+ name: info.name.into(),
+ description: info.description.into(),
+ author: info.author.into(),
+ icon_url: info.icon_url.into(),
+ version: info.version.into(),
+ }
+ }
+}
+
+struct BuiltinDapp {
+ app: Arc,
+}
+
+impl BuiltinDapp {
+ fn new(app: Arc) -> Self {
+ BuiltinDapp {
+ app: app,
+ }
+ }
+}
+
+impl handler::Dapp for BuiltinDapp {
+ type DappFile = BuiltinDappFile;
+
+ fn file(&self, path: &str) -> Option {
+ self.app.file(path).map(|_| {
+ BuiltinDappFile {
+ app: self.app.clone(),
+ path: path.into(),
+ write_pos: 0,
+ }
+ })
+ }
+}
+
+struct BuiltinDappFile {
+ app: Arc,
+ path: String,
+ write_pos: usize,
+}
+
+impl BuiltinDappFile {
+ fn file(&self) -> &File {
+ self.app.file(&self.path).expect("Check is done when structure is created.")
+ }
+}
+
+impl handler::DappFile for BuiltinDappFile {
+ 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;
+ }
+}
diff --git a/dapps/src/page/handler.rs b/dapps/src/page/handler.rs
new file mode 100644
index 00000000000..5167c85c8f9
--- /dev/null
+++ b/dapps/src/page/handler.rs
@@ -0,0 +1,207 @@
+// Copyright 2015, 2016 Ethcore (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+use std::io::Write;
+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;
+
+/// Represents a file that can be sent to client.
+/// Implementation should keep track of bytes already sent internally.
+pub trait DappFile: Send {
+ /// Returns a content-type of this file.
+ fn content_type(&self) -> &str;
+
+ /// 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;
+}
+
+/// A handler for a single webapp.
+/// Resolves correct paths and serves as a plumbing code between
+/// hyper server and dapp.
+pub struct PageHandler {
+ /// A Dapp.
+ pub app: T,
+ /// File currently being served (or `None` if file does not exist).
+ pub file: Option,
+ /// Optional prefix to strip from path.
+ pub prefix: Option,
+ /// Requested path.
+ pub path: EndpointPath,
+ /// Flag indicating if the file can be safely embeded (put in iframe).
+ pub safe_to_embed: bool,
+}
+
+impl PageHandler {
+ 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());
+
+ // 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()
+ }
+ }
+ }
+}
+
+impl server::Handler for PageHandler {
+ fn on_request(&mut self, req: server::Request) -> Next {
+ self.file = match *req.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,
+ };
+ Next::write()
+ }
+
+ fn on_request_readable(&mut self, _decoder: &mut Decoder) -> Next {
+ Next::write()
+ }
+
+ fn on_response(&mut self, res: &mut server::Response) -> Next {
+ if let Some(ref f) = self.file {
+ res.set_status(StatusCode::Ok);
+ res.headers_mut().set(header::ContentType(f.content_type().parse().unwrap()));
+ if !self.safe_to_embed {
+ res.headers_mut().set_raw("X-Frame-Options", vec![b"SAMEORIGIN".to_vec()]);
+ }
+ Next::write()
+ } else {
+ res.set_status(StatusCode::NotFound);
+ Next::write()
+ }
+ }
+
+ fn on_response_writable(&mut self, encoder: &mut Encoder) -> Next {
+ match self.file {
+ None => Next::end(),
+ Some(ref f) if f.is_drained() => Next::end(),
+ Some(ref mut f) => match encoder.write(f.next_chunk()) {
+ 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 {
+ 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(),
+ host: "".to_owned(),
+ port: 8080
+ },
+ file: None,
+ safe_to_embed: true,
+ };
+
+ // 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");
+}
diff --git a/dapps/src/page/local.rs b/dapps/src/page/local.rs
new file mode 100644
index 00000000000..52e32bf5e38
--- /dev/null
+++ b/dapps/src/page/local.rs
@@ -0,0 +1,118 @@
+// Copyright 2015, 2016 Ethcore (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+use mime_guess;
+use std::io::{Seek, Read, SeekFrom};
+use std::fs;
+use std::path::PathBuf;
+use page::handler;
+use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler};
+
+pub struct LocalPageEndpoint {
+ path: PathBuf,
+ info: EndpointInfo,
+}
+
+impl LocalPageEndpoint {
+ pub fn new(path: PathBuf, info: EndpointInfo) -> Self {
+ LocalPageEndpoint {
+ path: path,
+ info: info,
+ }
+ }
+}
+
+impl Endpoint for LocalPageEndpoint {
+ fn info(&self) -> Option<&EndpointInfo> {
+ Some(&self.info)
+ }
+
+ fn to_handler(&self, path: EndpointPath) -> Box {
+ Box::new(handler::PageHandler {
+ app: LocalDapp::new(self.path.clone()),
+ prefix: None,
+ path: path,
+ file: None,
+ safe_to_embed: false,
+ })
+ }
+}
+
+struct LocalDapp {
+ path: PathBuf,
+}
+
+impl LocalDapp {
+ fn new(path: PathBuf) -> Self {
+ LocalDapp {
+ path: path
+ }
+ }
+}
+
+impl handler::Dapp for LocalDapp {
+ type DappFile = LocalFile;
+
+ fn file(&self, file_path: &str) -> Option {
+ let mut path = self.path.clone();
+ for part in file_path.split('/') {
+ path.push(part);
+ }
+ // Check if file exists
+ fs::File::open(path.clone()).ok().map(|file| {
+ let content_type = mime_guess::guess_mime_type(path);
+ let len = file.metadata().ok().map_or(0, |meta| meta.len());
+ LocalFile {
+ content_type: content_type.to_string(),
+ buffer: [0; 4096],
+ file: file,
+ pos: 0,
+ len: len,
+ }
+ })
+ }
+}
+
+struct LocalFile {
+ content_type: String,
+ buffer: [u8; 4096],
+ file: fs::File,
+ len: u64,
+ pos: u64,
+}
+
+impl handler::DappFile for LocalFile {
+ fn content_type(&self) -> &str {
+ &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;
+ }
+}
diff --git a/dapps/src/page/mod.rs b/dapps/src/page/mod.rs
index 8199883106a..349c979c7bc 100644
--- a/dapps/src/page/mod.rs
+++ b/dapps/src/page/mod.rs
@@ -14,216 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-use std::sync::Arc;
-use std::io::Write;
-use hyper::uri::RequestUri;
-use hyper::server;
-use hyper::header;
-use hyper::status::StatusCode;
-use hyper::net::HttpStream;
-use hyper::{Decoder, Encoder, Next};
-use endpoint::{Endpoint, EndpointInfo, EndpointPath};
-use parity_dapps::{WebApp, Info};
-pub struct PageEndpoint {
- /// Content of the files
- pub app: Arc,
- /// Prefix to strip from the path (when `None` deducted from `app_id`)
- pub prefix: Option,
- /// Safe to be loaded in frame by other origin. (use wisely!)
- safe_to_embed: bool,
-}
+mod builtin;
+mod local;
+mod handler;
-impl PageEndpoint {
- pub fn new(app: T) -> Self {
- PageEndpoint {
- app: Arc::new(app),
- prefix: None,
- safe_to_embed: false,
- }
- }
+pub use self::local::LocalPageEndpoint;
+pub use self::builtin::PageEndpoint;
- pub fn with_prefix(app: T, prefix: String) -> Self {
- PageEndpoint {
- app: Arc::new(app),
- prefix: Some(prefix),
- safe_to_embed: false,
- }
- }
-
- /// Creates new `PageEndpoint` 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) -> Self {
- PageEndpoint {
- app: Arc::new(app),
- prefix: None,
- safe_to_embed: true,
- }
- }
-}
-
-impl Endpoint for PageEndpoint {
-
- fn info(&self) -> Option {
- Some(EndpointInfo::from(self.app.info()))
- }
-
- fn to_handler(&self, path: EndpointPath) -> Box> {
- Box::new(PageHandler {
- app: self.app.clone(),
- prefix: self.prefix.clone(),
- path: path,
- file: None,
- write_pos: 0,
- safe_to_embed: self.safe_to_embed,
- })
- }
-}
-
-impl From for EndpointInfo {
- fn from(info: Info) -> Self {
- EndpointInfo {
- name: info.name,
- description: info.description,
- author: info.author,
- icon_url: info.icon_url,
- version: info.version,
- }
- }
-}
-
-struct PageHandler {
- app: Arc,
- prefix: Option,
- path: EndpointPath,
- file: Option,
- write_pos: usize,
- safe_to_embed: bool,
-}
-
-impl PageHandler {
- 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());
-
- // 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()
- }
- }
- }
-}
-
-impl server::Handler for PageHandler {
- fn on_request(&mut self, req: server::Request) -> Next {
- self.file = match *req.uri() {
- RequestUri::AbsolutePath(ref path) => {
- Some(self.extract_path(path))
- },
- RequestUri::AbsoluteUri(ref url) => {
- Some(self.extract_path(url.path()))
- },
- _ => None,
- };
- Next::write()
- }
-
- fn on_request_readable(&mut self, _decoder: &mut Decoder) -> Next {
- Next::write()
- }
-
- fn on_response(&mut self, res: &mut server::Response) -> Next {
- if let Some(f) = self.file.as_ref().and_then(|f| self.app.file(f)) {
- res.set_status(StatusCode::Ok);
- res.headers_mut().set(header::ContentType(f.content_type.parse().unwrap()));
- if !self.safe_to_embed {
- res.headers_mut().set_raw("X-Frame-Options", vec![b"SAMEORIGIN".to_vec()]);
- }
- Next::write()
- } else {
- res.set_status(StatusCode::NotFound);
- Next::write()
- }
- }
-
- fn on_response_writable(&mut self, encoder: &mut Encoder) -> Next {
- let (wrote, res) = {
- let file = self.file.as_ref().and_then(|f| self.app.file(f));
- match file {
- None => (None, Next::end()),
- Some(f) if self.write_pos == f.content.len() => (None, Next::end()),
- Some(f) => match encoder.write(&f.content[self.write_pos..]) {
- Ok(bytes) => (Some(bytes), Next::write()),
- Err(e) => match e.kind() {
- ::std::io::ErrorKind::WouldBlock => (None, Next::write()),
- _ => (None, Next::end())
- },
- }
- }
- };
- if let Some(bytes) = wrote {
- self.write_pos += bytes;
- }
- res
- }
-}
-
-
-#[cfg(test)]
-use parity_dapps::File;
-
-#[cfg(test)]
-#[derive(Default)]
-struct TestWebapp;
-
-#[cfg(test)]
-impl WebApp for TestWebapp {
- fn file(&self, _path: &str) -> Option<&File> {
- None
- }
- fn info(&self) -> Info {
- unimplemented!()
- }
-}
-
-#[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: Arc::new(TestWebapp),
- prefix: None,
- path: EndpointPath {
- app_id: "app".to_owned(),
- host: "".to_owned(),
- port: 8080
- },
- file: None,
- write_pos: 0,
- safe_to_embed: true,
- };
-
- // 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");
-}
diff --git a/parity/cli.rs b/parity/cli.rs
index 95b77a00d81..bb67ee5c6ef 100644
--- a/parity/cli.rs
+++ b/parity/cli.rs
@@ -96,6 +96,8 @@ API and Console Options:
asked for password on startup.
--dapps-pass PASSWORD Specify password for Dapps server. Use only in
conjunction with --dapps-user.
+ --dapps-path PATH Specify directory where dapps should be installed.
+ [default: $HOME/.parity/dapps]
--signer Enable Trusted Signer WebSocket endpoint used by
System UIs.
@@ -239,6 +241,7 @@ pub struct Args {
pub flag_dapps_interface: String,
pub flag_dapps_user: Option,
pub flag_dapps_pass: Option,
+ pub flag_dapps_path: String,
pub flag_signer: bool,
pub flag_signer_port: u16,
pub flag_force_sealing: bool,
diff --git a/parity/configuration.rs b/parity/configuration.rs
index 61a571ae1ea..6185d2cf759 100644
--- a/parity/configuration.rs
+++ b/parity/configuration.rs
@@ -40,6 +40,7 @@ pub struct Configuration {
pub struct Directories {
pub keys: String,
pub db: String,
+ pub dapps: String,
}
impl Configuration {
@@ -325,11 +326,14 @@ impl Configuration {
&self.args.flag_keys_path
}
);
- ::std::fs::create_dir_all(&db_path).unwrap_or_else(|e| die_with_io_error("main", e));
+ ::std::fs::create_dir_all(&keys_path).unwrap_or_else(|e| die_with_io_error("main", e));
+ let dapps_path = Configuration::replace_home(&self.args.flag_dapps_path);
+ ::std::fs::create_dir_all(&dapps_path).unwrap_or_else(|e| die_with_io_error("main", e));
Directories {
keys: keys_path,
db: db_path,
+ dapps: dapps_path,
}
}
diff --git a/parity/dapps.rs b/parity/dapps.rs
index 91742d9e36e..59a9ee5521f 100644
--- a/parity/dapps.rs
+++ b/parity/dapps.rs
@@ -32,6 +32,7 @@ pub struct Configuration {
pub port: u16,
pub user: Option,
pub pass: Option,
+ pub dapps_path: String,
}
pub struct Dependencies {
@@ -63,12 +64,13 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Option,
) -> ! {
@@ -78,12 +80,13 @@ pub fn setup_dapps_server(
#[cfg(feature = "dapps")]
pub fn setup_dapps_server(
deps: Dependencies,
+ dapps_path: String,
url: &SocketAddr,
auth: Option<(String, String)>
) -> WebappServer {
use ethcore_dapps as dapps;
- let server = dapps::ServerBuilder::new();
+ let server = dapps::ServerBuilder::new(dapps_path);
let server = rpc_apis::setup_rpc(server, deps.apis.clone(), rpc_apis::ApiSet::UnsafeContext);
let start_result = match auth {
None => {
diff --git a/parity/main.rs b/parity/main.rs
index cae6aa61445..8795782d81b 100644
--- a/parity/main.rs
+++ b/parity/main.rs
@@ -232,6 +232,7 @@ fn execute_client(conf: Configuration, spec: Spec, client_config: ClientConfig)
port: conf.args.flag_dapps_port,
user: conf.args.flag_dapps_user.clone(),
pass: conf.args.flag_dapps_pass.clone(),
+ dapps_path: conf.directories().dapps,
}, dapps::Dependencies {
panic_handler: panic_handler.clone(),
apis: deps_for_rpc_apis.clone(),