Web view with web3.site support (#4313)
* Web-domain based routing * Support base32-encoded urls * Proper support for non-domain based routing * Handling long domain names * Switching to web3.site * Encoding for *.web3.site urls * Add DappUrlInput component * Update Web views with store * Update spec description * Update spec description * edited url does not allow in-place store edits * Fixing dapps access on 127.0.0.1:8180 * Use /web/<hash> urls for iframe * Redirecting to parity.web3.site * Disabling the redirection
This commit is contained in:
parent
c9d38cac6e
commit
b4c24d5ab3
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -106,6 +106,11 @@ dependencies = [
|
|||||||
"syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base32"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bigint"
|
name = "bigint"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@ -409,6 +414,7 @@ dependencies = [
|
|||||||
name = "ethcore-dapps"
|
name = "ethcore-dapps"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
|
"clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"ethcore-devtools 1.6.0",
|
"ethcore-devtools 1.6.0",
|
||||||
@ -2463,6 +2469,7 @@ dependencies = [
|
|||||||
"checksum app_dirs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7d1c0d48a81bbb13043847f957971f4d87c81542d80ece5e84ba3cba4058fd4"
|
"checksum app_dirs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7d1c0d48a81bbb13043847f957971f4d87c81542d80ece5e84ba3cba4058fd4"
|
||||||
"checksum arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "16e3bdb2f54b3ace0285975d59a97cf8ed3855294b2b6bc651fcf22a9c352975"
|
"checksum arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "16e3bdb2f54b3ace0285975d59a97cf8ed3855294b2b6bc651fcf22a9c352975"
|
||||||
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
|
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
|
||||||
|
"checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c"
|
||||||
"checksum bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2311bcd71b281e142a095311c22509f0d6bcd87b3000d7dbaa810929b9d6f6ae"
|
"checksum bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2311bcd71b281e142a095311c22509f0d6bcd87b3000d7dbaa810929b9d6f6ae"
|
||||||
"checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c"
|
"checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c"
|
||||||
"checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d"
|
"checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d"
|
||||||
|
@ -23,6 +23,7 @@ serde = "0.8"
|
|||||||
serde_json = "0.8"
|
serde_json = "0.8"
|
||||||
linked-hash-map = "0.3"
|
linked-hash-map = "0.3"
|
||||||
parity-dapps-glue = "1.4"
|
parity-dapps-glue = "1.4"
|
||||||
|
base32 = "0.3"
|
||||||
mime = "0.2"
|
mime = "0.2"
|
||||||
mime_guess = "1.6.1"
|
mime_guess = "1.6.1"
|
||||||
time = "0.1.35"
|
time = "0.1.35"
|
||||||
|
@ -123,6 +123,7 @@ impl server::Handler<net::HttpStream> for RestApiRouter {
|
|||||||
return Next::write();
|
return Next::write();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO [ToDr] Consider using `path.app_params` instead
|
||||||
let url = extract_url(&request);
|
let url = extract_url(&request);
|
||||||
if url.is_none() {
|
if url.is_none() {
|
||||||
// Just return 404 if we can't parse URL
|
// Just return 404 if we can't parse URL
|
||||||
|
@ -32,8 +32,8 @@ pub mod manifest;
|
|||||||
|
|
||||||
extern crate parity_ui;
|
extern crate parity_ui;
|
||||||
|
|
||||||
pub const HOME_PAGE: &'static str = "home";
|
pub const HOME_PAGE: &'static str = "parity";
|
||||||
pub const DAPPS_DOMAIN: &'static str = ".parity";
|
pub const DAPPS_DOMAIN: &'static str = ".web3.site";
|
||||||
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";
|
||||||
pub const UTILS_PATH: &'static str = "parity-utils";
|
pub const UTILS_PATH: &'static str = "parity-utils";
|
||||||
|
@ -22,6 +22,7 @@ use std::collections::BTreeMap;
|
|||||||
#[derive(Debug, PartialEq, Default, Clone)]
|
#[derive(Debug, PartialEq, Default, Clone)]
|
||||||
pub struct EndpointPath {
|
pub struct EndpointPath {
|
||||||
pub app_id: String,
|
pub app_id: String,
|
||||||
|
pub app_params: Vec<String>,
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub using_dapps_domains: bool,
|
pub using_dapps_domains: bool,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![cfg_attr(feature="nightly", plugin(clippy))]
|
#![cfg_attr(feature="nightly", plugin(clippy))]
|
||||||
|
|
||||||
|
extern crate base32;
|
||||||
extern crate hyper;
|
extern crate hyper;
|
||||||
extern crate time;
|
extern crate time;
|
||||||
extern crate url as url_lib;
|
extern crate url as url_lib;
|
||||||
@ -91,11 +92,11 @@ impl<F> SyncStatus for F where F: Fn() -> bool + Send + Sync {
|
|||||||
/// Validates Web Proxy tokens
|
/// Validates Web Proxy tokens
|
||||||
pub trait WebProxyTokens: Send + Sync {
|
pub trait WebProxyTokens: Send + Sync {
|
||||||
/// Should return true if token is a valid web proxy access token.
|
/// Should return true if token is a valid web proxy access token.
|
||||||
fn is_web_proxy_token_valid(&self, token: &String) -> bool;
|
fn is_web_proxy_token_valid(&self, token: &str) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync {
|
impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync {
|
||||||
fn is_web_proxy_token_valid(&self, token: &String) -> bool { self(token.to_owned()) }
|
fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Webapps HTTP+RPC server build.
|
/// Webapps HTTP+RPC server build.
|
||||||
@ -409,6 +410,6 @@ mod util_tests {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(none, Vec::<String>::new());
|
assert_eq!(none, Vec::<String>::new());
|
||||||
assert_eq!(some, vec!["http://home.parity".to_owned(), "http://127.0.0.1:18180".into()]);
|
assert_eq!(some, vec!["http://parity.web3.site".to_owned(), "http://127.0.0.1:18180".into()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,6 +252,7 @@ fn should_extract_path_with_appid() {
|
|||||||
prefix: None,
|
prefix: None,
|
||||||
path: EndpointPath {
|
path: EndpointPath {
|
||||||
app_id: "app".to_owned(),
|
app_id: "app".to_owned(),
|
||||||
|
app_params: vec![],
|
||||||
host: "".to_owned(),
|
host: "".to_owned(),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
using_dapps_domains: true,
|
using_dapps_domains: true,
|
||||||
|
@ -97,9 +97,7 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
|
|||||||
=>
|
=>
|
||||||
{
|
{
|
||||||
trace!(target: "dapps", "Redirecting to correct web request: {:?}", referer_url);
|
trace!(target: "dapps", "Redirecting to correct web request: {:?}", referer_url);
|
||||||
// TODO [ToDr] Some nice util for this!
|
let len = cmp::min(referer_url.path.len(), 2); // /web/<encoded>/
|
||||||
let using_domain = if referer.using_dapps_domains { 0 } else { 1 };
|
|
||||||
let len = cmp::min(referer_url.path.len(), using_domain + 3); // token + protocol + hostname
|
|
||||||
let base = referer_url.path[..len].join("/");
|
let base = referer_url.path[..len].join("/");
|
||||||
let requested = url.map(|u| u.path.join("/")).unwrap_or_default();
|
let requested = url.map(|u| u.path.join("/")).unwrap_or_default();
|
||||||
Redirection::boxed(&format!("/{}/{}", base, requested))
|
Redirection::boxed(&format!("/{}/{}", base, requested))
|
||||||
@ -262,20 +260,27 @@ fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint
|
|||||||
match *url {
|
match *url {
|
||||||
Some(ref url) => match url.host {
|
Some(ref url) => match url.host {
|
||||||
Host::Domain(ref domain) if domain.ends_with(DAPPS_DOMAIN) => {
|
Host::Domain(ref domain) if domain.ends_with(DAPPS_DOMAIN) => {
|
||||||
let len = domain.len() - DAPPS_DOMAIN.len();
|
let id = &domain[0..(domain.len() - DAPPS_DOMAIN.len())];
|
||||||
let id = domain[0..len].to_owned();
|
let (id, params) = if let Some(split) = id.rfind('.') {
|
||||||
|
let (params, id) = id.split_at(split);
|
||||||
|
(id[1..].to_owned(), [params.to_owned()].into_iter().chain(&url.path).cloned().collect())
|
||||||
|
} else {
|
||||||
|
(id.to_owned(), url.path.clone())
|
||||||
|
};
|
||||||
|
|
||||||
(Some(EndpointPath {
|
(Some(EndpointPath {
|
||||||
app_id: id,
|
app_id: id,
|
||||||
|
app_params: params,
|
||||||
host: domain.clone(),
|
host: domain.clone(),
|
||||||
port: url.port,
|
port: url.port,
|
||||||
using_dapps_domains: true,
|
using_dapps_domains: true,
|
||||||
}), special_endpoint(url))
|
}), special_endpoint(url))
|
||||||
},
|
},
|
||||||
_ if url.path.len() > 1 => {
|
_ if url.path.len() > 1 => {
|
||||||
let id = url.path[0].clone();
|
let id = url.path[0].to_owned();
|
||||||
(Some(EndpointPath {
|
(Some(EndpointPath {
|
||||||
app_id: id.clone(),
|
app_id: id,
|
||||||
|
app_params: url.path[1..].to_vec(),
|
||||||
host: format!("{}", url.host),
|
host: format!("{}", url.host),
|
||||||
port: url.port,
|
port: url.port,
|
||||||
using_dapps_domains: false,
|
using_dapps_domains: false,
|
||||||
@ -296,6 +301,7 @@ fn should_extract_endpoint() {
|
|||||||
extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok()),
|
extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok()),
|
||||||
(Some(EndpointPath {
|
(Some(EndpointPath {
|
||||||
app_id: "status".to_owned(),
|
app_id: "status".to_owned(),
|
||||||
|
app_params: vec!["index.html".to_owned()],
|
||||||
host: "localhost".to_owned(),
|
host: "localhost".to_owned(),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
using_dapps_domains: false,
|
using_dapps_domains: false,
|
||||||
@ -307,6 +313,7 @@ fn should_extract_endpoint() {
|
|||||||
extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok()),
|
extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok()),
|
||||||
(Some(EndpointPath {
|
(Some(EndpointPath {
|
||||||
app_id: "rpc".to_owned(),
|
app_id: "rpc".to_owned(),
|
||||||
|
app_params: vec!["".to_owned()],
|
||||||
host: "localhost".to_owned(),
|
host: "localhost".to_owned(),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
using_dapps_domains: false,
|
using_dapps_domains: false,
|
||||||
@ -314,10 +321,11 @@ fn should_extract_endpoint() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extract_endpoint(&Url::parse("http://my.status.parity/parity-utils/inject.js").ok()),
|
extract_endpoint(&Url::parse("http://my.status.web3.site/parity-utils/inject.js").ok()),
|
||||||
(Some(EndpointPath {
|
(Some(EndpointPath {
|
||||||
app_id: "my.status".to_owned(),
|
app_id: "status".to_owned(),
|
||||||
host: "my.status.parity".to_owned(),
|
app_params: vec!["my".to_owned(), "parity-utils".into(), "inject.js".into()],
|
||||||
|
host: "my.status.web3.site".to_owned(),
|
||||||
port: 80,
|
port: 80,
|
||||||
using_dapps_domains: true,
|
using_dapps_domains: true,
|
||||||
}), SpecialEndpoint::Utils)
|
}), SpecialEndpoint::Utils)
|
||||||
@ -325,10 +333,11 @@ fn should_extract_endpoint() {
|
|||||||
|
|
||||||
// By Subdomain
|
// By Subdomain
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extract_endpoint(&Url::parse("http://my.status.parity/test.html").ok()),
|
extract_endpoint(&Url::parse("http://status.web3.site/test.html").ok()),
|
||||||
(Some(EndpointPath {
|
(Some(EndpointPath {
|
||||||
app_id: "my.status".to_owned(),
|
app_id: "status".to_owned(),
|
||||||
host: "my.status.parity".to_owned(),
|
app_params: vec!["test.html".to_owned()],
|
||||||
|
host: "status.web3.site".to_owned(),
|
||||||
port: 80,
|
port: 80,
|
||||||
using_dapps_domains: true,
|
using_dapps_domains: true,
|
||||||
}), SpecialEndpoint::None)
|
}), SpecialEndpoint::None)
|
||||||
@ -336,10 +345,11 @@ fn should_extract_endpoint() {
|
|||||||
|
|
||||||
// RPC by subdomain
|
// RPC by subdomain
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extract_endpoint(&Url::parse("http://my.status.parity/rpc/").ok()),
|
extract_endpoint(&Url::parse("http://my.status.web3.site/rpc/").ok()),
|
||||||
(Some(EndpointPath {
|
(Some(EndpointPath {
|
||||||
app_id: "my.status".to_owned(),
|
app_id: "status".to_owned(),
|
||||||
host: "my.status.parity".to_owned(),
|
app_params: vec!["my".to_owned(), "rpc".into(), "".into()],
|
||||||
|
host: "my.status.web3.site".to_owned(),
|
||||||
port: 80,
|
port: 80,
|
||||||
using_dapps_domains: true,
|
using_dapps_domains: true,
|
||||||
}), SpecialEndpoint::Rpc)
|
}), SpecialEndpoint::Rpc)
|
||||||
@ -347,10 +357,11 @@ fn should_extract_endpoint() {
|
|||||||
|
|
||||||
// API by subdomain
|
// API by subdomain
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extract_endpoint(&Url::parse("http://my.status.parity/api/").ok()),
|
extract_endpoint(&Url::parse("http://my.status.web3.site/api/").ok()),
|
||||||
(Some(EndpointPath {
|
(Some(EndpointPath {
|
||||||
app_id: "my.status".to_owned(),
|
app_id: "status".to_owned(),
|
||||||
host: "my.status.parity".to_owned(),
|
app_params: vec!["my".to_owned(), "api".into(), "".into()],
|
||||||
|
host: "my.status.web3.site".to_owned(),
|
||||||
port: 80,
|
port: 80,
|
||||||
using_dapps_domains: true,
|
using_dapps_domains: true,
|
||||||
}), SpecialEndpoint::Api)
|
}), SpecialEndpoint::Api)
|
||||||
|
@ -143,7 +143,7 @@ fn should_return_signer_port_cors_headers_for_home_parity() {
|
|||||||
"\
|
"\
|
||||||
POST /api/ping HTTP/1.1\r\n\
|
POST /api/ping HTTP/1.1\r\n\
|
||||||
Host: localhost:8080\r\n\
|
Host: localhost:8080\r\n\
|
||||||
Origin: http://home.parity\r\n\
|
Origin: http://parity.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
{}
|
{}
|
||||||
@ -153,8 +153,8 @@ fn should_return_signer_port_cors_headers_for_home_parity() {
|
|||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||||
assert!(
|
assert!(
|
||||||
response.headers_raw.contains("Access-Control-Allow-Origin: http://home.parity"),
|
response.headers_raw.contains("Access-Control-Allow-Origin: http://parity.web3.site"),
|
||||||
"CORS header for home.parity missing: {:?}",
|
"CORS header for parity.web3.site missing: {:?}",
|
||||||
response.headers
|
response.headers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ fn should_resolve_dapp() {
|
|||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\
|
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -52,7 +52,7 @@ fn should_return_503_when_syncing_but_should_make_the_calls() {
|
|||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\
|
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -81,7 +81,7 @@ fn should_return_502_on_hash_mismatch() {
|
|||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: 94f093625c06887d94d9fee0d5f9cc4aaa46f33d24d1c7e4b5237e7c37d547dd.parity\r\n\
|
Host: 94f093625c06887d94d9fee0d5f9cc4aaa46f33d24d1c7e4b5237e7c37d547dd.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -112,7 +112,7 @@ fn should_return_error_for_invalid_dapp_zip() {
|
|||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
|
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -144,7 +144,7 @@ fn should_return_fetched_dapp_content() {
|
|||||||
let response1 = http_client::request(server.addr(),
|
let response1 = http_client::request(server.addr(),
|
||||||
"\
|
"\
|
||||||
GET /index.html HTTP/1.1\r\n\
|
GET /index.html HTTP/1.1\r\n\
|
||||||
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\
|
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -152,7 +152,7 @@ fn should_return_fetched_dapp_content() {
|
|||||||
let response2 = http_client::request(server.addr(),
|
let response2 = http_client::request(server.addr(),
|
||||||
"\
|
"\
|
||||||
GET /manifest.json HTTP/1.1\r\n\
|
GET /manifest.json HTTP/1.1\r\n\
|
||||||
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\
|
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -207,7 +207,7 @@ fn should_return_fetched_content() {
|
|||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
|
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -234,7 +234,7 @@ fn should_cache_content() {
|
|||||||
);
|
);
|
||||||
let request_str = "\
|
let request_str = "\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
|
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
";
|
";
|
||||||
@ -265,7 +265,7 @@ fn should_not_request_content_twice() {
|
|||||||
);
|
);
|
||||||
let request_str = "\
|
let request_str = "\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
|
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
";
|
";
|
||||||
@ -298,6 +298,17 @@ fn should_not_request_content_twice() {
|
|||||||
response2.assert_status("HTTP/1.1 200 OK");
|
response2.assert_status("HTTP/1.1 200 OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_encode_and_decode_base32() {
|
||||||
|
use base32;
|
||||||
|
|
||||||
|
let encoded = base32::encode(base32::Alphabet::Crockford, "token+https://parity.io".as_bytes());
|
||||||
|
assert_eq!("EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY", &encoded);
|
||||||
|
|
||||||
|
let data = base32::decode(base32::Alphabet::Crockford, "EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY").unwrap();
|
||||||
|
assert_eq!("token+https://parity.io", &String::from_utf8(data).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_stream_web_content() {
|
fn should_stream_web_content() {
|
||||||
// given
|
// given
|
||||||
@ -306,8 +317,8 @@ fn should_stream_web_content() {
|
|||||||
// when
|
// when
|
||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET /web/token/https/parity.io/ HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: localhost:8080\r\n\
|
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -322,20 +333,90 @@ fn should_stream_web_content() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_return_error_on_invalid_token() {
|
fn should_support_base32_encoded_web_urls() {
|
||||||
// given
|
// given
|
||||||
let (server, fetch) = serve_with_fetch("token");
|
let (server, fetch) = serve_with_fetch("token");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET /web/invalidtoken/https/parity.io/ HTTP/1.1\r\n\
|
GET /styles.css?test=123 HTTP/1.1\r\n\
|
||||||
|
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
|
||||||
|
Connection: close\r\n\
|
||||||
|
\r\n\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
response.assert_status("HTTP/1.1 200 OK");
|
||||||
|
assert_security_headers_for_embed(&response.headers);
|
||||||
|
|
||||||
|
fetch.assert_requested("https://parity.io/styles.css?test=123");
|
||||||
|
fetch.assert_no_more_requests();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_correctly_handle_long_label_when_splitted() {
|
||||||
|
// given
|
||||||
|
let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let response = request(server,
|
||||||
|
"\
|
||||||
|
GET /styles.css?test=123 HTTP/1.1\r\n\
|
||||||
|
Host: f1qprwk775k6am35a5wmpk3e9gnpgx3me1sk.mbsfcdqpwx3jd5h7ax39dxq2wvb5dhqpww3fe9t2wrvfdm.web.web3.site\r\n\
|
||||||
|
Connection: close\r\n\
|
||||||
|
\r\n\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
response.assert_status("HTTP/1.1 200 OK");
|
||||||
|
assert_security_headers_for_embed(&response.headers);
|
||||||
|
|
||||||
|
fetch.assert_requested("https://contribution.melonport.com/styles.css?test=123");
|
||||||
|
fetch.assert_no_more_requests();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_support_base32_encoded_web_urls_as_path() {
|
||||||
|
// given
|
||||||
|
let (server, fetch) = serve_with_fetch("token");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let response = request(server,
|
||||||
|
"\
|
||||||
|
GET /web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css?test=123 HTTP/1.1\r\n\
|
||||||
Host: localhost:8080\r\n\
|
Host: localhost:8080\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
response.assert_status("HTTP/1.1 200 OK");
|
||||||
|
assert_security_headers_for_embed(&response.headers);
|
||||||
|
|
||||||
|
fetch.assert_requested("https://parity.io/styles.css?test=123");
|
||||||
|
fetch.assert_no_more_requests();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_error_on_invalid_token() {
|
||||||
|
// given
|
||||||
|
let (server, fetch) = serve_with_fetch("test");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let response = request(server,
|
||||||
|
"\
|
||||||
|
GET / HTTP/1.1\r\n\
|
||||||
|
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
|
||||||
|
Connection: close\r\n\
|
||||||
|
\r\n\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
response.assert_status("HTTP/1.1 400 Bad Request");
|
response.assert_status("HTTP/1.1 400 Bad Request");
|
||||||
assert_security_headers_for_embed(&response.headers);
|
assert_security_headers_for_embed(&response.headers);
|
||||||
@ -365,28 +446,6 @@ fn should_return_error_on_invalid_protocol() {
|
|||||||
fetch.assert_no_more_requests();
|
fetch.assert_no_more_requests();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_redirect_if_trailing_slash_is_missing() {
|
|
||||||
// given
|
|
||||||
let (server, fetch) = serve_with_fetch("token");
|
|
||||||
|
|
||||||
// when
|
|
||||||
let response = request(server,
|
|
||||||
"\
|
|
||||||
GET /web/token/https/parity.io HTTP/1.1\r\n\
|
|
||||||
Host: localhost:8080\r\n\
|
|
||||||
Connection: close\r\n\
|
|
||||||
\r\n\
|
|
||||||
"
|
|
||||||
);
|
|
||||||
|
|
||||||
// then
|
|
||||||
response.assert_status("HTTP/1.1 302 Found");
|
|
||||||
response.assert_header("Location", "/web/token/https/parity.io/");
|
|
||||||
|
|
||||||
fetch.assert_no_more_requests();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_disallow_non_get_requests() {
|
fn should_disallow_non_get_requests() {
|
||||||
// given
|
// given
|
||||||
@ -395,8 +454,8 @@ fn should_disallow_non_get_requests() {
|
|||||||
// when
|
// when
|
||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
POST /token/https/parity.io/ HTTP/1.1\r\n\
|
POST / HTTP/1.1\r\n\
|
||||||
Host: web.parity\r\n\
|
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
|
||||||
Content-Type: application/json\r\n\
|
Content-Type: application/json\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
@ -423,14 +482,37 @@ fn should_fix_absolute_requests_based_on_referer() {
|
|||||||
GET /styles.css HTTP/1.1\r\n\
|
GET /styles.css HTTP/1.1\r\n\
|
||||||
Host: localhost:8080\r\n\
|
Host: localhost:8080\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
Referer: http://localhost:8080/web/token/https/parity.io/\r\n\
|
Referer: http://localhost:8080/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
response.assert_status("HTTP/1.1 302 Found");
|
response.assert_status("HTTP/1.1 302 Found");
|
||||||
response.assert_header("Location", "/web/token/https/parity.io/styles.css");
|
response.assert_header("Location", "/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css");
|
||||||
|
|
||||||
|
fetch.assert_no_more_requests();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_fix_absolute_requests_based_on_referer_in_url() {
|
||||||
|
// given
|
||||||
|
let (server, fetch) = serve_with_fetch("token");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let response = request(server,
|
||||||
|
"\
|
||||||
|
GET /styles.css HTTP/1.1\r\n\
|
||||||
|
Host: localhost:8080\r\n\
|
||||||
|
Connection: close\r\n\
|
||||||
|
Referer: http://localhost:8080/?__referer=web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/\r\n\
|
||||||
|
\r\n\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
response.assert_status("HTTP/1.1 302 Found");
|
||||||
|
response.assert_header("Location", "/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css");
|
||||||
|
|
||||||
fetch.assert_no_more_requests();
|
fetch.assert_no_more_requests();
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ fn should_display_404_on_invalid_dapp_with_domain() {
|
|||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: invaliddapp.parity\r\n\
|
Host: invaliddapp.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
"
|
"
|
||||||
@ -179,7 +179,7 @@ fn should_serve_proxy_pac() {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||||
assert_eq!(response.body, "D5\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"home.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\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());
|
assert_eq!(response.body, "DD\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"parity.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned());
|
||||||
assert_security_headers(&response.headers);
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ fn should_serve_dapps_domains() {
|
|||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET / HTTP/1.1\r\n\
|
GET / HTTP/1.1\r\n\
|
||||||
Host: ui.parity\r\n\
|
Host: ui.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
{}
|
{}
|
||||||
|
@ -20,6 +20,7 @@ use std::sync::Arc;
|
|||||||
use fetch::{self, Fetch};
|
use fetch::{self, Fetch};
|
||||||
use parity_reactor::Remote;
|
use parity_reactor::Remote;
|
||||||
|
|
||||||
|
use base32;
|
||||||
use hyper::{self, server, net, Next, Encoder, Decoder};
|
use hyper::{self, server, net, Next, Encoder, Decoder};
|
||||||
use hyper::status::StatusCode;
|
use hyper::status::StatusCode;
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ use apps;
|
|||||||
use endpoint::{Endpoint, Handler, EndpointPath};
|
use endpoint::{Endpoint, Handler, EndpointPath};
|
||||||
use handlers::{
|
use handlers::{
|
||||||
ContentFetcherHandler, ContentHandler, ContentValidator, ValidatorResponse,
|
ContentFetcherHandler, ContentHandler, ContentValidator, ValidatorResponse,
|
||||||
StreamingHandler, Redirection, extract_url,
|
StreamingHandler, extract_url,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use WebProxyTokens;
|
use WebProxyTokens;
|
||||||
@ -86,9 +87,10 @@ impl ContentValidator for WebInstaller {
|
|||||||
);
|
);
|
||||||
if is_html {
|
if is_html {
|
||||||
handler.set_initial_content(&format!(
|
handler.set_initial_content(&format!(
|
||||||
r#"<script src="/{}/inject.js"></script><script>history.replaceState({{}}, "", "/?{}{}")</script>"#,
|
r#"<script src="/{}/inject.js"></script><script>history.replaceState({{}}, "", "/?{}{}/{}")</script>"#,
|
||||||
apps::UTILS_PATH,
|
apps::UTILS_PATH,
|
||||||
apps::URL_REFERER,
|
apps::URL_REFERER,
|
||||||
|
apps::WEB_PATH,
|
||||||
&self.referer,
|
&self.referer,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -99,7 +101,6 @@ impl ContentValidator for WebInstaller {
|
|||||||
enum State<F: Fetch> {
|
enum State<F: Fetch> {
|
||||||
Initial,
|
Initial,
|
||||||
Error(ContentHandler),
|
Error(ContentHandler),
|
||||||
Redirecting(Redirection),
|
|
||||||
Fetching(ContentFetcherHandler<WebInstaller, F>),
|
Fetching(ContentFetcherHandler<WebInstaller, F>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,25 +115,26 @@ struct WebHandler<F: Fetch> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<F: Fetch> WebHandler<F> {
|
impl<F: Fetch> WebHandler<F> {
|
||||||
fn extract_target_url(&self, url: Option<Url>) -> Result<(String, String), State<F>> {
|
fn extract_target_url(&self, url: Option<Url>) -> Result<String, State<F>> {
|
||||||
let (path, query) = match url {
|
let token_and_url = self.path.app_params.get(0)
|
||||||
Some(url) => (url.path, url.query),
|
.map(|encoded| encoded.replace('.', ""))
|
||||||
None => {
|
.and_then(|encoded| base32::decode(base32::Alphabet::Crockford, &encoded.to_uppercase()))
|
||||||
return Err(State::Error(ContentHandler::error(
|
.and_then(|data| String::from_utf8(data).ok())
|
||||||
StatusCode::BadRequest, "Invalid URL", "Couldn't parse URL", None, self.embeddable_on.clone()
|
.ok_or_else(|| State::Error(ContentHandler::error(
|
||||||
)));
|
StatusCode::BadRequest,
|
||||||
}
|
"Invalid parameter",
|
||||||
};
|
"Couldn't parse given parameter:",
|
||||||
|
self.path.app_params.get(0).map(String::as_str),
|
||||||
|
self.embeddable_on.clone()
|
||||||
|
)))?;
|
||||||
|
|
||||||
// Support domain based routing.
|
let mut token_it = token_and_url.split('+');
|
||||||
let idx = match path.get(0).map(|m| m.as_ref()) {
|
let token = token_it.next();
|
||||||
Some(apps::WEB_PATH) => 1,
|
let target_url = token_it.next();
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if token supplied in URL is correct.
|
// Check if token supplied in URL is correct.
|
||||||
match path.get(idx) {
|
match token {
|
||||||
Some(ref token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {},
|
Some(token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {},
|
||||||
_ => {
|
_ => {
|
||||||
return Err(State::Error(ContentHandler::error(
|
return Err(State::Error(ContentHandler::error(
|
||||||
StatusCode::BadRequest, "Invalid Access Token", "Invalid or old web proxy access token supplied.", Some("Try refreshing the page."), self.embeddable_on.clone()
|
StatusCode::BadRequest, "Invalid Access Token", "Invalid or old web proxy access token supplied.", Some("Try refreshing the page."), self.embeddable_on.clone()
|
||||||
@ -141,9 +143,8 @@ impl<F: Fetch> WebHandler<F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate protocol
|
// Validate protocol
|
||||||
let protocol = match path.get(idx + 1).map(|a| a.as_str()) {
|
let mut target_url = match target_url {
|
||||||
Some("http") => "http",
|
Some(url) if url.starts_with("http://") || url.starts_with("https://") => url.to_owned(),
|
||||||
Some("https") => "https",
|
|
||||||
_ => {
|
_ => {
|
||||||
return Err(State::Error(ContentHandler::error(
|
return Err(State::Error(ContentHandler::error(
|
||||||
StatusCode::BadRequest, "Invalid Protocol", "Invalid protocol used.", None, self.embeddable_on.clone()
|
StatusCode::BadRequest, "Invalid Protocol", "Invalid protocol used.", None, self.embeddable_on.clone()
|
||||||
@ -151,28 +152,35 @@ impl<F: Fetch> WebHandler<F> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Redirect if address to main page does not end with /
|
if !target_url.ends_with("/") {
|
||||||
if let None = path.get(idx + 3) {
|
target_url = format!("{}/", target_url);
|
||||||
return Err(State::Redirecting(
|
|
||||||
Redirection::new(&format!("/{}/", path.join("/")))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = match query {
|
// TODO [ToDr] Should just use `path.app_params`
|
||||||
Some(query) => format!("?{}", query),
|
let (path, query) = match (&url, self.path.using_dapps_domains) {
|
||||||
|
(&Some(ref url), true) => (&url.path[..], &url.query),
|
||||||
|
(&Some(ref url), false) => (&url.path[2..], &url.query),
|
||||||
|
_ => {
|
||||||
|
return Err(State::Error(ContentHandler::error(
|
||||||
|
StatusCode::BadRequest, "Invalid URL", "Couldn't parse URL", None, self.embeddable_on.clone()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let query = match *query {
|
||||||
|
Some(ref query) => format!("?{}", query),
|
||||||
None => "".into(),
|
None => "".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((format!("{}://{}{}", protocol, path[idx + 2..].join("/"), query), path[0..].join("/")))
|
Ok(format!("{}{}{}", target_url, path.join("/"), query))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
|
impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
|
||||||
fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next {
|
fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next {
|
||||||
let url = extract_url(&request);
|
let url = extract_url(&request);
|
||||||
|
|
||||||
// First extract the URL (reject invalid URLs)
|
// First extract the URL (reject invalid URLs)
|
||||||
let (target_url, referer) = match self.extract_target_url(url) {
|
let target_url = match self.extract_target_url(url) {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
self.state = error;
|
self.state = error;
|
||||||
@ -186,7 +194,9 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
|
|||||||
self.control.clone(),
|
self.control.clone(),
|
||||||
WebInstaller {
|
WebInstaller {
|
||||||
embeddable_on: self.embeddable_on.clone(),
|
embeddable_on: self.embeddable_on.clone(),
|
||||||
referer: referer,
|
referer: self.path.app_params.get(0)
|
||||||
|
.expect("`target_url` is valid; app_params is not empty;qed")
|
||||||
|
.to_owned(),
|
||||||
},
|
},
|
||||||
self.embeddable_on.clone(),
|
self.embeddable_on.clone(),
|
||||||
self.remote.clone(),
|
self.remote.clone(),
|
||||||
@ -202,7 +212,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
|
|||||||
match self.state {
|
match self.state {
|
||||||
State::Initial => Next::end(),
|
State::Initial => Next::end(),
|
||||||
State::Error(ref mut handler) => handler.on_request_readable(decoder),
|
State::Error(ref mut handler) => handler.on_request_readable(decoder),
|
||||||
State::Redirecting(ref mut handler) => handler.on_request_readable(decoder),
|
|
||||||
State::Fetching(ref mut handler) => handler.on_request_readable(decoder),
|
State::Fetching(ref mut handler) => handler.on_request_readable(decoder),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,7 +220,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
|
|||||||
match self.state {
|
match self.state {
|
||||||
State::Initial => Next::end(),
|
State::Initial => Next::end(),
|
||||||
State::Error(ref mut handler) => handler.on_response(res),
|
State::Error(ref mut handler) => handler.on_response(res),
|
||||||
State::Redirecting(ref mut handler) => handler.on_response(res),
|
|
||||||
State::Fetching(ref mut handler) => handler.on_response(res),
|
State::Fetching(ref mut handler) => handler.on_response(res),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -220,7 +228,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
|
|||||||
match self.state {
|
match self.state {
|
||||||
State::Initial => Next::end(),
|
State::Initial => Next::end(),
|
||||||
State::Error(ref mut handler) => handler.on_response_writable(encoder),
|
State::Error(ref mut handler) => handler.on_response_writable(encoder),
|
||||||
State::Redirecting(ref mut handler) => handler.on_response_writable(encoder),
|
|
||||||
State::Fetching(ref mut handler) => handler.on_response_writable(encoder),
|
State::Fetching(ref mut handler) => handler.on_response_writable(encoder),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ pub fn request(address: &SocketAddr, request: &str) -> Response {
|
|||||||
let _ = req.read_to_string(&mut response);
|
let _ = req.read_to_string(&mut response);
|
||||||
|
|
||||||
let mut lines = response.lines();
|
let mut lines = response.lines();
|
||||||
let status = lines.next().unwrap().to_owned();
|
let status = lines.next().expect("Expected a response").to_owned();
|
||||||
let headers_raw = read_block(&mut lines, false);
|
let headers_raw = read_block(&mut lines, false);
|
||||||
let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect();
|
let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect();
|
||||||
let body = read_block(&mut lines, true);
|
let body = read_block(&mut lines, true);
|
||||||
|
@ -140,6 +140,7 @@
|
|||||||
"yargs": "6.6.0"
|
"yargs": "6.6.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"base32.js": "0.1.0",
|
||||||
"bignumber.js": "3.0.1",
|
"bignumber.js": "3.0.1",
|
||||||
"blockies": "0.0.2",
|
"blockies": "0.0.2",
|
||||||
"brace": "0.9.0",
|
"brace": "0.9.0",
|
||||||
|
61
js/src/ui/Form/DappUrlInput/dappUrlInput.js
Normal file
61
js/src/ui/Form/DappUrlInput/dappUrlInput.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import keycode from 'keycode';
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
|
export default class DappUrlInput extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onGoto: PropTypes.func.isRequired,
|
||||||
|
onRestore: PropTypes.func.isRequired,
|
||||||
|
url: PropTypes.string.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { className, url } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={ className }
|
||||||
|
onChange={ this.onChange }
|
||||||
|
onKeyDown={ this.onKeyDown }
|
||||||
|
type='text'
|
||||||
|
value={ url }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (event) => {
|
||||||
|
this.props.onChange(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
onKeyDown = (event) => {
|
||||||
|
switch (keycode(event)) {
|
||||||
|
case 'esc':
|
||||||
|
this.props.onRestore();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'enter':
|
||||||
|
this.props.onGoto();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
70
js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js
Normal file
70
js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import DappUrlInput from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let onChange;
|
||||||
|
let onGoto;
|
||||||
|
let onRestore;
|
||||||
|
|
||||||
|
function render (props = { url: 'http://some.url' }) {
|
||||||
|
onChange = sinon.stub();
|
||||||
|
onGoto = sinon.stub();
|
||||||
|
onRestore = sinon.stub();
|
||||||
|
|
||||||
|
component = shallow(
|
||||||
|
<DappUrlInput
|
||||||
|
onChange={ onChange }
|
||||||
|
onGoto={ onGoto }
|
||||||
|
onRestore={ onRestore }
|
||||||
|
{ ...props }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ui/Form/DappUrlInput', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('events', () => {
|
||||||
|
describe('onChange', () => {
|
||||||
|
it('calls the onChange callback as provided', () => {
|
||||||
|
component.simulate('change', { target: { value: 'testing' } });
|
||||||
|
expect(onChange).to.have.been.calledWith('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onKeyDown', () => {
|
||||||
|
it('calls the onGoto callback on enter', () => {
|
||||||
|
component.simulate('keyDown', { keyCode: 13 });
|
||||||
|
expect(onGoto).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the onRestor callback on esc', () => {
|
||||||
|
component.simulate('keyDown', { keyCode: 27 });
|
||||||
|
expect(onRestore).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
17
js/src/ui/Form/DappUrlInput/index.js
Normal file
17
js/src/ui/Form/DappUrlInput/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
export default from './dappUrlInput';
|
@ -15,6 +15,7 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import AddressSelect from './AddressSelect';
|
import AddressSelect from './AddressSelect';
|
||||||
|
import DappUrlInput from './DappUrlInput';
|
||||||
import FormWrap from './FormWrap';
|
import FormWrap from './FormWrap';
|
||||||
import Input from './Input';
|
import Input from './Input';
|
||||||
import InputAddress from './InputAddress';
|
import InputAddress from './InputAddress';
|
||||||
@ -31,6 +32,7 @@ import TypedInput from './TypedInput';
|
|||||||
export default from './form';
|
export default from './form';
|
||||||
export {
|
export {
|
||||||
AddressSelect,
|
AddressSelect,
|
||||||
|
DappUrlInput,
|
||||||
FormWrap,
|
FormWrap,
|
||||||
Input,
|
Input,
|
||||||
InputAddress,
|
InputAddress,
|
||||||
|
@ -35,7 +35,7 @@ import DappIcon from './DappIcon';
|
|||||||
import Editor from './Editor';
|
import Editor from './Editor';
|
||||||
import Errors from './Errors';
|
import Errors from './Errors';
|
||||||
import Features, { FEATURES, FeaturesStore } from './Features';
|
import Features, { FEATURES, FeaturesStore } from './Features';
|
||||||
import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
|
import Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
|
||||||
import GasPriceEditor from './GasPriceEditor';
|
import GasPriceEditor from './GasPriceEditor';
|
||||||
import GasPriceSelector from './GasPriceSelector';
|
import GasPriceSelector from './GasPriceSelector';
|
||||||
import Icons from './Icons';
|
import Icons from './Icons';
|
||||||
@ -80,8 +80,9 @@ export {
|
|||||||
ContextProvider,
|
ContextProvider,
|
||||||
CopyToClipboard,
|
CopyToClipboard,
|
||||||
CurrencySymbol,
|
CurrencySymbol,
|
||||||
DappIcon,
|
|
||||||
DappCard,
|
DappCard,
|
||||||
|
DappIcon,
|
||||||
|
DappUrlInput,
|
||||||
Editor,
|
Editor,
|
||||||
Errors,
|
Errors,
|
||||||
FEATURES,
|
FEATURES,
|
||||||
|
60
js/src/util/dapplink.js
Normal file
60
js/src/util/dapplink.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import base32 from 'base32.js';
|
||||||
|
|
||||||
|
const BASE_URL = '.web.web3.site';
|
||||||
|
const ENCODER_OPTS = { type: 'crockford' };
|
||||||
|
|
||||||
|
export function encodePath (token, url) {
|
||||||
|
const encoder = new base32.Encoder(ENCODER_OPTS);
|
||||||
|
const chars = `${token}+${url}`
|
||||||
|
.split('')
|
||||||
|
.map((char) => char.charCodeAt(0));
|
||||||
|
|
||||||
|
return encoder
|
||||||
|
.write(chars) // add the characters to encode
|
||||||
|
.finalize(); // create the encoded string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeUrl (token, url) {
|
||||||
|
const encoded = encodePath(token, url)
|
||||||
|
.match(/.{1,63}/g) // split into 63-character chunks, max length is 64 for URLs parts
|
||||||
|
.join('.'); // add '.' between URL parts
|
||||||
|
|
||||||
|
return `${encoded}${BASE_URL}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This export is really more a helper along the way of verifying the actual
|
||||||
|
// encoding (being able to decode test values from the node layer), than meant to
|
||||||
|
// be used as-is. Should the need arrise to decode URLs as well (instead of just
|
||||||
|
// producing), it would make sense to further split the output into the token/URL
|
||||||
|
export function decode (encoded) {
|
||||||
|
const decoder = new base32.Decoder(ENCODER_OPTS);
|
||||||
|
const sanitized = encoded
|
||||||
|
.replace(BASE_URL, '') // remove the BASE URL
|
||||||
|
.split('.') // split the string on the '.' (63-char boundaries)
|
||||||
|
.join(''); // combine without the '.'
|
||||||
|
|
||||||
|
return decoder
|
||||||
|
.write(sanitized) // add the string to decode
|
||||||
|
.finalize() // create the decoded buffer
|
||||||
|
.toString(); // create string from buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
BASE_URL
|
||||||
|
};
|
83
js/src/util/dapplink.spec.js
Normal file
83
js/src/util/dapplink.spec.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { BASE_URL, decode, encodePath, encodeUrl } from './dapplink';
|
||||||
|
|
||||||
|
const TEST_TOKEN = 'token';
|
||||||
|
const TEST_URL = 'https://parity.io';
|
||||||
|
const TEST_URL_LONG = 'http://some.very.very.very.long.long.long.domain.example.com';
|
||||||
|
const TEST_PREFIX = 'EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY';
|
||||||
|
const TEST_PREFIX_LONG = [
|
||||||
|
'EHQPPSBE5DM78X3G78QJYWVFDNJJWXK5E9WJWXK5E9WJWXK5E9WJWV3FDSKJWV3', 'FDSKJWV3FDSKJWS3FDNGPJVHECNW62VBGDHJJWRVFDM'
|
||||||
|
].join('.');
|
||||||
|
const TEST_RESULT = `${TEST_PREFIX}${BASE_URL}`;
|
||||||
|
const TEST_ENCODED = `${TEST_TOKEN}+${TEST_URL}`;
|
||||||
|
|
||||||
|
describe('util/ethlink', () => {
|
||||||
|
describe('decode', () => {
|
||||||
|
it('decodes into encoded url', () => {
|
||||||
|
expect(decode(TEST_PREFIX)).to.equal(TEST_ENCODED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes full into encoded url', () => {
|
||||||
|
expect(decode(TEST_RESULT)).to.equal(TEST_ENCODED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('encodePath', () => {
|
||||||
|
it('encodes a url/token combination', () => {
|
||||||
|
expect(encodePath(TEST_TOKEN, TEST_URL)).to.equal(TEST_PREFIX);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes when token changes', () => {
|
||||||
|
expect(encodePath('test-token-2', TEST_URL)).not.to.equal(TEST_PREFIX);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes when url changes', () => {
|
||||||
|
expect(encodePath(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_PREFIX);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('encodeUrl', () => {
|
||||||
|
it('encodes a url/token combination', () => {
|
||||||
|
expect(encodeUrl(TEST_TOKEN, TEST_URL)).to.equal(TEST_RESULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes when token changes', () => {
|
||||||
|
expect(encodeUrl('test-token-2', TEST_URL)).not.to.equal(TEST_RESULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes when url changes', () => {
|
||||||
|
expect(encodeUrl(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_RESULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('splitting', () => {
|
||||||
|
let encoded;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
encoded = encodeUrl(TEST_TOKEN, TEST_URL_LONG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('splits long values into boundary parts', () => {
|
||||||
|
expect(encoded).to.equal(`${TEST_PREFIX_LONG}${BASE_URL}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('first part 63 characters', () => {
|
||||||
|
expect(encoded.split('.')[0].length).to.equal(63);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,101 +14,62 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import Refresh from 'material-ui/svg-icons/navigation/refresh';
|
|
||||||
import Close from 'material-ui/svg-icons/navigation/close';
|
|
||||||
import Subdirectory from 'material-ui/svg-icons/navigation/subdirectory-arrow-left';
|
import Subdirectory from 'material-ui/svg-icons/navigation/subdirectory-arrow-left';
|
||||||
|
|
||||||
import { Button } from '~/ui';
|
import { Button, DappUrlInput } from '~/ui';
|
||||||
|
import { CloseIcon, RefreshIcon } from '~/ui/Icons';
|
||||||
const KEY_ESC = 27;
|
|
||||||
const KEY_ENTER = 13;
|
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class AddressBar extends Component {
|
export default class AddressBar extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
isLoading: PropTypes.bool.isRequired,
|
store: PropTypes.object.isRequired
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onRefresh: PropTypes.func.isRequired,
|
|
||||||
url: PropTypes.string.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
|
||||||
currentUrl: this.props.url
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
if (this.props.url === nextProps.url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
currentUrl: nextProps.url
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isPristine () {
|
|
||||||
return this.state.currentUrl === this.props.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isLoading } = this.props;
|
const { isLoading, isPristine, nextUrl } = this.props.store;
|
||||||
const { currentUrl } = this.state;
|
|
||||||
const isPristine = this.isPristine();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ this.props.className }>
|
<div className={ this.props.className }>
|
||||||
<Button
|
<Button
|
||||||
disabled={ isLoading }
|
disabled={ isLoading }
|
||||||
|
onClick={ this.onRefreshUrl }
|
||||||
icon={
|
icon={
|
||||||
isLoading
|
isLoading
|
||||||
? <Close />
|
? <CloseIcon />
|
||||||
: <Refresh />
|
: <RefreshIcon />
|
||||||
}
|
}
|
||||||
onClick={ this.onGo }
|
|
||||||
/>
|
/>
|
||||||
<input
|
<DappUrlInput
|
||||||
onChange={ this.onUpdateUrl }
|
onChange={ this.onChangeUrl }
|
||||||
onKeyDown={ this.onKey }
|
onGoto={ this.onGotoUrl }
|
||||||
type='text'
|
onRestore={ this.onRestoreUrl }
|
||||||
value={ currentUrl }
|
url={ nextUrl }
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={ isPristine }
|
disabled={ isPristine }
|
||||||
|
onClick={ this.onGotoUrl }
|
||||||
icon={ <Subdirectory /> }
|
icon={ <Subdirectory /> }
|
||||||
onClick={ this.onGo }
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdateUrl = (ev) => {
|
onRefreshUrl = () => {
|
||||||
this.setState({
|
this.props.store.reload();
|
||||||
currentUrl: ev.target.value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onKey = (ev) => {
|
|
||||||
const key = ev.which;
|
|
||||||
|
|
||||||
if (key === KEY_ESC) {
|
|
||||||
this.setState({
|
|
||||||
currentUrl: this.props.url
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === KEY_ENTER) {
|
onChangeUrl = (url) => {
|
||||||
this.onGo();
|
this.props.store.setNextUrl(url);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
onGo = () => {
|
onGotoUrl = () => {
|
||||||
if (this.isPristine()) {
|
this.props.store.gotoUrl();
|
||||||
this.props.onRefresh();
|
}
|
||||||
} else {
|
|
||||||
this.props.onChange(this.state.currentUrl);
|
onRestoreUrl = () => {
|
||||||
|
this.props.store.restoreUrl();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
48
js/src/views/Web/AddressBar/addressBar.spec.js
Normal file
48
js/src/views/Web/AddressBar/addressBar.spec.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AddressBar from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function createStore () {
|
||||||
|
store = {
|
||||||
|
nextUrl: 'https://parity.io'
|
||||||
|
};
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render (props = {}) {
|
||||||
|
component = shallow(
|
||||||
|
<AddressBar
|
||||||
|
className='testClass'
|
||||||
|
store={ createStore() }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('views/Web/AddressBar', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
158
js/src/views/Web/store.js
Normal file
158
js/src/views/Web/store.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { action, computed, observable, transaction } from 'mobx';
|
||||||
|
import localStore from 'store';
|
||||||
|
import { parse as parseUrl } from 'url';
|
||||||
|
|
||||||
|
import { encodePath, encodeUrl } from '~/util/dapplink';
|
||||||
|
|
||||||
|
const DEFAULT_URL = 'https://mkr.market';
|
||||||
|
const LS_LAST_ADDRESS = '_parity::webLastAddress';
|
||||||
|
|
||||||
|
const hasProtocol = /^https?:\/\//;
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
export default class Store {
|
||||||
|
@observable counter = Date.now();
|
||||||
|
@observable currentUrl = null;
|
||||||
|
@observable history = [];
|
||||||
|
@observable isLoading = false;
|
||||||
|
@observable parsedUrl = null;
|
||||||
|
@observable nextUrl = null;
|
||||||
|
@observable token = null;
|
||||||
|
|
||||||
|
constructor (api) {
|
||||||
|
this._api = api;
|
||||||
|
|
||||||
|
this.nextUrl = this.currentUrl = this.loadLastUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get encodedPath () {
|
||||||
|
return `${this._api.dappsUrl}/web/${encodePath(this.token, this.currentUrl)}?t=${this.counter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get encodedUrl () {
|
||||||
|
return `http://${encodeUrl(this.token, this.currentUrl)}:${this._api.dappsPort}?t=${this.counter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get frameId () {
|
||||||
|
return `_web_iframe_${this.counter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get isPristine () {
|
||||||
|
return this.currentUrl === this.nextUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action gotoUrl = (_url) => {
|
||||||
|
transaction(() => {
|
||||||
|
let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
|
||||||
|
|
||||||
|
if (!hasProtocol.test(url)) {
|
||||||
|
url = `https://${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setNextUrl(url);
|
||||||
|
this.setCurrentUrl(this.nextUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action reload = () => {
|
||||||
|
transaction(() => {
|
||||||
|
this.setLoading(true);
|
||||||
|
this.counter = Date.now();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action restoreUrl = () => {
|
||||||
|
this.setNextUrl(this.currentUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setHistory = (history) => {
|
||||||
|
this.history = history;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setLoading = (isLoading) => {
|
||||||
|
this.isLoading = isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setToken = (token) => {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setCurrentUrl = (_url) => {
|
||||||
|
const url = _url || this.currentUrl;
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.currentUrl = url;
|
||||||
|
this.parsedUrl = parseUrl(url);
|
||||||
|
|
||||||
|
this.saveLastUrl();
|
||||||
|
|
||||||
|
this.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setNextUrl = (url) => {
|
||||||
|
this.nextUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateToken = () => {
|
||||||
|
this.setToken(null);
|
||||||
|
|
||||||
|
return this._api.signer
|
||||||
|
.generateWebProxyAccessToken()
|
||||||
|
.then((token) => {
|
||||||
|
this.setToken(token);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('generateToken', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadHistory = () => {
|
||||||
|
return this._api.parity
|
||||||
|
.listRecentDapps()
|
||||||
|
.then((apps) => {
|
||||||
|
this.setHistory(apps);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('loadHistory', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLastUrl = () => {
|
||||||
|
return localStore.get(LS_LAST_ADDRESS) || DEFAULT_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLastUrl = () => {
|
||||||
|
return localStore.set(LS_LAST_ADDRESS, this.currentUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get (api) {
|
||||||
|
if (!instance) {
|
||||||
|
instance = new Store(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DEFAULT_URL,
|
||||||
|
LS_LAST_ADDRESS
|
||||||
|
};
|
202
js/src/views/Web/store.spec.js
Normal file
202
js/src/views/Web/store.spec.js
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import Store from './store';
|
||||||
|
|
||||||
|
const TEST_HISTORY = ['somethingA', 'somethingB'];
|
||||||
|
const TEST_TOKEN = 'testing-123';
|
||||||
|
const TEST_URL1 = 'http://some.test.domain.com';
|
||||||
|
const TEST_URL2 = 'http://something.different.com';
|
||||||
|
|
||||||
|
let api;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function createApi () {
|
||||||
|
api = {
|
||||||
|
dappsPort: 8080,
|
||||||
|
dappsUrl: 'http://home.web3.site:8080',
|
||||||
|
parity: {
|
||||||
|
listRecentDapps: sinon.stub().resolves(TEST_HISTORY)
|
||||||
|
},
|
||||||
|
signer: {
|
||||||
|
generateWebProxyAccessToken: sinon.stub().resolves(TEST_TOKEN)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
function create () {
|
||||||
|
store = new Store(createApi());
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('views/Web/Store', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
create();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@action', () => {
|
||||||
|
describe('gotoUrl', () => {
|
||||||
|
it('uses the nextUrl when none specified', () => {
|
||||||
|
store.setNextUrl('https://parity.io');
|
||||||
|
store.gotoUrl();
|
||||||
|
|
||||||
|
expect(store.currentUrl).to.equal('https://parity.io');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds https when no protocol', () => {
|
||||||
|
store.gotoUrl('google.com');
|
||||||
|
|
||||||
|
expect(store.currentUrl).to.equal('https://google.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('restoreUrl', () => {
|
||||||
|
it('sets the nextUrl to the currentUrl', () => {
|
||||||
|
store.setCurrentUrl(TEST_URL1);
|
||||||
|
store.setNextUrl(TEST_URL2);
|
||||||
|
store.restoreUrl();
|
||||||
|
|
||||||
|
expect(store.nextUrl).to.equal(TEST_URL1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCurrentUrl', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCurrentUrl(TEST_URL1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the url', () => {
|
||||||
|
expect(store.currentUrl).to.equal(TEST_URL1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setHistory', () => {
|
||||||
|
it('sets the history', () => {
|
||||||
|
store.setHistory(TEST_HISTORY);
|
||||||
|
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setLoading', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setLoading(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the loading state (true)', () => {
|
||||||
|
expect(store.isLoading).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the loading state (false)', () => {
|
||||||
|
store.setLoading(false);
|
||||||
|
|
||||||
|
expect(store.isLoading).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setNextUrl', () => {
|
||||||
|
it('sets the url', () => {
|
||||||
|
store.setNextUrl(TEST_URL1);
|
||||||
|
|
||||||
|
expect(store.nextUrl).to.equal(TEST_URL1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setToken', () => {
|
||||||
|
it('sets the token', () => {
|
||||||
|
store.setToken(TEST_TOKEN);
|
||||||
|
|
||||||
|
expect(store.token).to.equal(TEST_TOKEN);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@computed', () => {
|
||||||
|
describe('encodedUrl', () => {
|
||||||
|
describe('encodedPath', () => {
|
||||||
|
it('encodes current', () => {
|
||||||
|
store.setCurrentUrl(TEST_URL1);
|
||||||
|
expect(store.encodedPath).to.match(
|
||||||
|
/http:\/\/home\.web3\.site:8080\/web\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\?t=[0-9]*$/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encodes current', () => {
|
||||||
|
store.setCurrentUrl(TEST_URL1);
|
||||||
|
expect(store.encodedUrl).to.match(
|
||||||
|
/^http:\/\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\.web\.web3\.site:8080\?t=[0-9]*$/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frameId', () => {
|
||||||
|
it('creates an id', () => {
|
||||||
|
expect(store.frameId).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isPristine', () => {
|
||||||
|
it('is true when current === next', () => {
|
||||||
|
store.setCurrentUrl(TEST_URL1);
|
||||||
|
store.setNextUrl(TEST_URL1);
|
||||||
|
|
||||||
|
expect(store.isPristine).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is false when current !== next', () => {
|
||||||
|
store.setCurrentUrl(TEST_URL1);
|
||||||
|
store.setNextUrl(TEST_URL2);
|
||||||
|
|
||||||
|
expect(store.isPristine).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('operations', () => {
|
||||||
|
describe('generateToken', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return store.generateToken();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls signer_generateWebProxyAccessToken', () => {
|
||||||
|
expect(api.signer.generateWebProxyAccessToken).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the token as retrieved', () => {
|
||||||
|
expect(store.token).to.equal(TEST_TOKEN);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadHistory', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return store.loadHistory();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls parity_listRecentDapps', () => {
|
||||||
|
expect(api.parity.listRecentDapps).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the history as retrieved', () => {
|
||||||
|
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -14,19 +14,16 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import store from 'store';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { parse as parseUrl, format as formatUrl } from 'url';
|
|
||||||
import { parse as parseQuery } from 'querystring';
|
|
||||||
|
|
||||||
import AddressBar from './AddressBar';
|
import AddressBar from './AddressBar';
|
||||||
|
import Store from './store';
|
||||||
|
|
||||||
import styles from './web.css';
|
import styles from './web.css';
|
||||||
|
|
||||||
const LS_LAST_ADDRESS = '_parity::webLastAddress';
|
@observer
|
||||||
|
|
||||||
const hasProtocol = /^https?:\/\//;
|
|
||||||
|
|
||||||
export default class Web extends Component {
|
export default class Web extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
@ -36,120 +33,62 @@ export default class Web extends Component {
|
|||||||
params: PropTypes.object.isRequired
|
params: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
store = Store.get(this.context.api);
|
||||||
displayedUrl: null,
|
|
||||||
isLoading: true,
|
|
||||||
token: null,
|
|
||||||
url: null
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { api } = this.context;
|
this.store.gotoUrl(this.props.params.url);
|
||||||
const { params } = this.props;
|
return this.store.generateToken();
|
||||||
|
|
||||||
api
|
|
||||||
.signer
|
|
||||||
.generateWebProxyAccessToken()
|
|
||||||
.then((token) => {
|
|
||||||
this.setState({ token });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setUrl(params.url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (props) {
|
componentWillReceiveProps (props) {
|
||||||
this.setUrl(props.params.url);
|
this.store.gotoUrl(props.params.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUrl = (url) => {
|
|
||||||
url = url || store.get(LS_LAST_ADDRESS) || 'https://mkr.market';
|
|
||||||
if (!hasProtocol.test(url)) {
|
|
||||||
url = `https://${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ url, displayedUrl: url });
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { displayedUrl, isLoading, token } = this.state;
|
const { currentUrl, token } = this.store;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return (
|
return (
|
||||||
<div className={ styles.wrapper }>
|
<div className={ styles.wrapper }>
|
||||||
<h1 className={ styles.loading }>
|
<h1 className={ styles.loading }>
|
||||||
Requesting access token...
|
<FormattedMessage
|
||||||
|
id='web.requestToken'
|
||||||
|
defaultMessage='Requesting access token...'
|
||||||
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { dappsUrl } = this.context.api;
|
return currentUrl
|
||||||
const { url } = this.state;
|
? this.renderFrame()
|
||||||
|
: null;
|
||||||
if (!url || !token) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parseUrl(url);
|
renderFrame () {
|
||||||
const { protocol, host, path } = parsed;
|
const { encodedPath, frameId } = this.store;
|
||||||
const address = `${dappsUrl}/web/${token}/${protocol.slice(0, -1)}/${host}${path}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.wrapper }>
|
<div className={ styles.wrapper }>
|
||||||
<AddressBar
|
<AddressBar
|
||||||
className={ styles.url }
|
className={ styles.url }
|
||||||
isLoading={ isLoading }
|
store={ this.store }
|
||||||
onChange={ this.onUrlChange }
|
|
||||||
onRefresh={ this.onRefresh }
|
|
||||||
url={ displayedUrl }
|
|
||||||
/>
|
/>
|
||||||
<iframe
|
<iframe
|
||||||
className={ styles.frame }
|
className={ styles.frame }
|
||||||
frameBorder={ 0 }
|
frameBorder={ 0 }
|
||||||
name={ name }
|
id={ frameId }
|
||||||
|
name={ frameId }
|
||||||
onLoad={ this.iframeOnLoad }
|
onLoad={ this.iframeOnLoad }
|
||||||
sandbox='allow-forms allow-same-origin allow-scripts'
|
sandbox='allow-forms allow-same-origin allow-scripts'
|
||||||
scrolling='auto'
|
scrolling='auto'
|
||||||
src={ address }
|
src={ encodedPath }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUrlChange = (url) => {
|
|
||||||
if (!hasProtocol.test(url)) {
|
|
||||||
url = `https://${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.set(LS_LAST_ADDRESS, url);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isLoading: true,
|
|
||||||
displayedUrl: url,
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRefresh = () => {
|
|
||||||
const { displayedUrl } = this.state;
|
|
||||||
|
|
||||||
// Insert timestamp
|
|
||||||
// This is a hack to prevent caching.
|
|
||||||
const parsed = parseUrl(displayedUrl);
|
|
||||||
|
|
||||||
parsed.query = parseQuery(parsed.query);
|
|
||||||
parsed.query.t = Date.now().toString();
|
|
||||||
delete parsed.search;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isLoading: true,
|
|
||||||
url: formatUrl(parsed)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
iframeOnLoad = () => {
|
iframeOnLoad = () => {
|
||||||
this.setState({
|
this.store.setLoading(false);
|
||||||
isLoading: false
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
56
js/src/views/Web/web.spec.js
Normal file
56
js/src/views/Web/web.spec.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Web from './';
|
||||||
|
|
||||||
|
const TEST_URL = 'https://mkr.market';
|
||||||
|
|
||||||
|
let api;
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function createApi () {
|
||||||
|
api = {};
|
||||||
|
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render (url = TEST_URL) {
|
||||||
|
component = shallow(
|
||||||
|
<Web params={ { url } } />,
|
||||||
|
{
|
||||||
|
context: { api: createApi() }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('views/Web', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders loading with no token', () => {
|
||||||
|
expect(component.find('FormattedMessage').props().id).to.equal('web.requestToken');
|
||||||
|
});
|
||||||
|
});
|
@ -126,8 +126,8 @@ mod testing {
|
|||||||
// when
|
// when
|
||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
GET http://home.parity/ HTTP/1.1\r\n\
|
GET http://parity.web3.site/ HTTP/1.1\r\n\
|
||||||
Host: home.parity\r\n\
|
Host: parity.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
{}
|
{}
|
||||||
@ -139,6 +139,26 @@ mod testing {
|
|||||||
http_client::assert_security_headers_present(&response.headers, None);
|
http_client::assert_security_headers_present(&response.headers, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_redirect_to_parity_host() {
|
||||||
|
// given
|
||||||
|
let (server, port, _) = serve();
|
||||||
|
|
||||||
|
// when
|
||||||
|
let response = request(server,
|
||||||
|
&format!("\
|
||||||
|
GET / HTTP/1.1\r\n\
|
||||||
|
Host: 127.0.0.1:{}\r\n\
|
||||||
|
Connection: close\r\n\
|
||||||
|
\r\n\
|
||||||
|
{{}}
|
||||||
|
", port)
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_serve_styles_even_on_disallowed_domain() {
|
fn should_serve_styles_even_on_disallowed_domain() {
|
||||||
// given
|
// given
|
||||||
@ -168,8 +188,8 @@ mod testing {
|
|||||||
// when
|
// when
|
||||||
let response = request(server,
|
let response = request(server,
|
||||||
"\
|
"\
|
||||||
CONNECT home.parity:8080 HTTP/1.1\r\n\
|
CONNECT parity.web3.site:8080 HTTP/1.1\r\n\
|
||||||
Host: home.parity\r\n\
|
Host: parity.web3.site\r\n\
|
||||||
Connection: close\r\n\
|
Connection: close\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
{}
|
{}
|
||||||
|
@ -109,8 +109,9 @@ impl Server {
|
|||||||
|
|
||||||
// Create WebSocket
|
// Create WebSocket
|
||||||
let origin = format!("{}", addr);
|
let origin = format!("{}", addr);
|
||||||
|
let port = addr.port();
|
||||||
let ws = ws::Builder::new().with_settings(config).build(
|
let ws = ws::Builder::new().with_settings(config).build(
|
||||||
session::Factory::new(handler, origin, authcodes_path, skip_origin_validation)
|
session::Factory::new(handler, origin, port, authcodes_path, skip_origin_validation)
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let panic_handler = PanicHandler::new_in_arc();
|
let panic_handler = PanicHandler::new_in_arc();
|
||||||
|
@ -64,22 +64,16 @@ mod ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOME_DOMAIN: &'static str = "home.parity";
|
const HOME_DOMAIN: &'static str = "parity.web3.site";
|
||||||
|
|
||||||
fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool {
|
fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool {
|
||||||
match header {
|
match header.map(|h| String::from_utf8_lossy(h).into_owned()) {
|
||||||
None => false,
|
|
||||||
Some(h) => {
|
|
||||||
let v = String::from_utf8(h.to_owned()).ok();
|
|
||||||
match v {
|
|
||||||
Some(ref origin) if origin.starts_with("chrome-extension://") => true,
|
Some(ref origin) if origin.starts_with("chrome-extension://") => true,
|
||||||
Some(ref origin) if origin.starts_with(self_origin) => true,
|
Some(ref origin) if origin.starts_with(self_origin) => true,
|
||||||
Some(ref origin) if origin.starts_with(&format!("http://{}", self_origin)) => true,
|
Some(ref origin) if origin.starts_with(&format!("http://{}", self_origin)) => true,
|
||||||
Some(ref origin) if origin.starts_with(HOME_DOMAIN) => true,
|
Some(ref origin) if origin.starts_with(HOME_DOMAIN) => true,
|
||||||
Some(ref origin) if origin.starts_with(&format!("http://{}", HOME_DOMAIN)) => true,
|
Some(ref origin) if origin.starts_with(&format!("http://{}", HOME_DOMAIN)) => true,
|
||||||
_ => false
|
_ => false,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +128,7 @@ pub struct Session<M: Metadata> {
|
|||||||
out: ws::Sender,
|
out: ws::Sender,
|
||||||
skip_origin_validation: bool,
|
skip_origin_validation: bool,
|
||||||
self_origin: String,
|
self_origin: String,
|
||||||
|
self_port: u16,
|
||||||
authcodes_path: PathBuf,
|
authcodes_path: PathBuf,
|
||||||
handler: RpcHandler<M>,
|
handler: RpcHandler<M>,
|
||||||
file_handler: Arc<ui::Handler>,
|
file_handler: Arc<ui::Handler>,
|
||||||
@ -146,7 +141,8 @@ impl<M: Metadata> ws::Handler for Session<M> {
|
|||||||
|
|
||||||
// TODO [ToDr] ws server is not handling proxied requests correctly:
|
// TODO [ToDr] ws server is not handling proxied requests correctly:
|
||||||
// Trim domain name from resource part:
|
// Trim domain name from resource part:
|
||||||
let resource = req.resource().trim_left_matches(&format!("http://{}", HOME_DOMAIN));
|
let resource = req.resource().trim_left_matches(&format!("http://{}:{}", HOME_DOMAIN, self.self_port));
|
||||||
|
let resource = resource.trim_left_matches(&format!("http://{}", HOME_DOMAIN));
|
||||||
|
|
||||||
// Styles file is allowed for error pages to display nicely.
|
// Styles file is allowed for error pages to display nicely.
|
||||||
let is_styles_file = resource == "/styles.css";
|
let is_styles_file = resource == "/styles.css";
|
||||||
@ -229,16 +225,18 @@ pub struct Factory<M: Metadata> {
|
|||||||
handler: RpcHandler<M>,
|
handler: RpcHandler<M>,
|
||||||
skip_origin_validation: bool,
|
skip_origin_validation: bool,
|
||||||
self_origin: String,
|
self_origin: String,
|
||||||
|
self_port: u16,
|
||||||
authcodes_path: PathBuf,
|
authcodes_path: PathBuf,
|
||||||
file_handler: Arc<ui::Handler>,
|
file_handler: Arc<ui::Handler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Metadata> Factory<M> {
|
impl<M: Metadata> Factory<M> {
|
||||||
pub fn new(handler: RpcHandler<M>, self_origin: String, authcodes_path: PathBuf, skip_origin_validation: bool) -> Self {
|
pub fn new(handler: RpcHandler<M>, self_origin: String, self_port: u16, authcodes_path: PathBuf, skip_origin_validation: bool) -> Self {
|
||||||
Factory {
|
Factory {
|
||||||
handler: handler,
|
handler: handler,
|
||||||
skip_origin_validation: skip_origin_validation,
|
skip_origin_validation: skip_origin_validation,
|
||||||
self_origin: self_origin,
|
self_origin: self_origin,
|
||||||
|
self_port: self_port,
|
||||||
authcodes_path: authcodes_path,
|
authcodes_path: authcodes_path,
|
||||||
file_handler: Arc::new(ui::Handler::default()),
|
file_handler: Arc::new(ui::Handler::default()),
|
||||||
}
|
}
|
||||||
@ -254,6 +252,7 @@ impl<M: Metadata> ws::Factory for Factory<M> {
|
|||||||
handler: self.handler.clone(),
|
handler: self.handler.clone(),
|
||||||
skip_origin_validation: self.skip_origin_validation,
|
skip_origin_validation: self.skip_origin_validation,
|
||||||
self_origin: self.self_origin.clone(),
|
self_origin: self.self_origin.clone(),
|
||||||
|
self_port: self.self_port,
|
||||||
authcodes_path: self.authcodes_path.clone(),
|
authcodes_path: self.authcodes_path.clone(),
|
||||||
file_handler: self.file_handler.clone(),
|
file_handler: self.file_handler.clone(),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user