243 lines
7.3 KiB
Rust
243 lines
7.3 KiB
Rust
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
|
// This file is part of Parity.
|
|
|
|
// Parity is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// Parity is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//! Hyper Server Handler that fetches a file during a request (proxy).
|
|
|
|
use std::{fs, fmt};
|
|
use std::path::PathBuf;
|
|
use std::sync::{mpsc, Arc};
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::time::{Instant, Duration};
|
|
|
|
use hyper::{header, server, Decoder, Encoder, Next, Method, Control, Client};
|
|
use hyper::net::HttpStream;
|
|
use hyper::status::StatusCode;
|
|
|
|
use handlers::ContentHandler;
|
|
use handlers::client::{Fetch, FetchResult};
|
|
use apps::redirection_address;
|
|
use apps::urlhint::GithubApp;
|
|
use apps::manifest::Manifest;
|
|
|
|
const FETCH_TIMEOUT: u64 = 30;
|
|
|
|
enum FetchState {
|
|
NotStarted(GithubApp),
|
|
Error(ContentHandler),
|
|
InProgress {
|
|
deadline: Instant,
|
|
receiver: mpsc::Receiver<FetchResult>,
|
|
},
|
|
Done(Manifest),
|
|
}
|
|
|
|
pub trait ContentValidator {
|
|
type Error: fmt::Debug;
|
|
|
|
fn validate_and_install(&self, app: PathBuf) -> Result<Manifest, Self::Error>;
|
|
fn done(&self, Option<&Manifest>);
|
|
}
|
|
|
|
pub struct ContentFetcherHandler<H: ContentValidator> {
|
|
abort: Arc<AtomicBool>,
|
|
control: Option<Control>,
|
|
status: FetchState,
|
|
client: Option<Client<Fetch>>,
|
|
using_dapps_domains: bool,
|
|
dapp: H,
|
|
}
|
|
|
|
impl<H: ContentValidator> Drop for ContentFetcherHandler<H> {
|
|
fn drop(&mut self) {
|
|
let manifest = match self.status {
|
|
FetchState::Done(ref manifest) => Some(manifest),
|
|
_ => None,
|
|
};
|
|
self.dapp.done(manifest);
|
|
}
|
|
}
|
|
|
|
impl<H: ContentValidator> ContentFetcherHandler<H> {
|
|
|
|
pub fn new(
|
|
app: GithubApp,
|
|
abort: Arc<AtomicBool>,
|
|
control: Control,
|
|
using_dapps_domains: bool,
|
|
handler: H) -> Self {
|
|
|
|
let client = Client::new().expect("Failed to create a Client");
|
|
ContentFetcherHandler {
|
|
abort: abort,
|
|
control: Some(control),
|
|
client: Some(client),
|
|
status: FetchState::NotStarted(app),
|
|
using_dapps_domains: using_dapps_domains,
|
|
dapp: handler,
|
|
}
|
|
}
|
|
|
|
fn close_client(client: &mut Option<Client<Fetch>>) {
|
|
client.take()
|
|
.expect("After client is closed we are going into write, hence we can never close it again")
|
|
.close();
|
|
}
|
|
|
|
|
|
// TODO [todr] https support
|
|
fn fetch_app(client: &mut Client<Fetch>, app: &GithubApp, abort: Arc<AtomicBool>, control: Control) -> Result<mpsc::Receiver<FetchResult>, String> {
|
|
let url = try!(app.url().parse().map_err(|e| format!("{:?}", e)));
|
|
trace!(target: "dapps", "Fetching from: {:?}", url);
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
let res = client.request(url, Fetch::new(tx, abort, Box::new(move || {
|
|
trace!(target: "dapps", "Fetching finished.");
|
|
// Ignoring control errors
|
|
let _ = control.ready(Next::read());
|
|
})));
|
|
match res {
|
|
Ok(_) => Ok(rx),
|
|
Err(e) => Err(format!("{:?}", e)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<H> {
|
|
fn on_request(&mut self, request: server::Request<HttpStream>) -> Next {
|
|
let status = if let FetchState::NotStarted(ref app) = self.status {
|
|
Some(match *request.method() {
|
|
// Start fetching content
|
|
Method::Get => {
|
|
trace!(target: "dapps", "Fetching dapp: {:?}", app);
|
|
let control = self.control.take().expect("on_request is called only once, thus control is always Some");
|
|
let client = self.client.as_mut().expect("on_request is called before client is closed.");
|
|
let fetch = Self::fetch_app(client, app, self.abort.clone(), control);
|
|
match fetch {
|
|
Ok(receiver) => FetchState::InProgress {
|
|
deadline: Instant::now() + Duration::from_secs(FETCH_TIMEOUT),
|
|
receiver: receiver,
|
|
},
|
|
Err(e) => FetchState::Error(ContentHandler::error(
|
|
StatusCode::BadGateway,
|
|
"Unable To Start Dapp Download",
|
|
"Could not initialize download of the dapp. It might be a problem with remote server.",
|
|
Some(&format!("{}", e)),
|
|
)),
|
|
}
|
|
},
|
|
// or return error
|
|
_ => FetchState::Error(ContentHandler::error(
|
|
StatusCode::MethodNotAllowed,
|
|
"Method Not Allowed",
|
|
"Only <code>GET</code> requests are allowed.",
|
|
None,
|
|
)),
|
|
})
|
|
} else { None };
|
|
|
|
if let Some(status) = status {
|
|
self.status = status;
|
|
}
|
|
|
|
Next::read()
|
|
}
|
|
|
|
fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
|
|
let (status, next) = match self.status {
|
|
// Request may time out
|
|
FetchState::InProgress { ref deadline, .. } if *deadline < Instant::now() => {
|
|
trace!(target: "dapps", "Fetching dapp failed because of timeout.");
|
|
let timeout = ContentHandler::error(
|
|
StatusCode::GatewayTimeout,
|
|
"Download Timeout",
|
|
&format!("Could not fetch dapp bundle within {} seconds.", FETCH_TIMEOUT),
|
|
None
|
|
);
|
|
Self::close_client(&mut self.client);
|
|
(Some(FetchState::Error(timeout)), Next::write())
|
|
},
|
|
FetchState::InProgress { ref receiver, .. } => {
|
|
// Check if there is an answer
|
|
let rec = receiver.try_recv();
|
|
match rec {
|
|
// Unpack and validate
|
|
Ok(Ok(path)) => {
|
|
trace!(target: "dapps", "Fetching dapp finished. Starting validation.");
|
|
Self::close_client(&mut self.client);
|
|
// Unpack and verify
|
|
let state = match self.dapp.validate_and_install(path.clone()) {
|
|
Err(e) => {
|
|
trace!(target: "dapps", "Error while validating dapp: {:?}", e);
|
|
FetchState::Error(ContentHandler::error(
|
|
StatusCode::BadGateway,
|
|
"Invalid Dapp",
|
|
"Downloaded bundle does not contain a valid dapp.",
|
|
Some(&format!("{:?}", e))
|
|
))
|
|
},
|
|
Ok(manifest) => FetchState::Done(manifest)
|
|
};
|
|
// Remove temporary zip file
|
|
let _ = fs::remove_file(path);
|
|
(Some(state), Next::write())
|
|
},
|
|
Ok(Err(e)) => {
|
|
warn!(target: "dapps", "Unable to fetch new dapp: {:?}", e);
|
|
let error = ContentHandler::error(
|
|
StatusCode::BadGateway,
|
|
"Download Error",
|
|
"There was an error when fetching the dapp.",
|
|
Some(&format!("{:?}", e)),
|
|
);
|
|
(Some(FetchState::Error(error)), Next::write())
|
|
},
|
|
// wait some more
|
|
_ => (None, Next::wait())
|
|
}
|
|
},
|
|
FetchState::Error(ref mut handler) => (None, handler.on_request_readable(decoder)),
|
|
_ => (None, Next::write()),
|
|
};
|
|
|
|
if let Some(status) = status {
|
|
self.status = status;
|
|
}
|
|
|
|
next
|
|
}
|
|
|
|
fn on_response(&mut self, res: &mut server::Response) -> Next {
|
|
match self.status {
|
|
FetchState::Done(ref manifest) => {
|
|
trace!(target: "dapps", "Fetching dapp finished. Redirecting to {}", manifest.id);
|
|
res.set_status(StatusCode::Found);
|
|
res.headers_mut().set(header::Location(redirection_address(self.using_dapps_domains, &manifest.id)));
|
|
Next::write()
|
|
},
|
|
FetchState::Error(ref mut handler) => handler.on_response(res),
|
|
_ => Next::end(),
|
|
}
|
|
}
|
|
|
|
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
|
|
match self.status {
|
|
FetchState::Error(ref mut handler) => handler.on_response_writable(encoder),
|
|
_ => Next::end(),
|
|
}
|
|
}
|
|
}
|
|
|