diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c61a98d..249b82013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Next +* feat(http): create `Profile` abstraction [#421] + +[#421]: https://github.com/rs-ipfs/rust-ipfs/pull/421 + # 0.2.1 * fix: restore_bootstrappers doesn't enable content discovery [#406] diff --git a/http/src/config.rs b/http/src/config.rs index 24abe3461..ef66800c2 100644 --- a/http/src/config.rs +++ b/http/src/config.rs @@ -1,10 +1,12 @@ //! go-ipfs compatible configuration file handling and setup. -use parity_multiaddr::Multiaddr; +use parity_multiaddr::{multiaddr, Multiaddr}; use serde::{Deserialize, Serialize}; use std::fs::{self, File}; use std::num::NonZeroU16; use std::path::Path; +use std::str::FromStr; +use structopt::StructOpt; use thiserror::Error; /// Temporary module required to de/ser config files base64'd protobuf rsa private key format. @@ -13,6 +15,25 @@ mod keys_proto { include!(concat!(env!("OUT_DIR"), "/keys_proto.rs")); } +#[derive(Debug, StructOpt)] +pub enum Profile { + Test, + Default, +} + +// Required for structopt. +impl FromStr for Profile { + type Err = InitializationError; + + fn from_str(profile: &str) -> Result { + match profile { + "test" => Ok(Profile::Test), + "default" => Ok(Profile::Default), + _ => Err(InitializationError::InvalidProfile(profile.to_string())), + } + } +} + /// The way things can go wrong when calling [`initialize`]. #[derive(Error, Debug)] pub enum InitializationError { @@ -23,7 +44,7 @@ pub enum InitializationError { #[error("invalid RSA key length given: {0}")] InvalidRsaKeyLength(u16), #[error("unsupported profiles selected: {0:?}")] - InvalidProfiles(Vec), + InvalidProfile(String), #[error("key generation failed: {0}")] KeyGeneration(Box), #[error("key encoding failed: {0}")] @@ -37,8 +58,14 @@ pub enum InitializationError { pub fn initialize( ipfs_path: &Path, bits: NonZeroU16, - profiles: Vec, + profiles: Vec, ) -> Result<(), InitializationError> { + // This check is done here to avoid an empty config file being created in the case of an + // unsupported input. + if profiles.len() != 1 { + unimplemented!("Multiple profiles are currently unsupported!"); + } + let config_path = ipfs_path.join("config"); fs::create_dir_all(&ipfs_path) @@ -52,12 +79,17 @@ pub fn initialize( fn create( config: File, bits: NonZeroU16, - profiles: Vec, + profiles: Vec, ) -> Result<(), InitializationError> { use multibase::Base::Base64Pad; use prost::Message; use std::io::BufWriter; + let api_addr = match profiles[0] { + Profile::Test => multiaddr!(Ip4([127, 0, 0, 1]), Tcp(0u16)), + Profile::Default => multiaddr!(Ip4([127, 0, 0, 1]), Tcp(4004u16)), + }; + let bits = bits.get(); if bits < 2048 || bits > 16 * 1024 { @@ -65,14 +97,6 @@ fn create( return Err(InitializationError::InvalidRsaKeyLength(bits)); } - if profiles.len() != 1 || profiles[0] != "test" { - // profiles are expected to be (comma separated) "test" as there are no bootstrap peer - // handling yet. the conformance test cases seem to init `go-ipfs` in this profile where - // it does not have any bootstrap nodes, and multi node tests later call swarm apis to - // dial the nodes together. - return Err(InitializationError::InvalidProfiles(profiles)); - } - let pk = openssl::rsa::Rsa::generate(bits as u32) .map_err(|e| InitializationError::KeyGeneration(Box::new(e)))?; @@ -118,6 +142,7 @@ fn create( }, addresses: Addresses { swarm: vec!["/ip4/127.0.0.1/tcp/0".parse().unwrap()], + api: api_addr, }, }; @@ -147,7 +172,7 @@ pub enum LoadingError { /// Returns only the keypair and listening addresses or [`LoadingError`] but this should be /// extended to contain the bootstrap nodes at least later when we need to support those for /// testing purposes. -pub fn load(config: File) -> Result<(ipfs::Keypair, Vec), LoadingError> { +pub fn load(config: File) -> Result<(ipfs::Keypair, Vec, Multiaddr), LoadingError> { use std::io::BufReader; let CompatibleConfigFile { @@ -167,7 +192,7 @@ pub fn load(config: File) -> Result<(ipfs::Keypair, Vec), LoadingErro }); } - Ok((kp, addresses.swarm)) + Ok((kp, addresses.swarm, addresses.api)) } /// Converts a PEM format to DER where PEM is a container for Base64 data with padding, starting on @@ -245,6 +270,8 @@ struct CompatibleConfigFile { #[serde(rename_all = "PascalCase")] struct Addresses { swarm: Vec, + #[serde(rename = "API")] + api: Multiaddr, } #[derive(Debug, Serialize, Deserialize)] diff --git a/http/src/main.rs b/http/src/main.rs index cc4252f35..82e4de6ff 100644 --- a/http/src/main.rs +++ b/http/src/main.rs @@ -4,6 +4,7 @@ use structopt::StructOpt; use ipfs::{Ipfs, IpfsOptions, IpfsTypes, UninitializedIpfs}; use ipfs_http::{config, v0}; +use parity_multiaddr::{Multiaddr, Protocol}; #[macro_use] extern crate tracing; @@ -16,9 +17,12 @@ enum Options { /// Generated key length #[structopt(long)] bits: NonZeroU16, - /// List of configuration profiles to apply + /// List of configuration profiles to apply. Currently only the `Test` and `Default` + /// profiles are supported. + /// + /// `Test` uses ephemeral ports (necessary for conformance tests), `Default` uses `4004`. #[structopt(long, use_delimiter = true)] - profile: Vec, + profile: Vec, }, /// Start the IPFS node in the foreground (not detaching from parent process). Daemon, @@ -59,7 +63,7 @@ fn main() { let config_path = home.join("config"); - let (keypair, listening_addrs) = match opts { + let (keypair, listening_addrs, api_listening_addr) = match opts { Options::Init { bits, profile } => { println!("initializing IPFS node at {:?}", home); @@ -73,7 +77,7 @@ fn main() { match result { Ok(_) => { - let (kp, _) = std::fs::File::open(config_path) + let (kp, _, _) = std::fs::File::open(config_path) .map_err(config::LoadingError::ConfigurationFileOpening) .and_then(config::load) .unwrap(); @@ -101,8 +105,8 @@ fn main() { eprintln!("This is a fake version of ipfs cli which does not support much"); std::process::exit(1); } - Err(config::InitializationError::InvalidProfiles(profiles)) => { - eprintln!("Error: unsupported profile selection: {:?}", profiles); + Err(config::InitializationError::InvalidProfile(profile)) => { + eprintln!("Error: unsupported profile selection: {:?}", profile); eprintln!("This is a fake version of ipfs cli which does not support much"); std::process::exit(1); } @@ -153,7 +157,8 @@ fn main() { tokio::spawn(task); let api_link_file = home.join("api"); - let (addr, server) = serve(&ipfs); + + let (addr, server) = serve(&ipfs, api_listening_addr); // shutdown future will handle signalling the exit drop(ipfs); @@ -185,9 +190,12 @@ fn main() { fn serve( ipfs: &Ipfs, + listening_addr: Multiaddr, ) -> (std::net::SocketAddr, impl std::future::Future) { + use std::net::SocketAddr; use tokio::stream::StreamExt; use warp::Filter; + let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel::<()>(1); let routes = v0::routes(ipfs, shutdown_tx); @@ -195,7 +203,17 @@ fn serve( let ipfs = ipfs.clone(); - warp::serve(routes).bind_with_graceful_shutdown(([127, 0, 0, 1], 0), async move { + let components = listening_addr.iter().collect::>(); + + let socket_addr = match components.as_slice() { + [Protocol::Ip4(ip), Protocol::Tcp(port)] => SocketAddr::new(ip.clone().into(), *port), + _ => panic!( + "Couldn't convert MultiAddr into SocketAddr: {}", + listening_addr + ), + }; + + warp::serve(routes).bind_with_graceful_shutdown(socket_addr, async move { shutdown_rx.next().await; info!("Shutdown trigger received; starting shutdown"); ipfs.exit_daemon().await;