Loading local Dapps from FS. (#1214)

* apps list to separate module

* Preparing to support serving files from disk

* Serving files from disk

* Using dapps path from CLI

* Adding more docs
This commit is contained in:
Tomasz Drwięga 2016-06-03 11:51:11 +02:00 committed by Gav Wood
parent 81d8dafd9e
commit bb1b8cc08a
16 changed files with 657 additions and 241 deletions

1
Cargo.lock generated
View File

@ -272,6 +272,7 @@ dependencies = [
"jsonrpc-core 2.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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 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-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)", "parity-dapps-dao 0.3.0 (git+https://github.com/ethcore/parity-dapps-dao-rs.git)",

View File

@ -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-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-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 } 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} clippy = { version = "0.0.69", optional = true}
[build-dependencies] [build-dependencies]

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::sync::Arc; use std::sync::Arc;
use endpoint::{Endpoint, Endpoints, Handler, EndpointPath}; use endpoint::{Endpoint, Endpoints, EndpointInfo, Handler, EndpointPath};
use api::response::as_json; use api::response::as_json;
@ -23,8 +23,8 @@ pub struct RestApi {
endpoints: Arc<Endpoints>, endpoints: Arc<Endpoints>,
} }
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
struct App { pub struct App {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub description: String, pub description: String,
@ -34,6 +34,19 @@ struct App {
pub icon_url: String, 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 { impl RestApi {
pub fn new(endpoints: Arc<Endpoints>) -> Box<Endpoint> { pub fn new(endpoints: Arc<Endpoints>) -> Box<Endpoint> {
Box::new(RestApi { Box::new(RestApi {
@ -43,14 +56,7 @@ impl RestApi {
fn list_apps(&self) -> Vec<App> { fn list_apps(&self) -> Vec<App> {
self.endpoints.iter().filter_map(|(ref k, ref e)| { self.endpoints.iter().filter_map(|(ref k, ref e)| {
e.info().map(|ref info| App { e.info().map(|ref info| App::from_info(k, info))
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(),
})
}).collect() }).collect()
} }
} }

View File

@ -18,3 +18,4 @@ mod api;
mod response; mod response;
pub use self::api::RestApi; pub use self::api::RestApi;
pub use self::api::App;

116
dapps/src/apps/fs.rs Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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<LocalDapp> {
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::<App>(&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
}

View File

@ -19,10 +19,11 @@ use page::PageEndpoint;
use proxypac::ProxyPac; use proxypac::ProxyPac;
use parity_dapps::WebApp; use parity_dapps::WebApp;
mod fs;
extern crate parity_dapps_status; extern crate parity_dapps_status;
extern crate parity_dapps_builtins; extern crate parity_dapps_builtins;
pub const DAPPS_DOMAIN : &'static str = ".parity"; pub const DAPPS_DOMAIN : &'static str = ".parity";
pub const RPC_PATH : &'static str = "rpc"; pub const RPC_PATH : &'static str = "rpc";
pub const API_PATH : &'static str = "api"; pub const API_PATH : &'static str = "api";
@ -36,22 +37,24 @@ pub fn utils() -> Box<Endpoint> {
Box::new(PageEndpoint::with_prefix(parity_dapps_builtins::App::default(), UTILS_PATH.to_owned())) Box::new(PageEndpoint::with_prefix(parity_dapps_builtins::App::default(), UTILS_PATH.to_owned()))
} }
pub fn all_endpoints() -> Endpoints { pub fn all_endpoints(dapps_path: String) -> Endpoints {
let mut pages = Endpoints::new(); // fetch fs dapps at first to avoid overwriting builtins
pages.insert("proxy".into(), ProxyPac::boxed()); let mut pages = fs::local_endpoints(dapps_path);
// Home page needs to be safe embed // Home page needs to be safe embed
// because we use Cross-Origin LocalStorage. // because we use Cross-Origin LocalStorage.
// TODO [ToDr] Account naming should be moved to parity. // TODO [ToDr] Account naming should be moved to parity.
pages.insert("home".into(), Box::new( pages.insert("home".into(), Box::new(
PageEndpoint::new_safe_to_embed(parity_dapps_builtins::App::default()) PageEndpoint::new_safe_to_embed(parity_dapps_builtins::App::default())
)); ));
pages.insert("proxy".into(), ProxyPac::boxed());
insert::<parity_dapps_status::App>(&mut pages, "status"); insert::<parity_dapps_status::App>(&mut pages, "status");
insert::<parity_dapps_status::App>(&mut pages, "parity"); insert::<parity_dapps_status::App>(&mut pages, "parity");
// Optional dapps
wallet_page(&mut pages); wallet_page(&mut pages);
daodapp_page(&mut pages); daodapp_page(&mut pages);
makerotc_page(&mut pages); makerotc_page(&mut pages);
pages pages
} }

View File

@ -30,17 +30,17 @@ pub struct EndpointPath {
pub port: u16, pub port: u16,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub struct EndpointInfo { pub struct EndpointInfo {
pub name: &'static str, pub name: String,
pub description: &'static str, pub description: String,
pub version: &'static str, pub version: String,
pub author: &'static str, pub author: String,
pub icon_url: &'static str, pub icon_url: String,
} }
pub trait Endpoint : Send + Sync { pub trait Endpoint : Send + Sync {
fn info(&self) -> Option<EndpointInfo> { None } fn info(&self) -> Option<&EndpointInfo> { None }
fn to_handler(&self, path: EndpointPath) -> Box<server::Handler<HttpStream>>; fn to_handler(&self, path: EndpointPath) -> Box<server::Handler<HttpStream>>;
} }

View File

@ -53,6 +53,7 @@ extern crate jsonrpc_core;
extern crate jsonrpc_http_server; extern crate jsonrpc_http_server;
extern crate parity_dapps; extern crate parity_dapps;
extern crate ethcore_rpc; extern crate ethcore_rpc;
extern crate mime_guess;
mod endpoint; mod endpoint;
mod apps; mod apps;
@ -73,6 +74,7 @@ static DAPPS_DOMAIN : &'static str = ".parity";
/// Webapps HTTP+RPC server build. /// Webapps HTTP+RPC server build.
pub struct ServerBuilder { pub struct ServerBuilder {
dapps_path: String,
handler: Arc<IoHandler>, handler: Arc<IoHandler>,
} }
@ -84,8 +86,9 @@ impl Extendable for ServerBuilder {
impl ServerBuilder { impl ServerBuilder {
/// Construct new dapps server /// Construct new dapps server
pub fn new() -> Self { pub fn new(dapps_path: String) -> Self {
ServerBuilder { ServerBuilder {
dapps_path: dapps_path,
handler: Arc::new(IoHandler::new()) handler: Arc::new(IoHandler::new())
} }
} }
@ -93,13 +96,13 @@ impl ServerBuilder {
/// Asynchronously start server with no authentication, /// Asynchronously start server with no authentication,
/// returns result with `Server` handle on success or an error. /// returns result with `Server` handle on success or an error.
pub fn start_unsecure_http(&self, addr: &SocketAddr) -> Result<Server, ServerError> { pub fn start_unsecure_http(&self, addr: &SocketAddr) -> Result<Server, ServerError> {
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`, /// Asynchronously start server with `HTTP Basic Authentication`,
/// return result with `Server` handle on success or an error. /// 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, ServerError> { pub fn start_basic_auth_http(&self, addr: &SocketAddr, username: &str, password: &str) -> Result<Server, ServerError> {
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 { impl Server {
fn start_http<A: Authorization + 'static>(addr: &SocketAddr, authorization: A, handler: Arc<IoHandler>) -> Result<Server, ServerError> { fn start_http<A: Authorization + 'static>(addr: &SocketAddr, authorization: A, handler: Arc<IoHandler>, dapps_path: String) -> Result<Server, ServerError> {
let panic_handler = Arc::new(Mutex::new(None)); let panic_handler = Arc::new(Mutex::new(None));
let authorization = Arc::new(authorization); 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 special = Arc::new({
let mut special = HashMap::new(); let mut special = HashMap::new();
special.insert(router::SpecialEndpoint::Rpc, rpc::rpc(handler, panic_handler.clone())); special.insert(router::SpecialEndpoint::Rpc, rpc::rpc(handler, panic_handler.clone()));

154
dapps/src/page/builtin.rs Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
use page::handler;
use std::sync::Arc;
use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler};
use parity_dapps::{WebApp, File, Info};
pub struct PageEndpoint<T : WebApp + 'static> {
/// Content of the files
pub app: Arc<T>,
/// Prefix to strip from the path (when `None` deducted from `app_id`)
pub prefix: Option<String>,
/// Safe to be loaded in frame by other origin. (use wisely!)
safe_to_embed: bool,
info: EndpointInfo,
}
impl<T: WebApp + 'static> PageEndpoint<T> {
/// 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<T: WebApp> Endpoint for PageEndpoint<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()),
prefix: self.prefix.clone(),
path: path,
file: None,
safe_to_embed: self.safe_to_embed,
})
}
}
impl From<Info> 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<T: WebApp + 'static> {
app: Arc<T>,
}
impl<T: WebApp + 'static> BuiltinDapp<T> {
fn new(app: Arc<T>) -> Self {
BuiltinDapp {
app: app,
}
}
}
impl<T: WebApp + 'static> handler::Dapp for BuiltinDapp<T> {
type DappFile = BuiltinDappFile<T>;
fn file(&self, path: &str) -> Option<Self::DappFile> {
self.app.file(path).map(|_| {
BuiltinDappFile {
app: self.app.clone(),
path: path.into(),
write_pos: 0,
}
})
}
}
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;
}
}

207
dapps/src/page/handler.rs Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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<Self::DappFile>;
}
/// 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,
/// File currently being served (or `None` if file does not exist).
pub file: Option<T::DappFile>,
/// Optional prefix to strip from path.
pub prefix: Option<String>,
/// Requested path.
pub path: EndpointPath,
/// Flag indicating if the file can be safely embeded (put in iframe).
pub safe_to_embed: bool,
}
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());
// 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<T: Dapp> server::Handler<HttpStream> for PageHandler<T> {
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<HttpStream>) -> 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<HttpStream>) -> Next {
match self.file {
None => Next::end(),
Some(ref f) if f.is_drained() => Next::end(),
Some(ref mut f) => match encoder.write(f.next_chunk()) {
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(),
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");
}

118
dapps/src/page/local.rs Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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<Handler> {
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<Self::DappFile> {
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;
}
}

View File

@ -14,216 +14,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
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<T : WebApp + 'static> { mod builtin;
/// Content of the files mod local;
pub app: Arc<T>, mod handler;
/// Prefix to strip from the path (when `None` deducted from `app_id`)
pub prefix: Option<String>,
/// Safe to be loaded in frame by other origin. (use wisely!)
safe_to_embed: bool,
}
impl<T: WebApp + 'static> PageEndpoint<T> { pub use self::local::LocalPageEndpoint;
pub fn new(app: T) -> Self { pub use self::builtin::PageEndpoint;
PageEndpoint {
app: Arc::new(app),
prefix: None,
safe_to_embed: false,
}
}
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<T: WebApp> Endpoint for PageEndpoint<T> {
fn info(&self) -> Option<EndpointInfo> {
Some(EndpointInfo::from(self.app.info()))
}
fn to_handler(&self, path: EndpointPath) -> Box<server::Handler<HttpStream>> {
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<Info> 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<T: WebApp + 'static> {
app: Arc<T>,
prefix: Option<String>,
path: EndpointPath,
file: Option<String>,
write_pos: usize,
safe_to_embed: bool,
}
impl<T: WebApp + 'static> 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());
// 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<T: WebApp + 'static> server::Handler<HttpStream> for PageHandler<T> {
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<HttpStream>) -> 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<HttpStream>) -> 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");
}

View File

@ -96,6 +96,8 @@ API and Console Options:
asked for password on startup. asked for password on startup.
--dapps-pass PASSWORD Specify password for Dapps server. Use only in --dapps-pass PASSWORD Specify password for Dapps server. Use only in
conjunction with --dapps-user. 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 --signer Enable Trusted Signer WebSocket endpoint used by
System UIs. System UIs.
@ -239,6 +241,7 @@ pub struct Args {
pub flag_dapps_interface: String, pub flag_dapps_interface: String,
pub flag_dapps_user: Option<String>, pub flag_dapps_user: Option<String>,
pub flag_dapps_pass: Option<String>, pub flag_dapps_pass: Option<String>,
pub flag_dapps_path: String,
pub flag_signer: bool, pub flag_signer: bool,
pub flag_signer_port: u16, pub flag_signer_port: u16,
pub flag_force_sealing: bool, pub flag_force_sealing: bool,

View File

@ -40,6 +40,7 @@ pub struct Configuration {
pub struct Directories { pub struct Directories {
pub keys: String, pub keys: String,
pub db: String, pub db: String,
pub dapps: String,
} }
impl Configuration { impl Configuration {
@ -325,11 +326,14 @@ impl Configuration {
&self.args.flag_keys_path &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 { Directories {
keys: keys_path, keys: keys_path,
db: db_path, db: db_path,
dapps: dapps_path,
} }
} }

View File

@ -32,6 +32,7 @@ pub struct Configuration {
pub port: u16, pub port: u16,
pub user: Option<String>, pub user: Option<String>,
pub pass: Option<String>, pub pass: Option<String>,
pub dapps_path: String,
} }
pub struct Dependencies { pub struct Dependencies {
@ -63,12 +64,13 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Option<WebappSer
(username.to_owned(), password) (username.to_owned(), password)
}); });
Some(setup_dapps_server(deps, &addr, auth)) Some(setup_dapps_server(deps, configuration.dapps_path, &addr, auth))
} }
#[cfg(not(feature = "dapps"))] #[cfg(not(feature = "dapps"))]
pub fn setup_dapps_server( pub fn setup_dapps_server(
_deps: Dependencies, _deps: Dependencies,
_dapps_path: String,
_url: &SocketAddr, _url: &SocketAddr,
_auth: Option<(String, String)>, _auth: Option<(String, String)>,
) -> ! { ) -> ! {
@ -78,12 +80,13 @@ pub fn setup_dapps_server(
#[cfg(feature = "dapps")] #[cfg(feature = "dapps")]
pub fn setup_dapps_server( pub fn setup_dapps_server(
deps: Dependencies, deps: Dependencies,
dapps_path: String,
url: &SocketAddr, url: &SocketAddr,
auth: Option<(String, String)> auth: Option<(String, String)>
) -> WebappServer { ) -> WebappServer {
use ethcore_dapps as dapps; 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 server = rpc_apis::setup_rpc(server, deps.apis.clone(), rpc_apis::ApiSet::UnsafeContext);
let start_result = match auth { let start_result = match auth {
None => { None => {

View File

@ -231,6 +231,7 @@ fn execute_client(conf: Configuration, spec: Spec, client_config: ClientConfig)
port: conf.args.flag_dapps_port, port: conf.args.flag_dapps_port,
user: conf.args.flag_dapps_user.clone(), user: conf.args.flag_dapps_user.clone(),
pass: conf.args.flag_dapps_pass.clone(), pass: conf.args.flag_dapps_pass.clone(),
dapps_path: conf.directories().dapps,
}, dapps::Dependencies { }, dapps::Dependencies {
panic_handler: panic_handler.clone(), panic_handler: panic_handler.clone(),
apis: deps_for_rpc_apis.clone(), apis: deps_for_rpc_apis.clone(),