Remove obsolete dapps and update security headers (#2694)
* Embed allowed only on signer port * Adding security headers to dapps * Adding security headers to signer * Removing old dapps
This commit is contained in:
parent
487dfb0208
commit
5e67c89b4b
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -339,8 +339,6 @@ dependencies = [
|
|||||||
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
||||||
"parity-dapps-glue 1.4.0",
|
"parity-dapps-glue 1.4.0",
|
||||||
"parity-dapps-home 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
"parity-dapps-home 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
||||||
"parity-dapps-status 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
|
||||||
"parity-dapps-wallet 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
|
||||||
"parity-ui 1.4.0",
|
"parity-ui 1.4.0",
|
||||||
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@ -1202,22 +1200,6 @@ dependencies = [
|
|||||||
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parity-dapps-status"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "git+https://github.com/ethcore/parity-ui.git#8b1c31319228ad4cf9bd4ae740a0b933aa9e19c7"
|
|
||||||
dependencies = [
|
|
||||||
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parity-dapps-wallet"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "git+https://github.com/ethcore/parity-ui.git#8b1c31319228ad4cf9bd4ae740a0b933aa9e19c7"
|
|
||||||
dependencies = [
|
|
||||||
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parity-ui"
|
name = "parity-ui"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -1999,8 +1981,6 @@ dependencies = [
|
|||||||
"checksum odds 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b28c06e81b0f789122d415d6394b5fe849bde8067469f4c2980d3cdc10c78ec1"
|
"checksum odds 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b28c06e81b0f789122d415d6394b5fe849bde8067469f4c2980d3cdc10c78ec1"
|
||||||
"checksum parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "<none>"
|
"checksum parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "<none>"
|
||||||
"checksum parity-dapps-home 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "<none>"
|
"checksum parity-dapps-home 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "<none>"
|
||||||
"checksum parity-dapps-status 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "<none>"
|
|
||||||
"checksum parity-dapps-wallet 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "<none>"
|
|
||||||
"checksum parking_lot 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e0fd1be2c3cf5fef20a6d18fec252c4f3c87c14fc3039002eb7d4ed91e436826"
|
"checksum parking_lot 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e0fd1be2c3cf5fef20a6d18fec252c4f3c87c14fc3039002eb7d4ed91e436826"
|
||||||
"checksum phf 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "447d9d45f2e0b4a9b532e808365abf18fc211be6ca217202fcd45236ef12f026"
|
"checksum phf 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "447d9d45f2e0b4a9b532e808365abf18fc211be6ca217202fcd45236ef12f026"
|
||||||
"checksum phf_codegen 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "8af7ae7c3f75a502292b491e5cc0a1f69e3407744abe6e57e2a3b712bb82f01d"
|
"checksum phf_codegen 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "8af7ae7c3f75a502292b491e5cc0a1f69e3407744abe6e57e2a3b712bb82f01d"
|
||||||
|
@ -31,9 +31,7 @@ parity-ui = { path = "./ui" }
|
|||||||
parity-dapps-glue = { path = "./js-glue" }
|
parity-dapps-glue = { path = "./js-glue" }
|
||||||
### DEPRECATED
|
### DEPRECATED
|
||||||
parity-dapps = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" }
|
parity-dapps = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" }
|
||||||
parity-dapps-status = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" }
|
|
||||||
parity-dapps-home = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" }
|
parity-dapps-home = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" }
|
||||||
parity-dapps-wallet = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4", optional = true }
|
|
||||||
### /DEPRECATED
|
### /DEPRECATED
|
||||||
|
|
||||||
mime_guess = { version = "1.6.1" }
|
mime_guess = { version = "1.6.1" }
|
||||||
@ -43,14 +41,11 @@ clippy = { version = "0.0.90", optional = true}
|
|||||||
serde_codegen = { version = "0.8", optional = true }
|
serde_codegen = { version = "0.8", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serde_codegen", "extra-dapps"]
|
default = ["serde_codegen"]
|
||||||
extra-dapps = ["parity-dapps-wallet"]
|
|
||||||
nightly = ["serde_macros"]
|
nightly = ["serde_macros"]
|
||||||
dev = ["clippy", "ethcore-rpc/dev", "ethcore-util/dev"]
|
dev = ["clippy", "ethcore-rpc/dev", "ethcore-util/dev"]
|
||||||
|
|
||||||
use-precompiled-js = [
|
use-precompiled-js = [
|
||||||
"parity-ui/use-precompiled-js",
|
"parity-ui/use-precompiled-js",
|
||||||
"parity-dapps-status/use-precompiled-js",
|
|
||||||
"parity-dapps-home/use-precompiled-js",
|
"parity-dapps-home/use-precompiled-js",
|
||||||
"parity-dapps-wallet/use-precompiled-js"
|
|
||||||
]
|
]
|
||||||
|
@ -26,7 +26,6 @@ pub mod urlhint;
|
|||||||
pub mod fetcher;
|
pub mod fetcher;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
|
|
||||||
extern crate parity_dapps_status;
|
|
||||||
extern crate parity_dapps_home;
|
extern crate parity_dapps_home;
|
||||||
extern crate parity_ui;
|
extern crate parity_ui;
|
||||||
|
|
||||||
@ -50,37 +49,22 @@ pub fn utils() -> Box<Endpoint> {
|
|||||||
Box::new(PageEndpoint::with_prefix(parity_dapps_home::App::default(), UTILS_PATH.to_owned()))
|
Box::new(PageEndpoint::with_prefix(parity_dapps_home::App::default(), UTILS_PATH.to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_endpoints(dapps_path: String) -> Endpoints {
|
pub fn all_endpoints(dapps_path: String, signer_port: Option<u16>) -> Endpoints {
|
||||||
// fetch fs dapps at first to avoid overwriting builtins
|
// fetch fs dapps at first to avoid overwriting builtins
|
||||||
let mut pages = fs::local_endpoints(dapps_path);
|
let mut pages = fs::local_endpoints(dapps_path);
|
||||||
// Home page needs to be safe embed
|
|
||||||
// because we use Cross-Origin LocalStorage.
|
|
||||||
// TODO [ToDr] Account naming should be moved to parity.
|
|
||||||
pages.insert("home".into(), Box::new(
|
|
||||||
PageEndpoint::new_safe_to_embed(parity_dapps_home::App::default())
|
|
||||||
));
|
|
||||||
// NOTE [ToDr] Dapps will be currently embeded on 8180
|
// NOTE [ToDr] Dapps will be currently embeded on 8180
|
||||||
pages.insert("ui".into(), Box::new(
|
pages.insert("ui".into(), Box::new(
|
||||||
PageEndpoint::new_safe_to_embed(NewUi::default())
|
PageEndpoint::new_safe_to_embed(NewUi::default(), signer_port)
|
||||||
));
|
));
|
||||||
pages.insert("proxy".into(), ProxyPac::boxed());
|
|
||||||
insert::<parity_dapps_status::App>(&mut pages, "status");
|
|
||||||
insert::<parity_dapps_status::App>(&mut pages, "parity");
|
|
||||||
|
|
||||||
// Optional dapps
|
pages.insert("proxy".into(), ProxyPac::boxed());
|
||||||
wallet_page(&mut pages);
|
insert::<parity_dapps_home::App>(&mut pages, "home");
|
||||||
|
|
||||||
|
|
||||||
pages
|
pages
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "parity-dapps-wallet")]
|
|
||||||
fn wallet_page(pages: &mut Endpoints) {
|
|
||||||
extern crate parity_dapps_wallet;
|
|
||||||
insert::<parity_dapps_wallet::App>(pages, "wallet");
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "parity-dapps-wallet"))]
|
|
||||||
fn wallet_page(_pages: &mut Endpoints) {}
|
|
||||||
|
|
||||||
fn insert<T : WebApp + Default + 'static>(pages: &mut Endpoints, id: &str) {
|
fn insert<T : WebApp + Default + 'static>(pages: &mut Endpoints, id: &str) {
|
||||||
pages.insert(id.to_owned(), Box::new(PageEndpoint::new(T::default())));
|
pages.insert(id.to_owned(), Box::new(PageEndpoint::new(T::default())));
|
||||||
}
|
}
|
||||||
|
@ -23,53 +23,55 @@ use hyper::status::StatusCode;
|
|||||||
|
|
||||||
use util::version;
|
use util::version;
|
||||||
|
|
||||||
|
use handlers::add_security_headers;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ContentHandler {
|
pub struct ContentHandler {
|
||||||
code: StatusCode,
|
code: StatusCode,
|
||||||
content: String,
|
content: String,
|
||||||
mimetype: String,
|
mimetype: String,
|
||||||
write_pos: usize,
|
write_pos: usize,
|
||||||
|
safe_to_embed_at_port: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentHandler {
|
impl ContentHandler {
|
||||||
pub fn ok(content: String, mimetype: String) -> Self {
|
pub fn ok(content: String, mimetype: String) -> Self {
|
||||||
ContentHandler {
|
Self::new(StatusCode::Ok, content, mimetype)
|
||||||
code: StatusCode::Ok,
|
|
||||||
content: content,
|
|
||||||
mimetype: mimetype,
|
|
||||||
write_pos: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn not_found(content: String, mimetype: String) -> Self {
|
pub fn not_found(content: String, mimetype: String) -> Self {
|
||||||
ContentHandler {
|
Self::new(StatusCode::NotFound, content, mimetype)
|
||||||
code: StatusCode::NotFound,
|
|
||||||
content: content,
|
|
||||||
mimetype: mimetype,
|
|
||||||
write_pos: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn html(code: StatusCode, content: String) -> Self {
|
pub fn html(code: StatusCode, content: String, embeddable_at: Option<u16>) -> Self {
|
||||||
Self::new(code, content, "text/html".into())
|
Self::new_embeddable(code, content, "text/html".into(), embeddable_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn error(code: StatusCode, title: &str, message: &str, details: Option<&str>) -> Self {
|
pub fn error(code: StatusCode, title: &str, message: &str, details: Option<&str>) -> Self {
|
||||||
|
Self::error_embeddable(code, title, message, details, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error_embeddable(code: StatusCode, title: &str, message: &str, details: Option<&str>, embeddable_at: Option<u16>) -> Self {
|
||||||
Self::html(code, format!(
|
Self::html(code, format!(
|
||||||
include_str!("../error_tpl.html"),
|
include_str!("../error_tpl.html"),
|
||||||
title=title,
|
title=title,
|
||||||
message=message,
|
message=message,
|
||||||
details=details.unwrap_or_else(|| ""),
|
details=details.unwrap_or_else(|| ""),
|
||||||
version=version(),
|
version=version(),
|
||||||
))
|
), embeddable_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(code: StatusCode, content: String, mimetype: String) -> Self {
|
pub fn new(code: StatusCode, content: String, mimetype: String) -> Self {
|
||||||
|
Self::new_embeddable(code, content, mimetype, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_embeddable(code: StatusCode, content: String, mimetype: String, embeddable_at: Option<u16>) -> Self {
|
||||||
ContentHandler {
|
ContentHandler {
|
||||||
code: code,
|
code: code,
|
||||||
content: content,
|
content: content,
|
||||||
mimetype: mimetype,
|
mimetype: mimetype,
|
||||||
write_pos: 0,
|
write_pos: 0,
|
||||||
|
safe_to_embed_at_port: embeddable_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,6 +88,7 @@ impl server::Handler<HttpStream> for ContentHandler {
|
|||||||
fn on_response(&mut self, res: &mut server::Response) -> Next {
|
fn on_response(&mut self, res: &mut server::Response) -> Next {
|
||||||
res.set_status(self.code);
|
res.set_status(self.code);
|
||||||
res.headers_mut().set(header::ContentType(self.mimetype.parse().unwrap()));
|
res.headers_mut().set(header::ContentType(self.mimetype.parse().unwrap()));
|
||||||
|
add_security_headers(&mut res.headers_mut(), self.safe_to_embed_at_port.clone());
|
||||||
Next::write()
|
Next::write()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,24 @@ pub use self::fetch::{ContentFetcherHandler, ContentValidator, FetchControl};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use hyper::{server, header, net, uri};
|
use hyper::{server, header, net, uri};
|
||||||
|
|
||||||
|
/// Adds security-related headers to the Response.
|
||||||
|
pub fn add_security_headers(headers: &mut header::Headers, embeddable_at: Option<u16>) {
|
||||||
|
headers.set_raw("X-XSS-Protection", vec![b"1; mode=block".to_vec()]);
|
||||||
|
headers.set_raw("X-Content-Type-Options", vec![b"nosniff".to_vec()]);
|
||||||
|
|
||||||
|
// Embedding header:
|
||||||
|
if let Some(port) = embeddable_at {
|
||||||
|
headers.set_raw(
|
||||||
|
"X-Frame-Options",
|
||||||
|
vec![format!("ALLOW-FROM http://127.0.0.1:{}", port).into_bytes()]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// TODO [ToDr] Should we be more strict here (DENY?)?
|
||||||
|
headers.set_raw("X-Frame-Options", vec![b"SAMEORIGIN".to_vec()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts URL part from the Request.
|
||||||
pub fn extract_url(req: &server::Request<net::HttpStream>) -> Option<Url> {
|
pub fn extract_url(req: &server::Request<net::HttpStream>) -> Option<Url> {
|
||||||
match *req.uri() {
|
match *req.uri() {
|
||||||
uri::RequestUri::AbsoluteUri(ref url) => {
|
uri::RequestUri::AbsoluteUri(ref url) => {
|
||||||
|
@ -108,6 +108,7 @@ pub struct ServerBuilder {
|
|||||||
handler: Arc<IoHandler>,
|
handler: Arc<IoHandler>,
|
||||||
registrar: Arc<ContractClient>,
|
registrar: Arc<ContractClient>,
|
||||||
sync_status: Arc<SyncStatus>,
|
sync_status: Arc<SyncStatus>,
|
||||||
|
signer_port: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Extendable for ServerBuilder {
|
impl Extendable for ServerBuilder {
|
||||||
@ -124,6 +125,7 @@ impl ServerBuilder {
|
|||||||
handler: Arc::new(IoHandler::new()),
|
handler: Arc::new(IoHandler::new()),
|
||||||
registrar: registrar,
|
registrar: registrar,
|
||||||
sync_status: Arc::new(|| false),
|
sync_status: Arc::new(|| false),
|
||||||
|
signer_port: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +134,11 @@ impl ServerBuilder {
|
|||||||
self.sync_status = status;
|
self.sync_status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change default signer port.
|
||||||
|
pub fn with_signer_port(&mut self, signer_port: Option<u16>) {
|
||||||
|
self.signer_port = signer_port;
|
||||||
|
}
|
||||||
|
|
||||||
/// Asynchronously start server with no authentication,
|
/// Asynchronously start server with no authentication,
|
||||||
/// returns result with `Server` handle on success or an error.
|
/// returns result with `Server` handle on success or an error.
|
||||||
pub fn start_unsecured_http(&self, addr: &SocketAddr, hosts: Option<Vec<String>>) -> Result<Server, ServerError> {
|
pub fn start_unsecured_http(&self, addr: &SocketAddr, hosts: Option<Vec<String>>) -> Result<Server, ServerError> {
|
||||||
@ -141,6 +148,7 @@ impl ServerBuilder {
|
|||||||
NoAuth,
|
NoAuth,
|
||||||
self.handler.clone(),
|
self.handler.clone(),
|
||||||
self.dapps_path.clone(),
|
self.dapps_path.clone(),
|
||||||
|
self.signer_port.clone(),
|
||||||
self.registrar.clone(),
|
self.registrar.clone(),
|
||||||
self.sync_status.clone(),
|
self.sync_status.clone(),
|
||||||
)
|
)
|
||||||
@ -155,6 +163,7 @@ impl ServerBuilder {
|
|||||||
HttpBasicAuth::single_user(username, password),
|
HttpBasicAuth::single_user(username, password),
|
||||||
self.handler.clone(),
|
self.handler.clone(),
|
||||||
self.dapps_path.clone(),
|
self.dapps_path.clone(),
|
||||||
|
self.signer_port.clone(),
|
||||||
self.registrar.clone(),
|
self.registrar.clone(),
|
||||||
self.sync_status.clone(),
|
self.sync_status.clone(),
|
||||||
)
|
)
|
||||||
@ -189,13 +198,14 @@ impl Server {
|
|||||||
authorization: A,
|
authorization: A,
|
||||||
handler: Arc<IoHandler>,
|
handler: Arc<IoHandler>,
|
||||||
dapps_path: String,
|
dapps_path: String,
|
||||||
|
signer_port: Option<u16>,
|
||||||
registrar: Arc<ContractClient>,
|
registrar: Arc<ContractClient>,
|
||||||
sync_status: Arc<SyncStatus>,
|
sync_status: Arc<SyncStatus>,
|
||||||
) -> Result<Server, ServerError> {
|
) -> Result<Server, ServerError> {
|
||||||
let panic_handler = Arc::new(Mutex::new(None));
|
let panic_handler = Arc::new(Mutex::new(None));
|
||||||
let authorization = Arc::new(authorization);
|
let authorization = Arc::new(authorization);
|
||||||
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status));
|
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status));
|
||||||
let endpoints = Arc::new(apps::all_endpoints(dapps_path));
|
let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_port));
|
||||||
let special = Arc::new({
|
let special = Arc::new({
|
||||||
let mut special = HashMap::new();
|
let mut special = HashMap::new();
|
||||||
special.insert(router::SpecialEndpoint::Rpc, rpc::rpc(handler, panic_handler.clone()));
|
special.insert(router::SpecialEndpoint::Rpc, rpc::rpc(handler, panic_handler.clone()));
|
||||||
|
@ -25,7 +25,7 @@ pub struct PageEndpoint<T : WebApp + 'static> {
|
|||||||
/// Prefix to strip from the path (when `None` deducted from `app_id`)
|
/// Prefix to strip from the path (when `None` deducted from `app_id`)
|
||||||
pub prefix: Option<String>,
|
pub prefix: Option<String>,
|
||||||
/// Safe to be loaded in frame by other origin. (use wisely!)
|
/// Safe to be loaded in frame by other origin. (use wisely!)
|
||||||
safe_to_embed: bool,
|
safe_to_embed_at_port: Option<u16>,
|
||||||
info: EndpointInfo,
|
info: EndpointInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
|
|||||||
PageEndpoint {
|
PageEndpoint {
|
||||||
app: Arc::new(app),
|
app: Arc::new(app),
|
||||||
prefix: None,
|
prefix: None,
|
||||||
safe_to_embed: false,
|
safe_to_embed_at_port: None,
|
||||||
info: EndpointInfo::from(info),
|
info: EndpointInfo::from(info),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
|
|||||||
PageEndpoint {
|
PageEndpoint {
|
||||||
app: Arc::new(app),
|
app: Arc::new(app),
|
||||||
prefix: Some(prefix),
|
prefix: Some(prefix),
|
||||||
safe_to_embed: false,
|
safe_to_embed_at_port: None,
|
||||||
info: EndpointInfo::from(info),
|
info: EndpointInfo::from(info),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,12 +57,12 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
|
|||||||
/// Creates new `PageEndpoint` which can be safely used in iframe
|
/// Creates new `PageEndpoint` which can be safely used in iframe
|
||||||
/// even from different origin. It might be dangerous (clickjacking).
|
/// even from different origin. It might be dangerous (clickjacking).
|
||||||
/// Use wisely!
|
/// Use wisely!
|
||||||
pub fn new_safe_to_embed(app: T) -> Self {
|
pub fn new_safe_to_embed(app: T, port: Option<u16>) -> Self {
|
||||||
let info = app.info();
|
let info = app.info();
|
||||||
PageEndpoint {
|
PageEndpoint {
|
||||||
app: Arc::new(app),
|
app: Arc::new(app),
|
||||||
prefix: None,
|
prefix: None,
|
||||||
safe_to_embed: true,
|
safe_to_embed_at_port: port,
|
||||||
info: EndpointInfo::from(info),
|
info: EndpointInfo::from(info),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,8 +79,8 @@ impl<T: WebApp> Endpoint for PageEndpoint<T> {
|
|||||||
app: BuiltinDapp::new(self.app.clone()),
|
app: BuiltinDapp::new(self.app.clone()),
|
||||||
prefix: self.prefix.clone(),
|
prefix: self.prefix.clone(),
|
||||||
path: path,
|
path: path,
|
||||||
file: Default::default(),
|
file: handler::ServedFile::new(self.safe_to_embed_at_port.clone()),
|
||||||
safe_to_embed: self.safe_to_embed,
|
safe_to_embed_at_port: self.safe_to_embed_at_port.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ use hyper::net::HttpStream;
|
|||||||
use hyper::status::StatusCode;
|
use hyper::status::StatusCode;
|
||||||
use hyper::{Decoder, Encoder, Next};
|
use hyper::{Decoder, Encoder, Next};
|
||||||
use endpoint::EndpointPath;
|
use endpoint::EndpointPath;
|
||||||
use handlers::ContentHandler;
|
use handlers::{ContentHandler, add_security_headers};
|
||||||
|
|
||||||
/// Represents a file that can be sent to client.
|
/// Represents a file that can be sent to client.
|
||||||
/// Implementation should keep track of bytes already sent internally.
|
/// Implementation should keep track of bytes already sent internally.
|
||||||
@ -57,13 +57,14 @@ pub enum ServedFile<T: Dapp> {
|
|||||||
Error(ContentHandler),
|
Error(ContentHandler),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Dapp> Default for ServedFile<T> {
|
impl<T: Dapp> ServedFile<T> {
|
||||||
fn default() -> Self {
|
pub fn new(embeddable_at: Option<u16>) -> Self {
|
||||||
ServedFile::Error(ContentHandler::error(
|
ServedFile::Error(ContentHandler::error_embeddable(
|
||||||
StatusCode::NotFound,
|
StatusCode::NotFound,
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
"Requested dapp resource was not found.",
|
"Requested dapp resource was not found.",
|
||||||
None
|
None,
|
||||||
|
embeddable_at,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,7 +82,7 @@ pub struct PageHandler<T: Dapp> {
|
|||||||
/// Requested path.
|
/// Requested path.
|
||||||
pub path: EndpointPath,
|
pub path: EndpointPath,
|
||||||
/// Flag indicating if the file can be safely embeded (put in iframe).
|
/// Flag indicating if the file can be safely embeded (put in iframe).
|
||||||
pub safe_to_embed: bool,
|
pub safe_to_embed_at_port: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Dapp> PageHandler<T> {
|
impl<T: Dapp> PageHandler<T> {
|
||||||
@ -115,7 +116,7 @@ impl<T: Dapp> server::Handler<HttpStream> for PageHandler<T> {
|
|||||||
self.app.file(&self.extract_path(url.path()))
|
self.app.file(&self.extract_path(url.path()))
|
||||||
},
|
},
|
||||||
_ => None,
|
_ => None,
|
||||||
}.map_or_else(|| ServedFile::default(), |f| ServedFile::File(f));
|
}.map_or_else(|| ServedFile::new(self.safe_to_embed_at_port.clone()), |f| ServedFile::File(f));
|
||||||
Next::write()
|
Next::write()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,9 +129,8 @@ impl<T: Dapp> server::Handler<HttpStream> for PageHandler<T> {
|
|||||||
ServedFile::File(ref f) => {
|
ServedFile::File(ref f) => {
|
||||||
res.set_status(StatusCode::Ok);
|
res.set_status(StatusCode::Ok);
|
||||||
res.headers_mut().set(header::ContentType(f.content_type().parse().unwrap()));
|
res.headers_mut().set(header::ContentType(f.content_type().parse().unwrap()));
|
||||||
if !self.safe_to_embed {
|
// Security headers:
|
||||||
res.headers_mut().set_raw("X-Frame-Options", vec![b"SAMEORIGIN".to_vec()]);
|
add_security_headers(&mut res.headers_mut(), self.safe_to_embed_at_port);
|
||||||
}
|
|
||||||
Next::write()
|
Next::write()
|
||||||
},
|
},
|
||||||
ServedFile::Error(ref mut handler) => {
|
ServedFile::Error(ref mut handler) => {
|
||||||
@ -212,8 +212,8 @@ fn should_extract_path_with_appid() {
|
|||||||
port: 8080,
|
port: 8080,
|
||||||
using_dapps_domains: true,
|
using_dapps_domains: true,
|
||||||
},
|
},
|
||||||
file: Default::default(),
|
file: ServedFile::new(None),
|
||||||
safe_to_embed: true,
|
safe_to_embed_at_port: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// when
|
// when
|
||||||
|
@ -61,16 +61,16 @@ impl Endpoint for LocalPageEndpoint {
|
|||||||
app: LocalSingleFile { path: self.path.clone(), mime: mime.clone() },
|
app: LocalSingleFile { path: self.path.clone(), mime: mime.clone() },
|
||||||
prefix: None,
|
prefix: None,
|
||||||
path: path,
|
path: path,
|
||||||
file: Default::default(),
|
file: handler::ServedFile::new(None),
|
||||||
safe_to_embed: false,
|
safe_to_embed_at_port: None,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Box::new(handler::PageHandler {
|
Box::new(handler::PageHandler {
|
||||||
app: LocalDapp { path: self.path.clone() },
|
app: LocalDapp { path: self.path.clone() },
|
||||||
prefix: None,
|
prefix: None,
|
||||||
path: path,
|
path: path,
|
||||||
file: Default::default(),
|
file: handler::ServedFile::new(None),
|
||||||
safe_to_embed: false,
|
safe_to_embed_at_port: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use tests::helpers::{serve, serve_with_registrar, request};
|
use tests::helpers::{serve, serve_with_registrar, request, assert_security_headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_return_error() {
|
fn should_return_error() {
|
||||||
@ -36,6 +36,7 @@ fn should_return_error() {
|
|||||||
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
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.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."}"#));
|
assert_eq!(response.body, format!("58\n{}\n0\n\n", r#"{"code":"404","title":"Not Found","detail":"Resource you requested has not been found."}"#));
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -58,6 +59,7 @@ fn should_serve_apps() {
|
|||||||
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.headers.get(0).unwrap(), "Content-Type: application/json");
|
assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json");
|
||||||
assert!(response.body.contains("Parity Home Screen"), response.body);
|
assert!(response.body.contains("Parity Home Screen"), response.body);
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -80,6 +82,7 @@ fn should_handle_ping() {
|
|||||||
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.headers.get(0).unwrap(), "Content-Type: application/json");
|
assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json");
|
||||||
assert_eq!(response.body, "0\n\n".to_owned());
|
assert_eq!(response.body, "0\n\n".to_owned());
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -101,5 +104,6 @@ fn should_try_to_resolve_dapp() {
|
|||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
||||||
assert_eq!(registrar.calls.lock().len(), 2);
|
assert_eq!(registrar.calls.lock().len(), 2);
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use tests::helpers::{serve_with_auth, request};
|
use tests::helpers::{serve_with_auth, request, assert_security_headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_require_authorization() {
|
fn should_require_authorization() {
|
||||||
@ -76,4 +76,5 @@ fn should_allow_on_valid_auth() {
|
|||||||
|
|
||||||
// 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_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use tests::helpers::{serve_with_registrar, request};
|
use tests::helpers::{serve_with_registrar, request, assert_security_headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_resolve_dapp() {
|
fn should_resolve_dapp() {
|
||||||
@ -34,5 +34,6 @@ fn should_resolve_dapp() {
|
|||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
||||||
assert_eq!(registrar.calls.lock().len(), 2);
|
assert_eq!(registrar.calls.lock().len(), 2);
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,3 +92,7 @@ pub fn serve() -> Server {
|
|||||||
pub fn request(server: Server, request: &str) -> http_client::Response {
|
pub fn request(server: Server, request: &str) -> http_client::Response {
|
||||||
http_client::request(server.addr(), request)
|
http_client::request(server.addr(), request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn assert_security_headers(headers: &[String]) {
|
||||||
|
http_client::assert_security_headers_present(headers)
|
||||||
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use tests::helpers::{serve, request};
|
use tests::helpers::{serve, request, assert_security_headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_redirect_to_home() {
|
fn should_redirect_to_home() {
|
||||||
@ -74,6 +74,7 @@ fn should_display_404_on_invalid_dapp() {
|
|||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
||||||
assert!(response.body.contains("href=\"/home/"));
|
assert!(response.body.contains("href=\"/home/"));
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -94,6 +95,7 @@ fn should_display_404_on_invalid_dapp_with_domain() {
|
|||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
|
||||||
assert!(response.body.contains("href=\"http://home.parity/"));
|
assert!(response.body.contains("href=\"http://home.parity/"));
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -160,6 +162,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, "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());
|
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());
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -181,5 +184,6 @@ fn should_serve_utils() {
|
|||||||
// 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.contains("function(){"), true);
|
assert_eq!(response.body.contains("function(){"), true);
|
||||||
|
assert_security_headers(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,3 +64,18 @@ pub fn request(address: &SocketAddr, request: &str) -> Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if all required security headers are present
|
||||||
|
pub fn assert_security_headers_present(headers: &[String]) {
|
||||||
|
assert!(
|
||||||
|
headers.iter().find(|header| header.as_str() == "X-Frame-Options: SAMEORIGIN").is_some(),
|
||||||
|
"X-Frame-Options missing: {:?}", headers
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
headers.iter().find(|header| header.as_str() == "X-XSS-Protection: 1; mode=block").is_some(),
|
||||||
|
"X-XSS-Protection missing: {:?}", headers
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
headers.iter().find(|header| header.as_str() == "X-Content-Type-Options: nosniff").is_some(),
|
||||||
|
"X-Content-Type-Options missing: {:?}", headers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -58,6 +58,7 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<We
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let signer_port = deps.apis.signer_port.clone();
|
||||||
let url = format!("{}:{}", configuration.interface, configuration.port);
|
let url = format!("{}:{}", configuration.interface, configuration.port);
|
||||||
let addr = try!(url.parse().map_err(|_| format!("Invalid Webapps listen host/port given: {}", url)));
|
let addr = try!(url.parse().map_err(|_| format!("Invalid Webapps listen host/port given: {}", url)));
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<We
|
|||||||
(username.to_owned(), password)
|
(username.to_owned(), password)
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Some(try!(setup_dapps_server(deps, configuration.dapps_path, &addr, configuration.hosts, auth))))
|
Ok(Some(try!(setup_dapps_server(deps, configuration.dapps_path, &addr, configuration.hosts, auth, signer_port))))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use self::server::WebappServer;
|
pub use self::server::WebappServer;
|
||||||
@ -90,6 +91,7 @@ mod server {
|
|||||||
_url: &SocketAddr,
|
_url: &SocketAddr,
|
||||||
_allowed_hosts: Option<Vec<String>>,
|
_allowed_hosts: Option<Vec<String>>,
|
||||||
_auth: Option<(String, String)>,
|
_auth: Option<(String, String)>,
|
||||||
|
_signer_port: Option<u16>,
|
||||||
) -> Result<WebappServer, String> {
|
) -> Result<WebappServer, String> {
|
||||||
Err("Your Parity version has been compiled without WebApps support.".into())
|
Err("Your Parity version has been compiled without WebApps support.".into())
|
||||||
}
|
}
|
||||||
@ -115,7 +117,8 @@ mod server {
|
|||||||
dapps_path: String,
|
dapps_path: String,
|
||||||
url: &SocketAddr,
|
url: &SocketAddr,
|
||||||
allowed_hosts: Option<Vec<String>>,
|
allowed_hosts: Option<Vec<String>>,
|
||||||
auth: Option<(String, String)>
|
auth: Option<(String, String)>,
|
||||||
|
signer_port: Option<u16>,
|
||||||
) -> Result<WebappServer, String> {
|
) -> Result<WebappServer, String> {
|
||||||
use ethcore_dapps as dapps;
|
use ethcore_dapps as dapps;
|
||||||
|
|
||||||
@ -125,6 +128,8 @@ mod server {
|
|||||||
);
|
);
|
||||||
let sync = deps.sync.clone();
|
let sync = deps.sync.clone();
|
||||||
server.with_sync_status(Arc::new(move || sync.status().is_major_syncing()));
|
server.with_sync_status(Arc::new(move || sync.status().is_major_syncing()));
|
||||||
|
server.with_signer_port(signer_port);
|
||||||
|
|
||||||
let server = rpc_apis::setup_rpc(server, deps.apis.clone(), rpc_apis::ApiSet::UnsafeContext);
|
let server = rpc_apis::setup_rpc(server, deps.apis.clone(), rpc_apis::ApiSet::UnsafeContext);
|
||||||
let start_result = match auth {
|
let start_result = match auth {
|
||||||
None => {
|
None => {
|
||||||
|
@ -81,6 +81,7 @@ fn should_reject_invalid_host() {
|
|||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
||||||
assert!(response.body.contains("URL Blocked"));
|
assert!(response.body.contains("URL Blocked"));
|
||||||
|
http_client::assert_security_headers_present(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -101,6 +102,7 @@ fn should_serve_styles_even_on_disallowed_domain() {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
|
||||||
|
http_client::assert_security_headers_present(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -124,6 +126,7 @@ fn should_block_if_authorization_is_incorrect() {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
||||||
|
http_client::assert_security_headers_present(&response.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -202,4 +205,5 @@ fn should_allow_initial_connection_but_only_once() {
|
|||||||
// then
|
// then
|
||||||
assert_eq!(response1.status, "HTTP/1.1 101 Switching Protocols".to_owned());
|
assert_eq!(response1.status, "HTTP/1.1 101 Switching Protocols".to_owned());
|
||||||
assert_eq!(response2.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
assert_eq!(response2.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
|
||||||
|
http_client::assert_security_headers_present(&response2.headers);
|
||||||
}
|
}
|
||||||
|
@ -112,6 +112,8 @@ fn add_headers(mut response: ws::Response, mime: &str) -> ws::Response {
|
|||||||
{
|
{
|
||||||
let mut headers = response.headers_mut();
|
let mut headers = response.headers_mut();
|
||||||
headers.push(("X-Frame-Options".into(), b"SAMEORIGIN".to_vec()));
|
headers.push(("X-Frame-Options".into(), b"SAMEORIGIN".to_vec()));
|
||||||
|
headers.push(("X-XSS-Protection".into(), b"1; mode=block".to_vec()));
|
||||||
|
headers.push(("X-Content-Type-Options".into(), b"nosniff".to_vec()));
|
||||||
headers.push(("Server".into(), b"Parity/SignerUI".to_vec()));
|
headers.push(("Server".into(), b"Parity/SignerUI".to_vec()));
|
||||||
headers.push(("Content-Length".into(), content_len.as_bytes().to_vec()));
|
headers.push(("Content-Length".into(), content_len.as_bytes().to_vec()));
|
||||||
headers.push(("Content-Type".into(), mime.as_bytes().to_vec()));
|
headers.push(("Content-Type".into(), mime.as_bytes().to_vec()));
|
||||||
|
Loading…
Reference in New Issue
Block a user