feat(webrtc): add WebRTC for WASM environments

This PR implements `Transport` for WebRTC for browsers by using web-sys. Only the `webrtc-direct` spec is implemented. The `webrtc` spec for connecting two browsers with each other is left to a future PR.

Related: https://github.com/libp2p/specs/issues/475.
Related #2617.
Supersedes: #4229.

Pull-Request: #4248.

Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
Doug A
2023-09-17 16:13:11 -03:00
committed by GitHub
parent 508cad1f0d
commit f5e644da8f
49 changed files with 2192 additions and 412 deletions

View File

@ -0,0 +1,104 @@
#![cfg(target_arch = "wasm32")]
use futures::StreamExt;
use js_sys::Date;
use libp2p::core::Multiaddr;
use libp2p::identity::{Keypair, PeerId};
use libp2p::ping;
use libp2p::swarm::{keep_alive, NetworkBehaviour, SwarmBuilder, SwarmEvent};
use std::convert::From;
use std::io;
use wasm_bindgen::prelude::*;
use web_sys::{Document, HtmlElement};
#[wasm_bindgen]
pub async fn run(libp2p_endpoint: String) -> Result<(), JsError> {
wasm_logger::init(wasm_logger::Config::default());
let body = Body::from_current_window()?;
body.append_p("Let's ping the WebRTC Server!")?;
let local_key = Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
let mut swarm = SwarmBuilder::with_wasm_executor(
libp2p_webrtc_websys::Transport::new(libp2p_webrtc_websys::Config::new(&local_key)).boxed(),
Behaviour {
ping: ping::Behaviour::new(ping::Config::new()),
keep_alive: keep_alive::Behaviour,
},
local_peer_id,
)
.build();
log::info!("Initialize swarm with identity: {local_peer_id}");
let addr = libp2p_endpoint.parse::<Multiaddr>()?;
log::info!("Dialing {addr}");
swarm.dial(addr)?;
loop {
match swarm.next().await.unwrap() {
SwarmEvent::Behaviour(BehaviourEvent::Ping(ping::Event { result: Err(e), .. })) => {
log::error!("Ping failed: {:?}", e);
break;
}
SwarmEvent::Behaviour(BehaviourEvent::Ping(ping::Event {
peer,
result: Ok(rtt),
..
})) => {
log::info!("Ping successful: RTT: {rtt:?}, from {peer}");
body.append_p(&format!("RTT: {rtt:?} at {}", Date::new_0().to_string()))?;
}
evt => log::info!("Swarm event: {:?}", evt),
}
}
Ok(())
}
#[derive(NetworkBehaviour)]
struct Behaviour {
ping: ping::Behaviour,
keep_alive: keep_alive::Behaviour,
}
/// Convenience wrapper around the current document body
struct Body {
body: HtmlElement,
document: Document,
}
impl Body {
fn from_current_window() -> Result<Self, JsError> {
// Use `web_sys`'s global `window` function to get a handle on the global
// window object.
let document = web_sys::window()
.ok_or(js_error("no global `window` exists"))?
.document()
.ok_or(js_error("should have a document on window"))?;
let body = document
.body()
.ok_or(js_error("document should have a body"))?;
Ok(Self { body, document })
}
fn append_p(&self, msg: &str) -> Result<(), JsError> {
let val = self
.document
.create_element("p")
.map_err(|_| js_error("failed to create <p>"))?;
val.set_text_content(Some(msg));
self.body
.append_child(&val)
.map_err(|_| js_error("failed to append <p>"))?;
Ok(())
}
}
fn js_error(msg: &str) -> JsError {
io::Error::new(io::ErrorKind::Other, msg).into()
}

View File

@ -0,0 +1,157 @@
#![allow(non_upper_case_globals)]
use anyhow::Result;
use axum::extract::{Path, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse};
use axum::{http::Method, routing::get, Router};
use futures::StreamExt;
use libp2p::{
core::muxing::StreamMuxerBox,
core::Transport,
identity,
multiaddr::{Multiaddr, Protocol},
ping,
swarm::{keep_alive, NetworkBehaviour, SwarmBuilder, SwarmEvent},
};
use libp2p_webrtc as webrtc;
use rand::thread_rng;
use std::net::{Ipv4Addr, SocketAddr};
use tower_http::cors::{Any, CorsLayer};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::builder()
.parse_filters("browser_webrtc_example=debug,libp2p_webrtc=info,libp2p_ping=debug")
.parse_default_env()
.init();
let id_keys = identity::Keypair::generate_ed25519();
let local_peer_id = id_keys.public().to_peer_id();
let transport = webrtc::tokio::Transport::new(
id_keys,
webrtc::tokio::Certificate::generate(&mut thread_rng())?,
)
.map(|(peer_id, conn), _| (peer_id, StreamMuxerBox::new(conn)))
.boxed();
let behaviour = Behaviour {
ping: ping::Behaviour::new(ping::Config::new()),
keep_alive: keep_alive::Behaviour,
};
let mut swarm = SwarmBuilder::with_tokio_executor(transport, behaviour, local_peer_id).build();
let address_webrtc = Multiaddr::from(Ipv4Addr::UNSPECIFIED)
.with(Protocol::Udp(0))
.with(Protocol::WebRTCDirect);
swarm.listen_on(address_webrtc.clone())?;
let address = loop {
if let SwarmEvent::NewListenAddr { address, .. } = swarm.select_next_some().await {
if address
.iter()
.any(|e| e == Protocol::Ip4(Ipv4Addr::LOCALHOST))
{
log::debug!("Ignoring localhost address to make sure the example works in Firefox");
continue;
}
log::info!("Listening on: {address}");
break address;
}
};
let addr = address.with(Protocol::P2p(*swarm.local_peer_id()));
// Serve .wasm, .js and server multiaddress over HTTP on this address.
tokio::spawn(serve(addr));
loop {
tokio::select! {
swarm_event = swarm.next() => {
log::trace!("Swarm Event: {:?}", swarm_event)
},
_ = tokio::signal::ctrl_c() => {
break;
}
}
}
Ok(())
}
#[derive(NetworkBehaviour)]
struct Behaviour {
ping: ping::Behaviour,
keep_alive: keep_alive::Behaviour,
}
#[derive(rust_embed::RustEmbed)]
#[folder = "$CARGO_MANIFEST_DIR/static"]
struct StaticFiles;
/// Serve the Multiaddr we are listening on and the host files.
pub(crate) async fn serve(libp2p_transport: Multiaddr) {
let listen_addr = match libp2p_transport.iter().next() {
Some(Protocol::Ip4(addr)) => addr,
_ => panic!("Expected 1st protocol to be IP4"),
};
let server = Router::new()
.route("/", get(get_index))
.route("/index.html", get(get_index))
.route("/:path", get(get_static_file))
.with_state(Libp2pEndpoint(libp2p_transport))
.layer(
// allow cors
CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET]),
);
let addr = SocketAddr::new(listen_addr.into(), 8080);
log::info!("Serving client files at http://{addr}");
axum::Server::bind(&addr)
.serve(server.into_make_service())
.await
.unwrap();
}
#[derive(Clone)]
struct Libp2pEndpoint(Multiaddr);
/// Serves the index.html file for our client.
///
/// Our server listens on a random UDP port for the WebRTC transport.
/// To allow the client to connect, we replace the `__LIBP2P_ENDPOINT__` placeholder with the actual address.
async fn get_index(
State(Libp2pEndpoint(libp2p_endpoint)): State<Libp2pEndpoint>,
) -> Result<Html<String>, StatusCode> {
let content = StaticFiles::get("index.html")
.ok_or(StatusCode::NOT_FOUND)?
.data;
let html = std::str::from_utf8(&content)
.expect("index.html to be valid utf8")
.replace("__LIBP2P_ENDPOINT__", &libp2p_endpoint.to_string());
Ok(Html(html))
}
/// Serves the static files generated by `wasm-pack`.
async fn get_static_file(Path(path): Path<String>) -> Result<impl IntoResponse, StatusCode> {
log::debug!("Serving static file: {path}");
let content = StaticFiles::get(&path).ok_or(StatusCode::NOT_FOUND)?.data;
let content_type = mime_guess::from_path(path)
.first_or_octet_stream()
.to_string();
Ok(([(CONTENT_TYPE, content_type)], content))
}