diff --git a/dapps/src/apps/fs.rs b/dapps/src/apps/fs.rs index 12bad2e3d38..9984a112a09 100644 --- a/dapps/src/apps/fs.rs +++ b/dapps/src/apps/fs.rs @@ -17,7 +17,7 @@ use std::io; use std::io::Read; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use page::{LocalPageEndpoint, PageCache}; use endpoint::{Endpoints, EndpointInfo}; use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest}; @@ -28,49 +28,8 @@ struct LocalDapp { 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 = dir?; - let file_type = 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() -} - +/// Tries to find and read manifest file in given `path` to extract `EndpointInfo` +/// If manifest is not found sensible default `EndpointInfo` is returned based on given `name`. fn read_manifest(name: &str, mut path: PathBuf) -> EndpointInfo { path.push(MANIFEST_FILENAME); @@ -97,9 +56,38 @@ fn read_manifest(name: &str, mut path: PathBuf) -> EndpointInfo { }) } -pub fn local_endpoints(dapps_path: String, signer_address: Option<(String, u16)>) -> Endpoints { +/// 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>(path: P, signer_address: Option<(String, u16)>) -> Option<(String, Box)> { + 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, signer_address.clone()) + )) + }) + }) +} + + +fn local_dapp(name: String, path: PathBuf) -> LocalDapp { + // try to get manifest file + let info = read_manifest(&name, path.clone()); + LocalDapp { + id: name, + path: path, + info: info, + } +} + +/// Returns endpoints for Local Dapps found for given filesystem path. +/// Scans the directory and collects `LocalPageEndpoints`. +pub fn local_endpoints>(dapps_path: P, signer_address: Option<(String, u16)>) -> Endpoints { let mut pages = Endpoints::new(); - for dapp in local_dapps(dapps_path) { + for dapp in local_dapps(dapps_path.as_ref()) { pages.insert( dapp.id, Box::new(LocalPageEndpoint::new(dapp.path, dapp.info, PageCache::Disabled, signer_address.clone())) @@ -107,3 +95,39 @@ pub fn local_endpoints(dapps_path: String, signer_address: Option<(String, u16)> } pages } + + +fn local_dapps(dapps_path: &Path) -> Vec { + let files = fs::read_dir(dapps_path); + if let Err(e) = files { + warn!(target: "dapps", "Unable to load local dapps from: {}. Reason: {:?}", dapps_path.display(), e); + return vec![]; + } + + let files = files.expect("Check is done earlier"); + files.map(|dir| { + let entry = dir?; + let file_type = 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)| local_dapp(name, path)) + .collect() +} diff --git a/dapps/src/apps/mod.rs b/dapps/src/apps/mod.rs index 51f8f5572c1..f32cf904223 100644 --- a/dapps/src/apps/mod.rs +++ b/dapps/src/apps/mod.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +use std::path::PathBuf; use std::sync::Arc; use endpoint::{Endpoints, Endpoint}; use page::PageEndpoint; @@ -43,7 +44,8 @@ pub fn utils() -> Box { } pub fn all_endpoints( - dapps_path: String, + dapps_path: PathBuf, + extra_dapps: Vec, signer_address: Option<(String, u16)>, web_proxy_tokens: Arc, remote: Remote, @@ -51,6 +53,13 @@ pub fn all_endpoints( ) -> Endpoints { // fetch fs dapps at first to avoid overwriting builtins let mut pages = fs::local_endpoints(dapps_path, signer_address.clone()); + for path in extra_dapps { + if let Some((id, endpoint)) = fs::local_endpoint(path.clone(), signer_address.clone()) { + pages.insert(id, endpoint); + } else { + warn!(target: "dapps", "Ignoring invalid dapp at {}", path.display()); + } + } // NOTE [ToDr] Dapps will be currently embeded on 8180 insert::(&mut pages, "ui", Embeddable::Yes(signer_address.clone())); diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index effa29fcc80..64ee0c3410a 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -88,6 +88,7 @@ mod web; #[cfg(test)] mod tests; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::net::SocketAddr; use std::collections::HashMap; @@ -123,7 +124,8 @@ impl WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync { /// Webapps HTTP+RPC server build. pub struct ServerBuilder { - dapps_path: String, + dapps_path: PathBuf, + extra_dapps: Vec, handler: Arc, registrar: Arc, sync_status: Arc, @@ -141,9 +143,10 @@ impl Extendable for ServerBuilder { impl ServerBuilder { /// Construct new dapps server - pub fn new(dapps_path: String, registrar: Arc, remote: Remote) -> Self { + pub fn new>(dapps_path: P, registrar: Arc, remote: Remote) -> Self { ServerBuilder { - dapps_path: dapps_path, + dapps_path: dapps_path.as_ref().to_owned(), + extra_dapps: vec![], handler: Arc::new(IoHandler::new()), registrar: registrar, sync_status: Arc::new(|| false), @@ -160,6 +163,7 @@ impl ServerBuilder { pub fn fetch(self, fetch: X) -> ServerBuilder { ServerBuilder { dapps_path: self.dapps_path, + extra_dapps: vec![], handler: self.handler, registrar: self.registrar, sync_status: self.sync_status, @@ -188,6 +192,12 @@ impl ServerBuilder { self } + /// Change extra dapps paths (apart from `dapps_path`) + pub fn extra_dapps>(mut self, extra_dapps: &[P]) -> Self { + self.extra_dapps = extra_dapps.iter().map(|p| p.as_ref().to_owned()).collect(); + 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, hosts: Option>) -> Result { @@ -197,6 +207,7 @@ impl ServerBuilder { NoAuth, self.handler.clone(), self.dapps_path.clone(), + self.extra_dapps.clone(), self.signer_address.clone(), self.registrar.clone(), self.sync_status.clone(), @@ -215,6 +226,7 @@ impl ServerBuilder { HttpBasicAuth::single_user(username, password), self.handler.clone(), self.dapps_path.clone(), + self.extra_dapps.clone(), self.signer_address.clone(), self.registrar.clone(), self.sync_status.clone(), @@ -270,7 +282,8 @@ impl Server { hosts: Option>, authorization: A, handler: Arc, - dapps_path: String, + dapps_path: PathBuf, + extra_dapps: Vec, signer_address: Option<(String, u16)>, registrar: Arc, sync_status: Arc, @@ -287,7 +300,14 @@ impl Server { remote.clone(), fetch.clone(), )); - let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_address.clone(), web_proxy_tokens, remote.clone(), fetch.clone())); + let endpoints = Arc::new(apps::all_endpoints( + dapps_path, + extra_dapps, + signer_address.clone(), + web_proxy_tokens, + remote.clone(), + fetch.clone(), + )); let cors_domains = Self::cors_domains(signer_address.clone()); let special = Arc::new({ diff --git a/dapps/src/tests/helpers/mod.rs b/dapps/src/tests/helpers/mod.rs index d3f97b35bcf..5cc367fccf5 100644 --- a/dapps/src/tests/helpers/mod.rs +++ b/dapps/src/tests/helpers/mod.rs @@ -51,7 +51,7 @@ pub fn init_server(hosts: Option>, process: F, remote: Remote) 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.to_str().unwrap().into(), registrar.clone(), remote, + &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(), hosts).unwrap(); @@ -66,7 +66,7 @@ pub fn serve_with_auth(user: &str, pass: &str) -> Server { let registrar = Arc::new(FakeRegistrar::new()); let mut dapps_path = env::temp_dir(); dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading"); - ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar.clone(), Remote::new_sync()) + ServerBuilder::new(&dapps_path, registrar.clone(), Remote::new_sync()) .signer_address(Some(("127.0.0.1".into(), SIGNER_PORT))) .start_basic_auth_http(&"127.0.0.1:0".parse().unwrap(), None, user, pass).unwrap() } diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 47a0af0bb3d..818c263d03b 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -37,6 +37,7 @@ usage! { cmd_snapshot: bool, cmd_restore: bool, cmd_ui: bool, + cmd_dapp: bool, cmd_tools: bool, cmd_hash: bool, cmd_kill: bool, @@ -525,6 +526,7 @@ mod tests { cmd_snapshot: false, cmd_restore: false, cmd_ui: false, + cmd_dapp: false, cmd_tools: false, cmd_hash: false, cmd_db: false, diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index dc7c487ffaf..11ebb744760 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -5,6 +5,7 @@ Parity. Ethereum Client. Usage: parity [options] parity ui [options] + parity dapp [options] parity daemon [options] parity account (new | list ) [options] parity account import ... [options] @@ -36,15 +37,15 @@ Operating Options: (default: {flag_mode_alarm}). --auto-update SET Set a releases set to automatically update and install. - all - All updates in the our release track. + all - All updates in the our release track. critical - Only consensus/security updates. - none - No updates will be auto-installed. + none - No updates will be auto-installed. (default: {flag_auto_update}). --release-track TRACK Set which release track we should use for updates. - stable - Stable releases. - beta - Beta releases. + stable - Stable releases. + beta - Beta releases. nightly - Nightly releases (unstable). - testing - Testing releases (do not use). + testing - Testing releases (do not use). current - Whatever track this executable was released on (default: {flag_release_track}). --no-download Normally new releases will be downloaded ready for @@ -362,7 +363,7 @@ Legacy Options: --cache MB Equivalent to --cache-size MB. Internal Options: - --can-restart Executable will auto-restart if exiting with 69. + --can-restart Executable will auto-restart if exiting with 69. Miscellaneous Options: -c --config CONFIG Specify a filename containing a configuration file. diff --git a/parity/configuration.rs b/parity/configuration.rs index 23adffe6616..f388259d782 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -17,7 +17,7 @@ use std::time::Duration; use std::io::Read; use std::net::SocketAddr; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::cmp::max; use cli::{Args, ArgsError}; use util::{Hashable, U256, Uint, Bytes, version_data, Secret, Address}; @@ -335,6 +335,7 @@ impl Configuration { net_settings: self.network_settings(), dapps_conf: dapps_conf, signer_conf: signer_conf, + dapp: self.dapp_to_open()?, ui: self.args.cmd_ui, name: self.args.flag_identity, custom_bootnodes: self.args.flag_bootnodes.is_some(), @@ -507,8 +508,26 @@ impl Configuration { hosts: self.dapps_hosts(), user: self.args.flag_dapps_user.clone(), pass: self.args.flag_dapps_pass.clone(), - dapps_path: self.directories().dapps, + dapps_path: PathBuf::from(self.directories().dapps), + extra_dapps: if self.args.cmd_dapp { + self.args.arg_path.iter().map(|path| PathBuf::from(path)).collect() + } else { + vec![] + }, + } + } + + fn dapp_to_open(&self) -> Result, String> { + if !self.args.cmd_dapp { + return Ok(None); } + let path = self.args.arg_path.get(0).map(String::as_str).unwrap_or("."); + let path = Path::new(path).canonicalize() + .map_err(|e| format!("Invalid path: {}. Error: {:?}", path, e))?; + let name = path.file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| "Root path is not supported.".to_owned())?; + Ok(Some(name.into())) } fn gas_pricer_config(&self) -> Result { @@ -690,7 +709,7 @@ impl Configuration { "none" => UpdateFilter::None, "critical" => UpdateFilter::Critical, "all" => UpdateFilter::All, - _ => return Err("Invalid value for `--auto-update`. See `--help` for more information.".into()), + _ => return Err("Invalid value for `--auto-update`. See `--help` for more information.".into()), }, track: match self.args.flag_release_track.as_ref() { "stable" => ReleaseTrack::Stable, @@ -698,7 +717,7 @@ impl Configuration { "nightly" => ReleaseTrack::Nightly, "testing" => ReleaseTrack::Testing, "current" => ReleaseTrack::Unknown, - _ => return Err("Invalid value for `--releases-track`. See `--help` for more information.".into()), + _ => return Err("Invalid value for `--releases-track`. See `--help` for more information.".into()), }, path: default_hypervisor_path(), }) @@ -1025,6 +1044,7 @@ mod tests { dapps_conf: Default::default(), signer_conf: Default::default(), ui: false, + dapp: None, name: "".into(), custom_bootnodes: false, fat_db: Default::default(), @@ -1219,6 +1239,22 @@ mod tests { }); } + #[test] + fn should_parse_dapp_opening() { + // given + let temp = RandomTempPath::new(); + let name = temp.file_name().unwrap().to_str().unwrap(); + create_dir(temp.as_str().to_owned()).unwrap(); + + // when + let conf0 = parse(&["parity", "dapp", temp.to_str().unwrap()]); + + // then + assert_eq!(conf0.dapp_to_open(), Ok(Some(name.into()))); + let extra_dapps = conf0.dapps_config().extra_dapps; + assert_eq!(extra_dapps, vec![temp.to_owned()]); + } + #[test] fn should_not_bail_on_empty_line_in_reserved_peers() { let temp = RandomTempPath::new(); diff --git a/parity/dapps.rs b/parity/dapps.rs index 8ec526a0586..711ba3a4974 100644 --- a/parity/dapps.rs +++ b/parity/dapps.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +use std::path::PathBuf; use std::sync::Arc; use io::PanicHandler; use rpc_apis; @@ -33,7 +34,8 @@ pub struct Configuration { pub hosts: Option>, pub user: Option, pub pass: Option, - pub dapps_path: String, + pub dapps_path: PathBuf, + pub extra_dapps: Vec, } impl Default for Configuration { @@ -46,7 +48,8 @@ impl Default for Configuration { hosts: Some(Vec::new()), user: None, pass: None, - dapps_path: replace_home(&data_dir, "$BASE/dapps"), + dapps_path: replace_home(&data_dir, "$BASE/dapps").into(), + extra_dapps: vec![], } } } @@ -80,7 +83,14 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Result, _url: &SocketAddr, _allowed_hosts: Option>, _auth: Option<(String, String)>, @@ -106,6 +117,7 @@ mod server { #[cfg(feature = "dapps")] mod server { use super::Dependencies; + use std::path::PathBuf; use std::sync::Arc; use std::net::SocketAddr; use std::io; @@ -122,7 +134,8 @@ mod server { pub fn setup_dapps_server( deps: Dependencies, - dapps_path: String, + dapps_path: PathBuf, + extra_dapps: Vec, url: &SocketAddr, allowed_hosts: Option>, auth: Option<(String, String)>, @@ -130,7 +143,7 @@ mod server { use ethcore_dapps as dapps; let server = dapps::ServerBuilder::new( - dapps_path, + &dapps_path, Arc::new(Registrar { client: deps.client.clone() }), deps.remote.clone(), ); @@ -141,6 +154,7 @@ mod server { .fetch(deps.fetch.clone()) .sync_status(Arc::new(move || is_major_importing(Some(sync.status().state), client.queue_info()))) .web_proxy_tokens(Arc::new(move |token| signer.is_valid_web_proxy_access_token(&token))) + .extra_dapps(&extra_dapps) .signer_address(deps.signer.address()); let server = rpc_apis::setup_rpc(server, deps.apis.clone(), rpc_apis::ApiSet::UnsafeContext); diff --git a/parity/run.rs b/parity/run.rs index 672c80fcf55..5e91e67fbb2 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -91,6 +91,7 @@ pub struct RunCmd { pub net_settings: NetworkSettings, pub dapps_conf: dapps::Configuration, pub signer_conf: signer::Configuration, + pub dapp: Option, pub ui: bool, pub name: String, pub custom_bootnodes: bool, @@ -117,6 +118,17 @@ pub fn open_ui(dapps_conf: &dapps::Configuration, signer_conf: &signer::Configur Ok(()) } +pub fn open_dapp(dapps_conf: &dapps::Configuration, dapp: &str) -> Result<(), String> { + if !dapps_conf.enabled { + return Err("Cannot use DAPP command with Dapps turned off.".into()) + } + + let url = format!("http://{}:{}/{}/", dapps_conf.interface, dapps_conf.port, dapp); + url::open(&url); + Ok(()) +} + + pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> Result { if cmd.ui && cmd.dapps_conf.enabled { // Check if Parity is already running @@ -428,6 +440,10 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R open_ui(&cmd.dapps_conf, &cmd.signer_conf)?; } + if let Some(dapp) = cmd.dapp { + open_dapp(&cmd.dapps_conf, &dapp)?; + } + // Handle exit let restart = wait_for_exit( panic_handler,