diff --git a/dapps/src/handlers/redirect.rs b/dapps/src/handlers/redirect.rs index dbe5f6e4a..8b6158266 100644 --- a/dapps/src/handlers/redirect.rs +++ b/dapps/src/handlers/redirect.rs @@ -42,7 +42,8 @@ impl server::Handler for Redirection { } fn on_response(&mut self, res: &mut server::Response) -> Next { - res.set_status(StatusCode::MovedPermanently); + // 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() } diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index a2c17a42c..bc54b0f37 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -71,6 +71,8 @@ mod rpc; mod api; mod proxypac; mod url; +#[cfg(test)] +mod tests; pub use self::apps::urlhint::ContractClient; @@ -205,6 +207,12 @@ impl Server { pub fn set_panic_handler(&self, handler: F) where F : Fn() -> () + Send + 'static { *self.panic_handler.lock().unwrap() = Some(Box::new(handler)); } + + #[cfg(test)] + /// Returns address that this server is bound to. + 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").addr() + } } impl Drop for Server { @@ -239,7 +247,7 @@ pub fn random_filename() -> String { } #[cfg(test)] -mod tests { +mod util_tests { use super::Server; #[test] diff --git a/dapps/src/router/mod.rs b/dapps/src/router/mod.rs index 359337047..94d0a0fc0 100644 --- a/dapps/src/router/mod.rs +++ b/dapps/src/router/mod.rs @@ -83,7 +83,7 @@ impl server::Handler for Router { (Some(ref path), _) if self.endpoints.contains_key(&path.app_id) => { self.endpoints.get(&path.app_id).unwrap().to_handler(path.clone()) }, - // Try to resolve and fetch dapp + // Try to resolve and fetch the dapp (Some(ref path), _) if self.fetch.contains(&path.app_id) => { let control = self.control.take().expect("on_request is called only once, thus control is always defined."); self.fetch.to_handler(path.clone(), control) @@ -93,6 +93,11 @@ impl server::Handler for Router { let address = apps::redirection_address(path.using_dapps_domains, self.main_page); Redirection::new(address.as_str()) }, + // Redirect any GET request to home. + _ if *req.method() == hyper::method::Method::Get => { + let address = apps::redirection_address(false, self.main_page); + Redirection::new(address.as_str()) + }, // RPC by default _ => { self.special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default()) diff --git a/dapps/src/tests/api.rs b/dapps/src/tests/api.rs new file mode 100644 index 000000000..a9d3eba3b --- /dev/null +++ b/dapps/src/tests/api.rs @@ -0,0 +1,84 @@ +// 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 tests::helpers::{serve, request}; + +#[test] +fn should_return_error() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + GET /api/empty HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned()); + assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); + assert_eq!(response.body, format!("58\n{}\n0\n\n", r#"{"code":"404","title":"Not Found","detail":"Resource you requested has not been found."}"#)); +} + +#[test] +fn should_serve_apps() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + GET /api/apps HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); + assert!(response.body.contains("Parity Home Screen")); +} + +#[test] +fn should_handle_ping() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + POST /api/ping HTTP/1.1\r\n\ + Host: home.parity\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); + assert_eq!(response.body, "0\n\n".to_owned()); +} + diff --git a/dapps/src/tests/authorization.rs b/dapps/src/tests/authorization.rs new file mode 100644 index 000000000..dceb194b7 --- /dev/null +++ b/dapps/src/tests/authorization.rs @@ -0,0 +1,79 @@ +// 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 tests::helpers::{serve_with_auth, request}; + +#[test] +fn should_require_authorization() { + // given + let server = serve_with_auth("test", "test"); + + // 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 + assert_eq!(response.status, "HTTP/1.1 401 Unauthorized".to_owned()); + assert_eq!(response.headers.get(0).unwrap(), "WWW-Authenticate: Basic realm=\"Parity\""); +} + +#[test] +fn should_reject_on_invalid_auth() { + // given + let server = serve_with_auth("test", "test"); + + // when + let response = request(server, + "\ + GET / HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l\r\n + \r\n\ + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 401 Unauthorized".to_owned()); + assert_eq!(response.body, "15\n

Unauthorized

\n0\n\n".to_owned()); + assert_eq!(response.headers_raw.contains("WWW-Authenticate"), false); +} + +#[test] +fn should_allow_on_valid_auth() { + // given + let server = serve_with_auth("Aladdin", "OpenSesame"); + + // when + let response = request(server, + "\ + GET /home/ HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l\r\n + \r\n\ + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); +} diff --git a/dapps/src/tests/helpers.rs b/dapps/src/tests/helpers.rs new file mode 100644 index 000000000..84f638b34 --- /dev/null +++ b/dapps/src/tests/helpers.rs @@ -0,0 +1,123 @@ +// 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::env; +use std::io::{Read, Write}; +use std::str::{self, Lines}; +use std::sync::Arc; +use std::net::TcpStream; +use rustc_serialize::hex::{ToHex, FromHex}; + +use ServerBuilder; +use Server; +use apps::urlhint::ContractClient; +use util::{Bytes, Address, Mutex, ToPretty}; + +const REGISTRAR: &'static str = "8e4e9b13d4b45cb0befc93c3061b1408f67316b2"; +const URLHINT: &'static str = "deadbeefcafe0000000000000000000000000000"; + +pub struct FakeRegistrar { + pub calls: Arc>>, + pub responses: Mutex>>, +} + +impl FakeRegistrar { + fn new() -> Self { + FakeRegistrar { + calls: Arc::new(Mutex::new(Vec::new())), + responses: Mutex::new( + vec![ + Ok(format!("000000000000000000000000{}", URLHINT).from_hex().unwrap()), + Ok(Vec::new()) + ] + ), + } + } +} + +impl ContractClient for FakeRegistrar { + fn registrar(&self) -> Result { + Ok(REGISTRAR.parse().unwrap()) + } + + fn call(&self, address: Address, data: Bytes) -> Result { + self.calls.lock().push((address.to_hex(), data.to_hex())); + self.responses.lock().remove(0) + } +} + +pub fn serve_hosts(hosts: Option>) -> 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"); + let builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar); + builder.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), hosts).unwrap() +} + +pub fn serve_with_auth(user: &str, pass: &str) -> Server { + let registrar = Arc::new(FakeRegistrar::new()); + let builder = ServerBuilder::new(env::temp_dir().to_str().unwrap().into(), registrar); + builder.start_basic_auth_http(&"127.0.0.1:0".parse().unwrap(), None, user, pass).unwrap() +} + +pub fn serve() -> Server { + serve_hosts(None) +} + +pub struct Response { + pub status: String, + pub headers: Vec, + pub headers_raw: String, + pub body: String, +} + +pub fn read_block(lines: &mut Lines, all: bool) -> String { + let mut block = String::new(); + loop { + let line = lines.next(); + match line { + None => break, + Some("") if !all => break, + Some(v) => { + block.push_str(v); + block.push_str("\n"); + }, + } + } + block +} + +pub fn request(server: Server, request: &str) -> Response { + let mut req = TcpStream::connect(server.addr()).unwrap(); + req.write_all(request.as_bytes()).unwrap(); + + let mut response = String::new(); + req.read_to_string(&mut response).unwrap(); + + let mut lines = response.lines(); + let status = lines.next().unwrap().to_owned(); + let headers_raw = read_block(&mut lines, false); + let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect(); + let body = read_block(&mut lines, true); + + Response { + status: status, + headers: headers, + headers_raw: headers_raw, + body: body, + } +} + diff --git a/dapps/src/tests/mod.rs b/dapps/src/tests/mod.rs new file mode 100644 index 000000000..8c5bf2283 --- /dev/null +++ b/dapps/src/tests/mod.rs @@ -0,0 +1,25 @@ +// 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 . + +//! Dapps server test suite + +mod helpers; + +mod api; +mod authorization; +mod redirection; +mod validation; + diff --git a/dapps/src/tests/redirection.rs b/dapps/src/tests/redirection.rs new file mode 100644 index 000000000..53aa393e2 --- /dev/null +++ b/dapps/src/tests/redirection.rs @@ -0,0 +1,185 @@ +// 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 tests::helpers::{serve, request}; + +#[test] +fn should_redirect_to_home() { + // given + let server = serve(); + + // 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 + assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned()); + assert_eq!(response.headers.get(0).unwrap(), "Location: /home/"); +} + +#[test] +fn should_redirect_to_home_when_trailing_slash_is_missing() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + GET /app HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + \r\n\ + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned()); + assert_eq!(response.headers.get(0).unwrap(), "Location: /home/"); +} + +#[test] +fn should_redirect_to_home_on_invalid_dapp() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + GET /invaliddapp/ HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + \r\n\ + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned()); + assert_eq!(response.headers.get(0).unwrap(), "Location: /home/"); +} + +#[test] +fn should_redirect_to_home_on_invalid_dapp_with_domain() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + GET / HTTP/1.1\r\n\ + Host: invaliddapp.parity\r\n\ + Connection: close\r\n\ + \r\n\ + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned()); + assert_eq!(response.headers.get(0).unwrap(), "Location: http://home.parity/"); +} + +#[test] +fn should_serve_rpc() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + POST / HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + Content-Type: application/json\r\n + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert_eq!(response.body, format!("57\n{}\n0\n\n", r#"{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error","data":null},"id":null}"#)); +} + +#[test] +fn should_serve_rpc_at_slash_rpc() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + POST /rpc HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + Content-Type: application/json\r\n + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert_eq!(response.body, format!("57\n{}\n0\n\n", r#"{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error","data":null},"id":null}"#)); +} + + +#[test] +fn should_serve_proxy_pac() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + GET /proxy/proxy.pac HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert_eq!(response.body, "86\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"*.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned()); +} + +#[test] +fn should_serve_utils() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + GET /parity-utils/inject.js HTTP/1.1\r\n\ + Host: 127.0.0.1:8080\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert_eq!(response.body.contains("function(){"), true); +} + diff --git a/dapps/src/tests/validation.rs b/dapps/src/tests/validation.rs new file mode 100644 index 000000000..b233a07d8 --- /dev/null +++ b/dapps/src/tests/validation.rs @@ -0,0 +1,79 @@ +// 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 tests::helpers::{serve_hosts, request}; + +#[test] +fn should_reject_invalid_host() { + // given + let server = serve_hosts(Some(vec!["localhost:8080".into()])); + + // 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 + assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned()); + assert_eq!(response.body, "85\n\n\t\t

Request with disallowed Host header has been blocked.

\n\t\t

Check the URL in your browser address bar.

\n\t\t\n0\n\n".to_owned()); +} + +#[test] +fn should_allow_valid_host() { + // given + let server = serve_hosts(Some(vec!["localhost:8080".into()])); + + // when + let response = request(server, + "\ + GET /home/ HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); +} + +#[test] +fn should_serve_dapps_domains() { + // given + let server = serve_hosts(Some(vec!["localhost:8080".into()])); + + // when + let response = request(server, + "\ + GET / HTTP/1.1\r\n\ + Host: home.parity\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); +} +