// Copyright 2020 Parity Technologies (UK) Ltd. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. //! A minimal node that can interact with ipfs //! //! This node implements the gossipsub, ping and identify protocols. It supports //! the ipfs private swarms feature by reading the pre shared key file `swarm.key` //! from the IPFS_PATH environment variable or from the default location. //! //! You can pass any number of nodes to be dialed. //! //! On startup, this example will show a list of addresses that you can dial //! from a go-ipfs or js-ipfs node. //! //! You can ping this node, or use pubsub (gossipsub) on the topic "chat". For this //! to work, the ipfs node needs to be configured to use gossipsub. use async_std::io; use futures::{prelude::*, select}; use libp2p::{ core::{ either::EitherTransport, muxing::StreamMuxerBox, transport, transport::upgrade::Version, }, gossipsub::{self, Gossipsub, GossipsubConfigBuilder, GossipsubEvent, MessageAuthenticity}, identify::{Identify, IdentifyConfig, IdentifyEvent}, identity, multiaddr::Protocol, noise, ping, pnet::{PnetConfig, PreSharedKey}, swarm::{NetworkBehaviourEventProcess, SwarmEvent}, tcp::TcpConfig, yamux::YamuxConfig, Multiaddr, NetworkBehaviour, PeerId, Swarm, Transport, }; use std::{env, error::Error, fs, path::Path, str::FromStr, time::Duration}; /// Builds the transport that serves as a common ground for all connections. pub fn build_transport( key_pair: identity::Keypair, psk: Option, ) -> transport::Boxed<(PeerId, StreamMuxerBox)> { let noise_keys = noise::Keypair::::new() .into_authentic(&key_pair) .unwrap(); let noise_config = noise::NoiseConfig::xx(noise_keys).into_authenticated(); let yamux_config = YamuxConfig::default(); let base_transport = TcpConfig::new().nodelay(true); let maybe_encrypted = match psk { Some(psk) => EitherTransport::Left( base_transport.and_then(move |socket, _| PnetConfig::new(psk).handshake(socket)), ), None => EitherTransport::Right(base_transport), }; maybe_encrypted .upgrade(Version::V1) .authenticate(noise_config) .multiplex(yamux_config) .timeout(Duration::from_secs(20)) .boxed() } /// Get the current ipfs repo path, either from the IPFS_PATH environment variable or /// from the default $HOME/.ipfs fn get_ipfs_path() -> Box { env::var("IPFS_PATH") .map(|ipfs_path| Path::new(&ipfs_path).into()) .unwrap_or_else(|_| { env::var("HOME") .map(|home| Path::new(&home).join(".ipfs")) .expect("could not determine home directory") .into() }) } /// Read the pre shared key file from the given ipfs directory fn get_psk(path: Box) -> std::io::Result> { let swarm_key_file = path.join("swarm.key"); match fs::read_to_string(swarm_key_file) { Ok(text) => Ok(Some(text)), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(e) => Err(e), } } /// for a multiaddr that ends with a peer id, this strips this suffix. Rust-libp2p /// only supports dialing to an address without providing the peer id. fn strip_peer_id(addr: &mut Multiaddr) { let last = addr.pop(); match last { Some(Protocol::P2p(peer_id)) => { let mut addr = Multiaddr::empty(); addr.push(Protocol::P2p(peer_id)); println!( "removing peer id {} so this address can be dialed by rust-libp2p", addr ); } Some(other) => addr.push(other), _ => {} } } /// parse a legacy multiaddr (replace ipfs with p2p), and strip the peer id /// so it can be dialed by rust-libp2p fn parse_legacy_multiaddr(text: &str) -> Result> { let sanitized = text .split('/') .map(|part| if part == "ipfs" { "p2p" } else { part }) .collect::>() .join("/"); let mut res = Multiaddr::from_str(&sanitized)?; strip_peer_id(&mut res); Ok(res) } #[async_std::main] async fn main() -> Result<(), Box> { env_logger::init(); let ipfs_path: Box = get_ipfs_path(); println!("using IPFS_PATH {:?}", ipfs_path); let psk: Option = get_psk(ipfs_path)? .map(|text| PreSharedKey::from_str(&text)) .transpose()?; // Create a random PeerId let local_key = identity::Keypair::generate_ed25519(); let local_peer_id = PeerId::from(local_key.public()); println!("using random peer id: {:?}", local_peer_id); for psk in psk { println!("using swarm key with fingerprint: {}", psk.fingerprint()); } // Set up a an encrypted DNS-enabled TCP Transport over and Yamux protocol let transport = build_transport(local_key.clone(), psk); // Create a Gosspipsub topic let gossipsub_topic = gossipsub::IdentTopic::new("chat"); // We create a custom network behaviour that combines gossipsub, ping and identify. #[derive(NetworkBehaviour)] #[behaviour(event_process = true)] struct MyBehaviour { gossipsub: Gossipsub, identify: Identify, ping: ping::Behaviour, } impl NetworkBehaviourEventProcess for MyBehaviour { // Called when `identify` produces an event. fn inject_event(&mut self, event: IdentifyEvent) { println!("identify: {:?}", event); } } impl NetworkBehaviourEventProcess for MyBehaviour { // Called when `gossipsub` produces an event. fn inject_event(&mut self, event: GossipsubEvent) { match event { GossipsubEvent::Message { propagation_source: peer_id, message_id: id, message, } => println!( "Got message: {} with id: {} from peer: {:?}", String::from_utf8_lossy(&message.data), id, peer_id ), _ => {} } } } impl NetworkBehaviourEventProcess for MyBehaviour { // Called when `ping` produces an event. fn inject_event(&mut self, event: ping::Event) { match event { ping::Event { peer, result: Result::Ok(ping::Success::Ping { rtt }), } => { println!( "ping: rtt to {} is {} ms", peer.to_base58(), rtt.as_millis() ); } ping::Event { peer, result: Result::Ok(ping::Success::Pong), } => { println!("ping: pong from {}", peer.to_base58()); } ping::Event { peer, result: Result::Err(ping::Failure::Timeout), } => { println!("ping: timeout to {}", peer.to_base58()); } ping::Event { peer, result: Result::Err(ping::Failure::Unsupported), } => { println!("ping: {} does not support ping protocol", peer.to_base58()); } ping::Event { peer, result: Result::Err(ping::Failure::Other { error }), } => { println!("ping: ping::Failure with {}: {}", peer.to_base58(), error); } } } } // Create a Swarm to manage peers and events let mut swarm = { let gossipsub_config = GossipsubConfigBuilder::default() .max_transmit_size(262144) .build() .expect("valid config"); let mut behaviour = MyBehaviour { gossipsub: Gossipsub::new( MessageAuthenticity::Signed(local_key.clone()), gossipsub_config, ) .expect("Valid configuration"), identify: Identify::new(IdentifyConfig::new( "/ipfs/0.1.0".into(), local_key.public(), )), ping: ping::Behaviour::new(ping::Config::new()), }; println!("Subscribing to {:?}", gossipsub_topic); behaviour.gossipsub.subscribe(&gossipsub_topic).unwrap(); Swarm::new(transport, behaviour, local_peer_id) }; // Reach out to other nodes if specified for to_dial in std::env::args().skip(1) { let addr: Multiaddr = parse_legacy_multiaddr(&to_dial)?; swarm.dial(addr)?; println!("Dialed {:?}", to_dial) } // Read full lines from stdin let mut stdin = io::BufReader::new(io::stdin()).lines().fuse(); // Listen on all interfaces and whatever port the OS assigns swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; // Kick it off loop { select! { line = stdin.select_next_some() => { if let Err(e) = swarm .behaviour_mut() .gossipsub .publish(gossipsub_topic.clone(), line.expect("Stdin not to close").as_bytes()) { println!("Publish error: {:?}", e); } }, event = swarm.select_next_some() => { if let SwarmEvent::NewListenAddr { address, .. } = event { println!("Listening on {:?}", address); } } } } }