diff --git a/Cargo.lock b/Cargo.lock index 35babcec3..957ea8270 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,7 @@ dependencies = [ "num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-hash-fetch 1.6.0", + "parity-ipfs-api 1.6.0", "parity-reactor 0.1.0", "parity-rpc-client 1.4.0", "parity-updater 1.6.0", @@ -104,6 +105,11 @@ dependencies = [ "syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "base-x" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "base32" version = "0.3.1" @@ -189,6 +195,16 @@ name = "cfg-if" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "cid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "multibase 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "multihash 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "varmint 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "clippy" version = "0.0.103" @@ -1316,6 +1332,23 @@ dependencies = [ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "multibase" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base-x 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "multihash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tiny-keccak 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "nanomsg" version = "0.5.1" @@ -1572,6 +1605,19 @@ dependencies = [ "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "parity-ipfs-api" +version = "1.6.0" +dependencies = [ + "cid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ethcore 1.6.0", + "ethcore-util 1.6.0", + "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", + "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "multihash 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rlp 0.1.0", +] + [[package]] name = "parity-reactor" version = "0.1.0" @@ -1827,6 +1873,15 @@ dependencies = [ "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ring" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rlp" version = "0.1.0" @@ -2336,6 +2391,11 @@ name = "unicode-xid" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "untrusted" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "url" version = "1.2.0" @@ -2363,6 +2423,11 @@ name = "utf8-ranges" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "varmint" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "vecio" version = "0.1.0" @@ -2460,6 +2525,7 @@ dependencies = [ "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 aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a" +"checksum base-x 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2f59103b47307f76e03bef1633aec7fa9e29bfb5aa6daf5a334f94233c71f6c1" "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 bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e6e1e6fb1c9e3d6fcdec57216a74eaa03e41f52a22f13a16438251d8e88b89da" @@ -2474,6 +2540,7 @@ dependencies = [ "checksum bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27" "checksum bytes 0.4.0-dev (git+https://github.com/carllerche/bytes)" = "" "checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c" +"checksum cid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0ad0fdcfbfdfa789a0cf941dd19f7f1d3a377522f6e4c2a760d246ac56b4780" "checksum clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)" = "5b4fabf979ddf6419a313c1c0ada4a5b95cfd2049c56e8418d622d27b4b6ff32" "checksum clippy_lints 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)" = "ce96ec05bfe018a0d5d43da115e54850ea2217981ff0f2e462780ab9d594651a" "checksum cookie 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "90266f45846f14a1e986c77d1e9c2626b8c342ed806fe60241ec38cc8697b245" @@ -2541,6 +2608,8 @@ dependencies = [ "checksum mio 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "410a1a0ff76f5a226f1e4e3ff1756128e65cd30166e39c3892283e2ac09d5b67" "checksum miow 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d5bfc6782530ac8ace97af10a540054a37126b63b0702ddaaa243b73b5745b9a" "checksum msdos_time 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c04b68cc63a8480fb2550343695f7be72effdec953a9d4508161c3e69041c7d8" +"checksum multibase 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b9c35dac080fd6e16a99924c8dfdef0af89d797dd851adab25feaffacf7850d6" +"checksum multihash 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "755d5a39bee3faaf649437e873beab334990221b2faf1f2e56ca10a9e4600235" "checksum nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)" = "" "checksum nanomsg-sys 0.5.0 (git+https://github.com/ethcore/nanomsg.rs.git)" = "" "checksum native-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4e52995154bb6f0b41e4379a279482c9387c1632e3798ba4e511ef8c54ee09" @@ -2591,6 +2660,7 @@ dependencies = [ "checksum regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)" = "b4329b8928a284580a1c63ec9d846b12f6d3472317243ff7077aff11f23f2b29" "checksum regex-syntax 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "841591b1e05609a643e3b4d0045fce04f701daba7151ddcd3ad47b080693d5a9" "checksum reqwest 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bef9ed8fdfcc30947d6b774938dc0c3f369a474efe440df2c7f278180b2d2e6" +"checksum ring 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "87ac4fce2ee4bb10dd106788e90fdfa4c5a7f3f9f6aae29824db77dc57e2767d" "checksum rocksdb 0.4.5 (git+https://github.com/ethcore/rust-rocksdb)" = "" "checksum rocksdb-sys 0.3.0 (git+https://github.com/ethcore/rust-rocksdb)" = "" "checksum rotor 0.6.3 (git+https://github.com/ethcore/rotor)" = "" @@ -2652,9 +2722,11 @@ dependencies = [ "checksum unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "26643a2f83bac55f1976fb716c10234485f9202dcd65cfbdf9da49867b271172" "checksum unicode-xid 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "36dff09cafb4ec7c8cf0023eb0b686cb6ce65499116a12201c9e11840ca01beb" "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum untrusted 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "193df64312e3515fd983ded55ad5bcaa7647a035804828ed757e832ce6029ef3" "checksum url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afe9ec54bc4db14bc8744b7fed060d785ac756791450959b2248443319d5b119" "checksum user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47" "checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" +"checksum varmint 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5211976e8f86adc9920dd7621777bf8974c7812e48eb2aeb97fb1c26cd55ae84" "checksum vecio 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0795a11576d29ae80525a3fda315bf7b534f8feb9d34101e5fe63fb95bb2fd24" "checksum vergen 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "56b639f935488eb40f06d17c3e3bcc3054f6f75d264e187b1107c8d1cba8d31c" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/Cargo.toml b/Cargo.toml index e44852c74..45179ef10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ rlp = { path = "util/rlp" } rpc-cli = { path = "rpc_cli" } parity-rpc-client = { path = "rpc_client" } parity-hash-fetch = { path = "hash-fetch" } +parity-ipfs-api = { path = "ipfs" } parity-updater = { path = "updater" } parity-reactor = { path = "util/reactor" } ethcore-dapps = { path = "dapps", optional = true } diff --git a/ipfs/Cargo.toml b/ipfs/Cargo.toml new file mode 100644 index 000000000..d7698ac74 --- /dev/null +++ b/ipfs/Cargo.toml @@ -0,0 +1,15 @@ +[package] +description = "Parity IPFS-compatible API" +name = "parity-ipfs-api" +version = "1.6.0" +license = "GPL-3.0" +authors = ["Parity Technologies "] + +[dependencies] +ethcore = { path = "../ethcore" } +ethcore-util = { path = "../util" } +rlp = { path = "../util/rlp" } +mime = "0.2" +hyper = { default-features = false, git = "https://github.com/ethcore/hyper" } +cid = "0.2" +multihash = "0.5" diff --git a/ipfs/src/error.rs b/ipfs/src/error.rs new file mode 100644 index 000000000..f379f254b --- /dev/null +++ b/ipfs/src/error.rs @@ -0,0 +1,94 @@ +// 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 . + +use {multihash, cid, hyper}; +use handler::Out; + +pub type Result = ::std::result::Result; + +/// IPFS server error +#[derive(Debug)] +pub enum ServerError { + /// Wrapped `std::io::Error` + IoError(::std::io::Error), + /// Other `hyper` error + Other(hyper::error::Error), +} + +#[derive(Debug, PartialEq)] +pub enum Error { + CidParsingFailed, + UnsupportedHash, + UnsupportedCid, + BlockNotFound, + TransactionNotFound, + StateRootNotFound, + ContractNotFound, +} + +/// Convert Error into Out, handy when switching from Rust's Result-based +/// error handling to Hyper's request handling. +impl From for Out { + fn from(err: Error) -> Out { + use self::Error::*; + + match err { + UnsupportedHash => Out::Bad("Hash must be Keccak-256"), + UnsupportedCid => Out::Bad("CID codec not supported"), + CidParsingFailed => Out::Bad("CID parsing failed"), + BlockNotFound => Out::NotFound("Block not found"), + TransactionNotFound => Out::NotFound("Transaction not found"), + StateRootNotFound => Out::NotFound("State root not found"), + ContractNotFound => Out::NotFound("Contract not found"), + } + } +} + +/// Convert Content ID errors. +impl From for Error { + fn from(_: cid::Error) -> Error { + Error::CidParsingFailed + } +} + +/// Convert multihash errors (multihash being part of CID). +impl From for Error { + fn from(_: multihash::Error) -> Error { + Error::CidParsingFailed + } +} + +/// Handle IO errors (ports taken when starting the server). +impl From<::std::io::Error> for ServerError { + fn from(err: ::std::io::Error) -> ServerError { + ServerError::IoError(err) + } +} + +impl From for ServerError { + fn from(err: hyper::error::Error) -> ServerError { + ServerError::Other(err) + } +} + +impl From for String { + fn from(err: ServerError) -> String { + match err { + ServerError::IoError(err) => err.to_string(), + ServerError::Other(err) => err.to_string(), + } + } +} diff --git a/ipfs/src/handler.rs b/ipfs/src/handler.rs new file mode 100644 index 000000000..543792fa5 --- /dev/null +++ b/ipfs/src/handler.rs @@ -0,0 +1,268 @@ +// 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 . + +use {rlp, multihash}; +use error::{Error, Result}; +use cid::{ToCid, Codec}; + +use std::sync::Arc; +use multihash::Hash; +use hyper::Next; +use util::{Bytes, H256}; +use ethcore::client::{BlockId, TransactionId, BlockChainClient}; + +type Reason = &'static str; + +/// Keeps the state of the response to send out +#[derive(Debug, PartialEq)] +pub enum Out { + OctetStream(Bytes), + NotFound(Reason), + Bad(Reason), +} + +/// Request/response handler +pub struct IpfsHandler { + /// Reference to the Blockchain Client + client: Arc, + + /// Response to send out + pub out: Out, + + /// How many bytes from the response have been written + pub out_progress: usize, +} + +impl IpfsHandler { + pub fn new(client: Arc) -> Self { + IpfsHandler { + client: client, + out: Out::Bad("Invalid Request"), + out_progress: 0, + } + } + + /// Route path + query string to a specialized method + pub fn route(&mut self, path: &str, query: Option<&str>) -> Next { + self.out = match path { + "/api/v0/block/get" => { + let arg = query.and_then(|q| get_param(q, "arg")).unwrap_or(""); + + self.route_cid(arg).unwrap_or_else(Into::into) + }, + + _ => Out::NotFound("Route not found") + }; + + Next::write() + } + + /// Attempt to read Content ID from `arg` query parameter, get a hash and + /// route further by the CID's codec. + fn route_cid(&self, cid: &str) -> Result { + let cid = cid.to_cid()?; + + let mh = multihash::decode(&cid.hash)?; + + if mh.alg != Hash::Keccak256 { return Err(Error::UnsupportedHash); } + + let hash: H256 = mh.digest.into(); + + match cid.codec { + Codec::EthereumBlock => self.block(hash), + Codec::EthereumBlockList => self.block_list(hash), + Codec::EthereumTx => self.transaction(hash), + Codec::EthereumStateTrie => self.state_trie(hash), + Codec::Raw => self.contract_code(hash), + _ => return Err(Error::UnsupportedCid), + } + } + + /// Get block header by hash as raw binary. + fn block(&self, hash: H256) -> Result { + let block_id = BlockId::Hash(hash); + let block = self.client.block_header(block_id).ok_or(Error::BlockNotFound)?; + + Ok(Out::OctetStream(block.into_inner())) + } + + /// Get list of block ommers by hash as raw binary. + fn block_list(&self, hash: H256) -> Result { + let uncles = self.client.find_uncles(&hash).ok_or(Error::BlockNotFound)?; + + Ok(Out::OctetStream(rlp::encode(&uncles).to_vec())) + } + + /// Get transaction by hash and return as raw binary. + fn transaction(&self, hash: H256) -> Result { + let tx_id = TransactionId::Hash(hash); + let tx = self.client.transaction(tx_id).ok_or(Error::TransactionNotFound)?; + + Ok(Out::OctetStream(rlp::encode(&*tx).to_vec())) + } + + /// Get state trie node by hash and return as raw binary. + fn state_trie(&self, hash: H256) -> Result { + let data = self.client.state_data(&hash).ok_or(Error::StateRootNotFound)?; + + Ok(Out::OctetStream(data)) + } + + /// Get state trie node by hash and return as raw binary. + fn contract_code(&self, hash: H256) -> Result { + let data = self.client.state_data(&hash).ok_or(Error::ContractNotFound)?; + + Ok(Out::OctetStream(data)) + } +} + +/// Get a query parameter's value by name. +fn get_param<'a>(query: &'a str, name: &str) -> Option<&'a str> { + query.split('&') + .find(|part| part.starts_with(name) && part[name.len()..].starts_with("=")) + .map(|part| &part[name.len() + 1..]) +} + +#[cfg(test)] +mod tests { + use super::*; + use ethcore::client::TestBlockChainClient; + + fn get_mocked_handler() -> IpfsHandler { + IpfsHandler::new(Arc::new(TestBlockChainClient::new())) + } + + #[test] + fn test_get_param() { + let query = "foo=100&bar=200&qux=300"; + + assert_eq!(get_param(query, "foo"), Some("100")); + assert_eq!(get_param(query, "bar"), Some("200")); + assert_eq!(get_param(query, "qux"), Some("300")); + assert_eq!(get_param(query, "bar="), None); + assert_eq!(get_param(query, "200"), None); + assert_eq!(get_param("", "foo"), None); + assert_eq!(get_param("foo", "foo"), None); + assert_eq!(get_param("foo&bar", "foo"), None); + assert_eq!(get_param("bar&foo", "foo"), None); + } + + #[test] + fn cid_route_block() { + let handler = get_mocked_handler(); + + // `eth-block` with Keccak-256 + let cid = "z43AaGF5tmkT9SEX6urrhwpEW5ZSaACY73Vw357ZXTsur2fR8BM"; + + assert_eq!(Err(Error::BlockNotFound), handler.route_cid(cid)); + } + + #[test] + fn cid_route_block_list() { + let handler = get_mocked_handler(); + + // `eth-block-list` with Keccak-256 + let cid = "z43c7o7FsNxqdLJW8Ucj19tuCALtnmUb2EkDptj4W6xSkFVTqWs"; + + assert_eq!(Err(Error::BlockNotFound), handler.route_cid(cid)); + } + + #[test] + fn cid_route_tx() { + let handler = get_mocked_handler(); + + // `eth-tx` with Keccak-256 + let cid = "z44VCrqbpbPcb8SUBc8Tba4EaKuoDz2grdEoQXx4TP7WYh9ZGBu"; + + assert_eq!(Err(Error::TransactionNotFound), handler.route_cid(cid)); + } + + #[test] + fn cid_route_state_trie() { + let handler = get_mocked_handler(); + + // `eth-state-trie` with Keccak-256 + let cid = "z45oqTS7kR2n2peRGJQ4VCJEeaG9sorqcCyfmznZPJM7FMdhQCT"; + + assert_eq!(Err(Error::StateRootNotFound), handler.route_cid(&cid)); + } + + #[test] + fn cid_route_contract_code() { + let handler = get_mocked_handler(); + + // `raw` with Keccak-256 + let cid = "zb34WAp1Q5fhtLGZ3w3jhnTWaNbVV5ZZvGq4vuJQzERj6Pu3H"; + + assert_eq!(Err(Error::ContractNotFound), handler.route_cid(&cid)); + } + + #[test] + fn cid_route_invalid_hash() { + let handler = get_mocked_handler(); + + // `eth-block` with SHA3-256 hash + let cid = "z43Aa9gr1MM7TENJh4Em9d9Ttr7p3UcfyMpNei6WLVeCmSEPu8F"; + + assert_eq!(Err(Error::UnsupportedHash), handler.route_cid(cid)); + } + + #[test] + fn cid_route_invalid_codec() { + let handler = get_mocked_handler(); + + // `bitcoin-block` with Keccak-256 + let cid = "z4HFyHvb8CarYARyxz4cCcPaciduXd49TFPCKLhYmvNxf7Auvwu"; + + assert_eq!(Err(Error::UnsupportedCid), handler.route_cid(&cid)); + } + + #[test] + fn route_block() { + let mut handler = get_mocked_handler(); + + let _ = handler.route("/api/v0/block/get", Some("arg=z43AaGF5tmkT9SEX6urrhwpEW5ZSaACY73Vw357ZXTsur2fR8BM")); + + assert_eq!(handler.out, Out::NotFound("Block not found")); + } + + #[test] + fn route_block_missing_query() { + let mut handler = get_mocked_handler(); + + let _ = handler.route("/api/v0/block/get", None); + + assert_eq!(handler.out, Out::Bad("CID parsing failed")); + } + + #[test] + fn route_block_invalid_query() { + let mut handler = get_mocked_handler(); + + let _ = handler.route("/api/v0/block/get", Some("arg=foobarz43AaGF5tmkT9SEX6urrhwpEW5ZSaACY73Vw357ZXTsur2fR8BM")); + + assert_eq!(handler.out, Out::Bad("CID parsing failed")); + } + + #[test] + fn route_invalid_route() { + let mut handler = get_mocked_handler(); + + let _ = handler.route("/foo/bar/baz", Some("arg=z43AaGF5tmkT9SEX6urrhwpEW5ZSaACY73Vw357ZXTsur2fR8BM")); + + assert_eq!(handler.out, Out::NotFound("Route not found")); + } +} diff --git a/ipfs/src/lib.rs b/ipfs/src/lib.rs new file mode 100644 index 000000000..37373344a --- /dev/null +++ b/ipfs/src/lib.rs @@ -0,0 +1,205 @@ +// 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 . + +#[macro_use] +extern crate mime; +extern crate hyper; +extern crate multihash; +extern crate cid; + +extern crate rlp; +extern crate ethcore; +extern crate ethcore_util as util; + +mod error; +mod handler; + +use std::io::Write; +use std::sync::Arc; +use std::net::{SocketAddr, IpAddr, Ipv4Addr}; +use error::ServerError; +use handler::{IpfsHandler, Out}; +use hyper::server::{Listening, Handler, Request, Response}; +use hyper::net::HttpStream; +use hyper::header::{ContentLength, ContentType, Origin}; +use hyper::{Next, Encoder, Decoder, Method, RequestUri, StatusCode}; +use ethcore::client::BlockChainClient; + +/// Implement Hyper's HTTP handler +impl Handler for IpfsHandler { + fn on_request(&mut self, req: Request) -> Next { + if *req.method() != Method::Get { + return Next::write(); + } + + // Reject requests if the Origin header isn't valid + if req.headers().get::().map(|o| "127.0.0.1" != &o.host.hostname).unwrap_or(false) { + self.out = Out::Bad("Illegal Origin"); + + return Next::write(); + } + + let (path, query) = match *req.uri() { + RequestUri::AbsolutePath { ref path, ref query } => (path, query.as_ref().map(AsRef::as_ref)), + _ => return Next::write(), + }; + + self.route(path, query) + } + + fn on_request_readable(&mut self, _decoder: &mut Decoder) -> Next { + Next::write() + } + + fn on_response(&mut self, res: &mut Response) -> Next { + use Out::*; + + match self.out { + OctetStream(ref bytes) => { + use mime::{Mime, TopLevel, SubLevel}; + + // `OctetStream` is not a valid variant, so need to construct + // the type manually. + let content_type = Mime( + TopLevel::Application, + SubLevel::Ext("octet-stream".into()), + vec![] + ); + + res.headers_mut().set(ContentLength(bytes.len() as u64)); + res.headers_mut().set(ContentType(content_type)); + + Next::write() + }, + NotFound(reason) => { + res.set_status(StatusCode::NotFound); + + res.headers_mut().set(ContentLength(reason.len() as u64)); + res.headers_mut().set(ContentType(mime!(Text/Plain))); + + Next::write() + }, + Bad(reason) => { + res.set_status(StatusCode::BadRequest); + + res.headers_mut().set(ContentLength(reason.len() as u64)); + res.headers_mut().set(ContentType(mime!(Text/Plain))); + + Next::write() + } + } + } + + fn on_response_writable(&mut self, transport: &mut Encoder) -> Next { + use Out::*; + + // Get the data to write as a byte slice + let data = match self.out { + OctetStream(ref bytes) => &bytes, + NotFound(reason) | Bad(reason) => reason.as_bytes(), + }; + + write_chunk(transport, &mut self.out_progress, data) + } +} + +fn write_chunk(transport: &mut W, progress: &mut usize, data: &[u8]) -> Next { + // Skip any bytes that have already been written + let chunk = &data[*progress..]; + + // Write an get written count + let written = match transport.write(chunk) { + Ok(written) => written, + Err(_) => return Next::end(), + }; + + *progress += written; + + // Close the connection if the entire chunk has been written, otherwise increment progress + if written < chunk.len() { + Next::write() + } else { + Next::end() + } +} + +pub fn start_server(port: u16, client: Arc) -> Result { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port); + + Ok( + hyper::Server::http(&addr)? + .handle(move |_| IpfsHandler::new(client.clone())) + .map(|(listening, srv)| { + + ::std::thread::spawn(move || { + srv.run(); + }); + + listening + })? + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn write_chunk_to_vec() { + let mut transport = Vec::new(); + let mut progress = 0; + + let _ = write_chunk(&mut transport, &mut progress, b"foobar"); + + assert_eq!(b"foobar".to_vec(), transport); + assert_eq!(6, progress); + } + + #[test] + fn write_chunk_to_vec_part() { + let mut transport = Vec::new(); + let mut progress = 3; + + let _ = write_chunk(&mut transport, &mut progress, b"foobar"); + + assert_eq!(b"bar".to_vec(), transport); + assert_eq!(6, progress); + } + + #[test] + fn write_chunk_to_array() { + use std::io::Cursor; + + let mut buf = [0u8; 3]; + let mut progress = 0; + + { + let mut transport: Cursor<&mut [u8]> = Cursor::new(&mut buf); + let _ = write_chunk(&mut transport, &mut progress, b"foobar"); + } + + assert_eq!(*b"foo", buf); + assert_eq!(3, progress); + + { + let mut transport: Cursor<&mut [u8]> = Cursor::new(&mut buf); + let _ = write_chunk(&mut transport, &mut progress, b"foobar"); + } + + assert_eq!(*b"bar", buf); + assert_eq!(6, progress); + } +} diff --git a/parity/cli/config.full.toml b/parity/cli/config.full.toml index 5e6bc367a..0983bf792 100644 --- a/parity/cli/config.full.toml +++ b/parity/cli/config.full.toml @@ -67,6 +67,10 @@ path = "$HOME/.parity/dapps" user = "test_user" pass = "test_pass" +[ipfs] +enable = false +port = 5001 + [mining] author = "0xdeadbeefcafe0000000000000000000000000001" engine_signer = "0xdeadbeefcafe0000000000000000000000000001" diff --git a/parity/cli/config.toml b/parity/cli/config.toml index 608999799..288f3b2ed 100644 --- a/parity/cli/config.toml +++ b/parity/cli/config.toml @@ -38,6 +38,10 @@ port = 8080 user = "username" pass = "password" +[ipfs] +enable = false +port = 5001 + [mining] author = "0xdeadbeefcafe0000000000000000000000000001" engine_signer = "0xdeadbeefcafe0000000000000000000000000001" diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 7ac7f8c8d..a416aa4ce 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -189,6 +189,12 @@ usage! { or |c: &Config| otry!(c.dapps).pass.clone().map(Some), flag_dapps_apis_all: bool = false, or |_| None, + // IPFS + flag_ipfs_api: bool = false, + or |c: &Config| otry!(c.ipfs).enable.clone(), + flag_ipfs_api_port: u16 = 5001u16, + or |c: &Config| otry!(c.ipfs).port.clone(), + // -- Sealing/Mining Options flag_author: Option = None, or |c: &Config| otry!(c.mining).author.clone().map(Some), @@ -321,6 +327,7 @@ struct Config { rpc: Option, ipc: Option, dapps: Option, + ipfs: Option, mining: Option, footprint: Option, snapshots: Option, @@ -409,6 +416,12 @@ struct Dapps { pass: Option, } +#[derive(Default, Debug, PartialEq, RustcDecodable)] +struct Ipfs { + enable: Option, + port: Option, +} + #[derive(Default, Debug, PartialEq, RustcDecodable)] struct Mining { author: Option, @@ -482,7 +495,7 @@ struct Misc { mod tests { use super::{ Args, ArgsError, - Config, Operating, Account, Ui, Network, Rpc, Ipc, Dapps, Mining, Footprint, Snapshots, VM, Misc + Config, Operating, Account, Ui, Network, Rpc, Ipc, Dapps, Ipfs, Mining, Footprint, Snapshots, VM, Misc }; use toml; @@ -637,6 +650,10 @@ mod tests { flag_dapps_pass: Some("test_pass".into()), flag_dapps_apis_all: false, + // IPFS + flag_ipfs_api: false, + flag_ipfs_api_port: 5001u16, + // -- Sealing/Mining Options flag_author: Some("0xdeadbeefcafe0000000000000000000000000001".into()), flag_engine_signer: Some("0xdeadbeefcafe0000000000000000000000000001".into()), @@ -822,6 +839,10 @@ mod tests { user: Some("username".into()), pass: Some("password".into()) }), + ipfs: Some(Ipfs { + enable: Some(false), + port: Some(5001) + }), mining: Some(Mining { author: Some("0xdeadbeefcafe0000000000000000000000000001".into()), engine_signer: Some("0xdeadbeefcafe0000000000000000000000000001".into()), diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index 90c207378..fd19a8004 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -175,6 +175,9 @@ API and Console Options: --dapps-apis-all Expose all possible RPC APIs on Dapps port. WARNING: INSECURE. Used only for development. (default: {flag_dapps_apis_all}) + --ipfs-api Enable IPFS-compatible HTTP API. (default: {flag_ipfs_api}) + --ipfs-api-port PORT Configure on which port the IPFS HTTP API should listen. + (default: {flag_ipfs_api_port}) Sealing/Mining Options: --author ADDRESS Specify the block author (aka "coinbase") address diff --git a/parity/configuration.rs b/parity/configuration.rs index 349e57679..34fea453d 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -37,6 +37,7 @@ use params::{ResealPolicy, AccountsConfig, GasPricerConfig, MinerExtras}; use ethcore_logger::Config as LogConfig; use dir::{self, Directories, default_hypervisor_path, default_local_path, default_data_path}; use dapps::Configuration as DappsConfiguration; +use ipfs::Configuration as IpfsConfiguration; use signer::{Configuration as SignerConfiguration}; use updater::{UpdatePolicy, UpdateFilter, ReleaseTrack}; use run::RunCmd; @@ -118,6 +119,7 @@ impl Configuration { let geth_compatibility = self.args.flag_geth; let ui_address = self.ui_port().map(|port| (self.ui_interface(), port)); let dapps_conf = self.dapps_config(); + let ipfs_conf = self.ipfs_config(); let signer_conf = self.signer_config(); let format = self.format()?; @@ -342,6 +344,7 @@ impl Configuration { ui_address: ui_address, net_settings: self.network_settings(), dapps_conf: dapps_conf, + ipfs_conf: ipfs_conf, signer_conf: signer_conf, dapp: self.dapp_to_open()?, ui: self.args.cmd_ui, @@ -539,6 +542,13 @@ impl Configuration { } } + fn ipfs_config(&self) -> IpfsConfiguration { + IpfsConfiguration { + enabled: self.args.flag_ipfs_api, + port: self.args.flag_ipfs_api_port, + } + } + fn dapp_to_open(&self) -> Result, String> { if !self.args.cmd_dapp { return Ok(None); @@ -1101,6 +1111,7 @@ mod tests { ui_address: Some(("127.0.0.1".into(), 8180)), net_settings: Default::default(), dapps_conf: Default::default(), + ipfs_conf: Default::default(), signer_conf: Default::default(), ui: false, dapp: None, diff --git a/parity/ipfs.rs b/parity/ipfs.rs new file mode 100644 index 000000000..c68ace3c1 --- /dev/null +++ b/parity/ipfs.rs @@ -0,0 +1,16 @@ +pub use parity_ipfs_api::start_server; + +#[derive(Debug, PartialEq, Clone)] +pub struct Configuration { + pub enabled: bool, + pub port: u16, +} + +impl Default for Configuration { + fn default() -> Self { + Configuration { + enabled: false, + port: 5001, + } + } +} diff --git a/parity/main.rs b/parity/main.rs index b0c8f9070..bd4afca6c 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -56,6 +56,7 @@ extern crate ethcore_signer; extern crate ethcore_util as util; extern crate ethsync; extern crate parity_hash_fetch as hash_fetch; +extern crate parity_ipfs_api; extern crate parity_reactor; extern crate parity_updater as updater; extern crate rpc_cli; @@ -86,6 +87,7 @@ mod cache; mod cli; mod configuration; mod dapps; +mod ipfs; mod deprecated; mod dir; mod helpers; diff --git a/parity/run.rs b/parity/run.rs index 56cd26ea0..df9fc7384 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -47,6 +47,7 @@ use dir::Directories; use cache::CacheConfig; use user_defaults::UserDefaults; use dapps; +use ipfs; use signer; use modules; use rpc_apis; @@ -93,6 +94,7 @@ pub struct RunCmd { pub ui_address: Option<(String, u16)>, pub net_settings: NetworkSettings, pub dapps_conf: dapps::Configuration, + pub ipfs_conf: ipfs::Configuration, pub signer_conf: signer::Configuration, pub dapp: Option, pub ui: bool, @@ -420,6 +422,12 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R }; let signer_server = signer::start(cmd.signer_conf.clone(), signer_deps)?; + // the ipfs server + let ipfs_server = match cmd.ipfs_conf.enabled { + true => Some(ipfs::start_server(cmd.ipfs_conf.port, client.clone())?), + false => None, + }; + // the informant let informant = Arc::new(Informant::new( service.client(), @@ -476,7 +484,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R let restart = wait_for_exit(panic_handler, Some(updater), can_restart); // drop this stuff as soon as exit detected. - drop((http_server, ipc_server, dapps_server, signer_server, event_loop)); + drop((http_server, ipc_server, dapps_server, signer_server, ipfs_server, event_loop)); info!("Finishing work, please wait...");