mirror of
https://github.com/fluencelabs/rust-libp2p
synced 2025-05-28 18:21:20 +00:00
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:
parent
508cad1f0d
commit
f5e644da8f
216
Cargo.lock
generated
216
Cargo.lock
generated
@ -98,6 +98,15 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.0.2"
|
||||
@ -652,6 +661,32 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "browser-webrtc-example"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"env_logger 0.10.0",
|
||||
"futures",
|
||||
"js-sys",
|
||||
"libp2p",
|
||||
"libp2p-webrtc",
|
||||
"libp2p-webrtc-websys",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"rand 0.8.5",
|
||||
"rust-embed 6.8.1",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-logger",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bs58"
|
||||
version = "0.5.0"
|
||||
@ -661,6 +696,16 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.13.0"
|
||||
@ -1239,6 +1284,26 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.4"
|
||||
@ -1731,6 +1796,19 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
|
||||
dependencies = [
|
||||
"aho-corasick 0.7.20",
|
||||
"bstr",
|
||||
"fnv",
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.6"
|
||||
@ -2119,12 +2197,13 @@ dependencies = [
|
||||
"libp2p",
|
||||
"libp2p-mplex",
|
||||
"libp2p-webrtc",
|
||||
"libp2p-webrtc-websys",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"rand 0.8.5",
|
||||
"redis",
|
||||
"reqwest",
|
||||
"rust-embed",
|
||||
"rust-embed 8.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thirtyfour",
|
||||
@ -3089,11 +3168,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libp2p-webrtc"
|
||||
version = "0.6.0-alpha"
|
||||
version = "0.6.1-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"asynchronous-codec",
|
||||
"bytes",
|
||||
"env_logger 0.10.0",
|
||||
"futures",
|
||||
@ -3106,15 +3184,13 @@ dependencies = [
|
||||
"libp2p-noise",
|
||||
"libp2p-ping",
|
||||
"libp2p-swarm",
|
||||
"libp2p-webrtc-utils",
|
||||
"log",
|
||||
"multihash",
|
||||
"quick-protobuf",
|
||||
"quick-protobuf-codec",
|
||||
"quickcheck",
|
||||
"rand 0.8.5",
|
||||
"rcgen 0.11.1",
|
||||
"serde",
|
||||
"sha2 0.10.7",
|
||||
"stun",
|
||||
"thiserror",
|
||||
"tinytemplate",
|
||||
@ -3125,6 +3201,55 @@ dependencies = [
|
||||
"webrtc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libp2p-webrtc-utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asynchronous-codec",
|
||||
"bytes",
|
||||
"futures",
|
||||
"hex",
|
||||
"hex-literal",
|
||||
"libp2p-core",
|
||||
"libp2p-identity",
|
||||
"libp2p-noise",
|
||||
"log",
|
||||
"quick-protobuf",
|
||||
"quick-protobuf-codec",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"sha2 0.10.7",
|
||||
"thiserror",
|
||||
"tinytemplate",
|
||||
"unsigned-varint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libp2p-webrtc-websys"
|
||||
version = "0.1.0-alpha"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"getrandom 0.2.10",
|
||||
"hex",
|
||||
"hex-literal",
|
||||
"js-sys",
|
||||
"libp2p-core",
|
||||
"libp2p-identity",
|
||||
"libp2p-noise",
|
||||
"libp2p-ping",
|
||||
"libp2p-swarm",
|
||||
"libp2p-webrtc-utils",
|
||||
"log",
|
||||
"send_wrapper 0.6.0",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libp2p-websocket"
|
||||
version = "0.42.1"
|
||||
@ -3782,7 +3907,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.3.5",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
]
|
||||
@ -4321,6 +4446,15 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.3.5"
|
||||
@ -4330,13 +4464,24 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
"redox_syscall 0.2.16",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 1.0.2",
|
||||
"memchr",
|
||||
"regex-automata 0.3.8",
|
||||
"regex-syntax 0.7.5",
|
||||
@ -4357,7 +4502,7 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 1.0.2",
|
||||
"memchr",
|
||||
"regex-syntax 0.7.5",
|
||||
]
|
||||
@ -4542,14 +4687,39 @@ dependencies = [
|
||||
"webrtc-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "6.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
|
||||
dependencies = [
|
||||
"rust-embed-impl 6.8.1",
|
||||
"rust-embed-utils 7.8.1",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"rust-embed-impl 8.0.0",
|
||||
"rust-embed-utils 8.0.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "6.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils 7.8.1",
|
||||
"shellexpand",
|
||||
"syn 2.0.32",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@ -4561,11 +4731,22 @@ checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"rust-embed-utils 8.0.0",
|
||||
"syn 2.0.32",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "7.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
|
||||
dependencies = [
|
||||
"globset",
|
||||
"sha2 0.10.7",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.0.0"
|
||||
@ -4961,6 +5142,15 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
@ -5238,7 +5428,7 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand 2.0.0",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix 0.38.4",
|
||||
"windows-sys",
|
||||
]
|
||||
|
@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"core",
|
||||
"examples/autonat",
|
||||
"examples/browser-webrtc",
|
||||
"examples/chat-example",
|
||||
"examples/dcutr",
|
||||
"examples/distributed-key-value-store",
|
||||
@ -26,6 +27,7 @@ members = [
|
||||
"misc/quickcheck-ext",
|
||||
"misc/rw-stream-sink",
|
||||
"misc/server",
|
||||
"misc/webrtc-utils",
|
||||
"muxers/mplex",
|
||||
"muxers/test-harness",
|
||||
"muxers/yamux",
|
||||
@ -56,6 +58,7 @@ members = [
|
||||
"transports/uds",
|
||||
"transports/wasm-ext",
|
||||
"transports/webrtc",
|
||||
"transports/webrtc-websys",
|
||||
"transports/websocket",
|
||||
"transports/webtransport-websys",
|
||||
"wasm-tests/webtransport-tests",
|
||||
@ -102,7 +105,9 @@ libp2p-tcp = { version = "0.40.0", path = "transports/tcp" }
|
||||
libp2p-tls = { version = "0.2.1", path = "transports/tls" }
|
||||
libp2p-uds = { version = "0.39.0", path = "transports/uds" }
|
||||
libp2p-wasm-ext = { version = "0.40.0", path = "transports/wasm-ext" }
|
||||
libp2p-webrtc = { version = "0.6.0-alpha", path = "transports/webrtc" }
|
||||
libp2p-webrtc = { version = "0.6.1-alpha", path = "transports/webrtc" }
|
||||
libp2p-webrtc-utils = { version = "0.1.0", path = "misc/webrtc-utils" }
|
||||
libp2p-webrtc-websys = { version = "0.1.0-alpha", path = "transports/webrtc-websys" }
|
||||
libp2p-websocket = { version = "0.42.1", path = "transports/websocket" }
|
||||
libp2p-webtransport-websys = { version = "0.1.0", path = "transports/webtransport-websys" }
|
||||
libp2p-yamux = { version = "0.44.1", path = "muxers/yamux" }
|
||||
@ -113,7 +118,6 @@ rw-stream-sink = { version = "0.4.0", path = "misc/rw-stream-sink" }
|
||||
multiaddr = "0.18.0"
|
||||
multihash = "0.19.1"
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
# Patch away `libp2p-idnentity` in our dependency tree with the workspace version.
|
||||
|
40
examples/browser-webrtc/Cargo.toml
Normal file
40
examples/browser-webrtc/Cargo.toml
Normal file
@ -0,0 +1,40 @@
|
||||
[package]
|
||||
authors = ["Doug Anderson <douganderson444@peerpiper.io>"]
|
||||
description = "Example use of the WebRTC transport in a browser wasm environment"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "browser-webrtc-example"
|
||||
publish = false
|
||||
repository = "https://github.com/libp2p/rust-libp2p"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.1.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.72"
|
||||
env_logger = "0.10"
|
||||
futures = "0.3.28"
|
||||
log = "0.4"
|
||||
rand = "0.8"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
axum = "0.6.19"
|
||||
libp2p = { path = "../../libp2p", features = ["ed25519", "macros", "ping", "wasm-bindgen", "tokio"] }
|
||||
libp2p-webrtc = { workspace = true, features = ["tokio"] }
|
||||
rust-embed = { version = "6.8.1", features = ["include-exclude", "interpolate-folder-path"] }
|
||||
tokio = { version = "1.29", features = ["macros", "net", "rt", "signal"] }
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.4.0", features = ["cors"] }
|
||||
mime_guess = "2.0.4"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = "0.3.64"
|
||||
libp2p = { path = "../../libp2p", features = ["ed25519", "macros", "ping", "wasm-bindgen"] }
|
||||
libp2p-webrtc-websys = { workspace = true }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-futures = "0.4.37"
|
||||
wasm-logger = { version = "0.2.0" }
|
||||
web-sys = { version = "0.3", features = ['Document', 'Element', 'HtmlElement', 'Node', 'Response', 'Window'] }
|
18
examples/browser-webrtc/README.md
Normal file
18
examples/browser-webrtc/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Rust-libp2p Browser-Server WebRTC Example
|
||||
|
||||
This example demonstrates how to use the `libp2p-webrtc-websys` transport library in a browser to ping the WebRTC Server.
|
||||
It uses [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/) to build the project for use in the browser.
|
||||
|
||||
## Running the example
|
||||
|
||||
1. Build the client library:
|
||||
```shell
|
||||
wasm-pack build --target web --out-dir static
|
||||
```
|
||||
|
||||
2. Start the server:
|
||||
```shell
|
||||
cargo run
|
||||
```
|
||||
|
||||
3. Open the URL printed in the terminal
|
104
examples/browser-webrtc/src/lib.rs
Normal file
104
examples/browser-webrtc/src/lib.rs
Normal 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()
|
||||
}
|
157
examples/browser-webrtc/src/main.rs
Normal file
157
examples/browser-webrtc/src/main.rs
Normal 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))
|
||||
}
|
23
examples/browser-webrtc/static/index.html
Normal file
23
examples/browser-webrtc/static/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<link
|
||||
rel="icon"
|
||||
href="https://docs.libp2p.io/logos/libp2p_color_symbol.svg"
|
||||
sizes="any"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<h1>Rust Libp2p Demo!</h1>
|
||||
</div>
|
||||
|
||||
<script type="module" defer>
|
||||
import init, { run } from "./browser_webrtc_example.js"
|
||||
|
||||
await init();
|
||||
run("__LIBP2P_ENDPOINT__"); // This placeholder will be replaced by the server at runtime with the actual listening address.
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -34,6 +34,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
libp2p = { path = "../libp2p", features = ["ping", "macros", "webtransport-websys", "wasm-bindgen", "identify"] }
|
||||
libp2p-webrtc-websys = { workspace = true }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
wasm-logger = { version = "0.2.0" }
|
||||
|
@ -8,13 +8,9 @@ You can run this test locally by having a local Redis instance and by having
|
||||
another peer that this test can dial or listen for. For example to test that we
|
||||
can dial/listen for ourselves we can do the following:
|
||||
|
||||
1. Start redis (needed by the tests): `docker run --rm -it -p 6379:6379
|
||||
redis/redis-stack`.
|
||||
2. In one terminal run the dialer: `redis_addr=localhost:6379 ip="0.0.0.0"
|
||||
transport=quic-v1 security=quic muxer=quic is_dialer="true" cargo run --bin ping`
|
||||
3. In another terminal, run the listener: `redis_addr=localhost:6379
|
||||
ip="0.0.0.0" transport=quic-v1 security=quic muxer=quic is_dialer="false" cargo run --bin native_ping`
|
||||
|
||||
1. Start redis (needed by the tests): `docker run --rm -p 6379:6379 redis:7-alpine`.
|
||||
2. In one terminal run the dialer: `redis_addr=localhost:6379 ip="0.0.0.0" transport=quic-v1 security=quic muxer=quic is_dialer="true" cargo run --bin ping`
|
||||
3. In another terminal, run the listener: `redis_addr=localhost:6379 ip="0.0.0.0" transport=quic-v1 security=quic muxer=quic is_dialer="false" cargo run --bin native_ping`
|
||||
|
||||
To test the interop with other versions do something similar, except replace one
|
||||
of these nodes with the other version's interop test.
|
||||
@ -29,6 +25,15 @@ Firefox is not yet supported as it doesn't support all required features yet
|
||||
1. Build the wasm package: `wasm-pack build --target web`
|
||||
2. Run the dialer: `redis_addr=127.0.0.1:6379 ip=0.0.0.0 transport=webtransport is_dialer=true cargo run --bin wasm_ping`
|
||||
|
||||
# Running this test with webrtc-direct
|
||||
|
||||
To run the webrtc-direct test, you'll need the `chromedriver` in your `$PATH`, compatible with your Chrome browser.
|
||||
|
||||
1. Start redis: `docker run --rm -p 6379:6379 redis:7-alpine`.
|
||||
1. Build the wasm package: `wasm-pack build --target web`
|
||||
1. With the webrtc-direct listener `RUST_LOG=debug,webrtc=off,webrtc_sctp=off redis_addr="127.0.0.1:6379" ip="0.0.0.0" transport=webrtc-direct is_dialer="false" cargo run --bin native_ping`
|
||||
1. Run the webrtc-direct dialer: `RUST_LOG=debug,hyper=off redis_addr="127.0.0.1:6379" ip="0.0.0.0" transport=webrtc-direct is_dialer=true cargo run --bin wasm_ping`
|
||||
|
||||
# Running all interop tests locally with Compose
|
||||
|
||||
To run this test against all released libp2p versions you'll need to have the
|
||||
|
@ -1,7 +1,10 @@
|
||||
{
|
||||
"id": "chromium-rust-libp2p-head",
|
||||
"containerImageID": "chromium-rust-libp2p-head",
|
||||
"transports": [{ "name": "webtransport", "onlyDial": true }],
|
||||
"transports": [
|
||||
{ "name": "webtransport", "onlyDial": true },
|
||||
{ "name": "webrtc-direct", "onlyDial": true }
|
||||
],
|
||||
"secureChannels": [],
|
||||
"muxers": []
|
||||
}
|
||||
|
@ -159,6 +159,7 @@ pub(crate) mod wasm {
|
||||
use libp2p::identity::Keypair;
|
||||
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder};
|
||||
use libp2p::PeerId;
|
||||
use libp2p_webrtc_websys as webrtc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{BlpopRequest, Transport};
|
||||
@ -181,16 +182,19 @@ pub(crate) mod wasm {
|
||||
ip: &str,
|
||||
transport: Transport,
|
||||
) -> Result<(BoxedTransport, String)> {
|
||||
if let Transport::Webtransport = transport {
|
||||
Ok((
|
||||
match transport {
|
||||
Transport::Webtransport => Ok((
|
||||
libp2p::webtransport_websys::Transport::new(
|
||||
libp2p::webtransport_websys::Config::new(&local_key),
|
||||
)
|
||||
.boxed(),
|
||||
format!("/ip4/{ip}/udp/0/quic/webtransport"),
|
||||
))
|
||||
} else {
|
||||
bail!("Only webtransport supported with wasm")
|
||||
)),
|
||||
Transport::WebRtcDirect => Ok((
|
||||
webrtc::Transport::new(webrtc::Config::new(&local_key)).boxed(),
|
||||
format!("/ip4/{ip}/udp/0/webrtc-direct"),
|
||||
)),
|
||||
_ => bail!("Only webtransport and webrtc-direct are supported with wasm"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -50,6 +50,7 @@ pub async fn run_test(
|
||||
let mut maybe_id = None;
|
||||
|
||||
// See https://github.com/libp2p/rust-libp2p/issues/4071.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if transport == Transport::WebRtcDirect {
|
||||
maybe_id = Some(swarm.listen_on(local_addr.parse()?)?);
|
||||
}
|
||||
|
6
misc/webrtc-utils/CHANGELOG.md
Normal file
6
misc/webrtc-utils/CHANGELOG.md
Normal file
@ -0,0 +1,6 @@
|
||||
## 0.1.0 - unreleased
|
||||
|
||||
- Initial release.
|
||||
See [PR 4248].
|
||||
|
||||
[PR 4248]: https://github.com/libp2p/rust-libp2p/pull/4248
|
32
misc/webrtc-utils/Cargo.toml
Normal file
32
misc/webrtc-utils/Cargo.toml
Normal file
@ -0,0 +1,32 @@
|
||||
[package]
|
||||
authors = ["Doug Anderson <DougAnderson444@peerpiper.io>"]
|
||||
categories = ["network-programming"]
|
||||
description = "Utilities for WebRTC in libp2p"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "libp2p-webrtc-utils"
|
||||
repository = "https://github.com/libp2p/rust-libp2p"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.1.0"
|
||||
publish = false # TEMP fix for https://github.com/obi1kenobi/cargo-semver-checks-action/issues/53.
|
||||
|
||||
[dependencies]
|
||||
bytes = "1"
|
||||
futures = "0.3"
|
||||
hex = "0.4"
|
||||
libp2p-core = { workspace = true }
|
||||
libp2p-identity = { workspace = true }
|
||||
libp2p-noise = { workspace = true }
|
||||
log = "0.4.19"
|
||||
quick-protobuf = "0.8"
|
||||
quick-protobuf-codec = { workspace = true }
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha2 = "0.10.7"
|
||||
thiserror = "1"
|
||||
tinytemplate = "1.2"
|
||||
asynchronous-codec = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.4"
|
||||
unsigned-varint = { version = "0.7", features = ["asynchronous_codec"] }
|
109
misc/webrtc-utils/src/fingerprint.rs
Normal file
109
misc/webrtc-utils/src/fingerprint.rs
Normal file
@ -0,0 +1,109 @@
|
||||
// Copyright 2023 Doug Anderson.
|
||||
// Copyright 2022 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.
|
||||
|
||||
use libp2p_core::multihash;
|
||||
use sha2::Digest as _;
|
||||
use std::fmt;
|
||||
|
||||
pub const SHA256: &str = "sha-256";
|
||||
const MULTIHASH_SHA256_CODE: u64 = 0x12;
|
||||
|
||||
type Multihash = multihash::Multihash<64>;
|
||||
|
||||
/// A certificate fingerprint that is assumed to be created using the SHA256 hash algorithm.
|
||||
#[derive(Eq, PartialEq, Copy, Clone)]
|
||||
pub struct Fingerprint([u8; 32]);
|
||||
|
||||
impl Fingerprint {
|
||||
pub const FF: Fingerprint = Fingerprint([0xFF; 32]);
|
||||
|
||||
pub const fn raw(digest: [u8; 32]) -> Self {
|
||||
Fingerprint(digest)
|
||||
}
|
||||
|
||||
/// Creates a new [Fingerprint] from a raw certificate by hashing the given bytes with SHA256.
|
||||
pub fn from_certificate(bytes: &[u8]) -> Self {
|
||||
Fingerprint(sha2::Sha256::digest(bytes).into())
|
||||
}
|
||||
|
||||
/// Converts [`Multihash`](multihash::Multihash) to [`Fingerprint`].
|
||||
pub fn try_from_multihash(hash: Multihash) -> Option<Self> {
|
||||
if hash.code() != MULTIHASH_SHA256_CODE {
|
||||
// Only support SHA256 for now.
|
||||
return None;
|
||||
}
|
||||
|
||||
let bytes = hash.digest().try_into().ok()?;
|
||||
|
||||
Some(Self(bytes))
|
||||
}
|
||||
|
||||
/// Converts this fingerprint to [`Multihash`](multihash::Multihash).
|
||||
pub fn to_multihash(self) -> Multihash {
|
||||
Multihash::wrap(MULTIHASH_SHA256_CODE, &self.0).expect("fingerprint's len to be 32 bytes")
|
||||
}
|
||||
|
||||
/// Formats this fingerprint as uppercase hex, separated by colons (`:`).
|
||||
///
|
||||
/// This is the format described in <https://www.rfc-editor.org/rfc/rfc4572#section-5>.
|
||||
pub fn to_sdp_format(self) -> String {
|
||||
self.0.map(|byte| format!("{byte:02X}")).join(":")
|
||||
}
|
||||
|
||||
/// Returns the algorithm used (e.g. "sha-256").
|
||||
/// See <https://datatracker.ietf.org/doc/html/rfc8122#section-5>
|
||||
pub fn algorithm(&self) -> String {
|
||||
SHA256.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Fingerprint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&hex::encode(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SDP_FORMAT: &str = "7D:E3:D8:3F:81:A6:80:59:2A:47:1E:6B:6A:BB:07:47:AB:D3:53:85:A8:09:3F:DF:E1:12:C1:EE:BB:6C:C6:AC";
|
||||
const REGULAR_FORMAT: [u8; 32] =
|
||||
hex_literal::hex!("7DE3D83F81A680592A471E6B6ABB0747ABD35385A8093FDFE112C1EEBB6CC6AC");
|
||||
|
||||
#[test]
|
||||
fn sdp_format() {
|
||||
let fp = Fingerprint::raw(REGULAR_FORMAT);
|
||||
|
||||
let formatted = fp.to_sdp_format();
|
||||
|
||||
assert_eq!(formatted, SDP_FORMAT)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_sdp() {
|
||||
let mut bytes = [0; 32];
|
||||
bytes.copy_from_slice(&hex::decode(SDP_FORMAT.replace(':', "")).unwrap());
|
||||
|
||||
let fp = Fingerprint::raw(bytes);
|
||||
assert_eq!(fp, Fingerprint::raw(REGULAR_FORMAT));
|
||||
}
|
||||
}
|
15
misc/webrtc-utils/src/lib.rs
Normal file
15
misc/webrtc-utils/src/lib.rs
Normal file
@ -0,0 +1,15 @@
|
||||
mod proto {
|
||||
#![allow(unreachable_pub)]
|
||||
include!("generated/mod.rs");
|
||||
pub use self::webrtc::pb::{mod_Message::Flag, Message};
|
||||
}
|
||||
|
||||
mod fingerprint;
|
||||
pub mod noise;
|
||||
pub mod sdp;
|
||||
mod stream;
|
||||
mod transport;
|
||||
|
||||
pub use fingerprint::{Fingerprint, SHA256};
|
||||
pub use stream::{DropListener, Stream, MAX_MSG_LEN};
|
||||
pub use transport::parse_webrtc_dial_addr;
|
@ -24,15 +24,14 @@ use libp2p_identity as identity;
|
||||
use libp2p_identity::PeerId;
|
||||
use libp2p_noise as noise;
|
||||
|
||||
use crate::tokio::fingerprint::Fingerprint;
|
||||
use crate::tokio::Error;
|
||||
use crate::fingerprint::Fingerprint;
|
||||
|
||||
pub(crate) async fn inbound<T>(
|
||||
pub async fn inbound<T>(
|
||||
id_keys: identity::Keypair,
|
||||
stream: T,
|
||||
client_fingerprint: Fingerprint,
|
||||
server_fingerprint: Fingerprint,
|
||||
) -> Result<PeerId, Error>
|
||||
) -> Result<PeerId, libp2p_noise::Error>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
@ -49,12 +48,12 @@ where
|
||||
Ok(peer_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn outbound<T>(
|
||||
pub async fn outbound<T>(
|
||||
id_keys: identity::Keypair,
|
||||
stream: T,
|
||||
server_fingerprint: Fingerprint,
|
||||
client_fingerprint: Fingerprint,
|
||||
) -> Result<PeerId, Error>
|
||||
) -> Result<PeerId, libp2p_noise::Error>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
157
misc/webrtc-utils/src/sdp.rs
Normal file
157
misc/webrtc-utils/src/sdp.rs
Normal file
@ -0,0 +1,157 @@
|
||||
// Copyright 2023 Doug Anderson
|
||||
// Copyright 2022 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.
|
||||
use crate::fingerprint::Fingerprint;
|
||||
use serde::Serialize;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use tinytemplate::TinyTemplate;
|
||||
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
pub fn answer(addr: SocketAddr, server_fingerprint: Fingerprint, client_ufrag: &str) -> String {
|
||||
let answer = render_description(
|
||||
SERVER_SESSION_DESCRIPTION,
|
||||
addr,
|
||||
server_fingerprint,
|
||||
client_ufrag,
|
||||
);
|
||||
|
||||
log::trace!("Created SDP answer: {answer}");
|
||||
|
||||
answer
|
||||
}
|
||||
|
||||
// See [`CLIENT_SESSION_DESCRIPTION`].
|
||||
//
|
||||
// a=ice-lite
|
||||
//
|
||||
// A lite implementation is only appropriate for devices that will *always* be connected to
|
||||
// the public Internet and have a public IP address at which it can receive packets from any
|
||||
// correspondent. ICE will not function when a lite implementation is placed behind a NAT
|
||||
// (RFC8445).
|
||||
//
|
||||
// a=tls-id:<id>
|
||||
//
|
||||
// "TLS ID" uniquely identifies a TLS association.
|
||||
// The ICE protocol uses a "TLS ID" system to indicate whether a fresh DTLS connection
|
||||
// must be reopened in case of ICE renegotiation. Considering that ICE renegotiations
|
||||
// never happen in our use case, we can simply put a random value and not care about
|
||||
// it. Note however that the TLS ID in the answer must be present if and only if the
|
||||
// offer contains one. (RFC8842)
|
||||
// TODO: is it true that renegotiations never happen? what about a connection closing?
|
||||
// "tls-id" attribute MUST be present in the initial offer and respective answer (RFC8839).
|
||||
// XXX: but right now browsers don't send it.
|
||||
//
|
||||
// a=setup:passive
|
||||
//
|
||||
// "passive" indicates that the remote DTLS server will only listen for incoming
|
||||
// connections. (RFC5763)
|
||||
// The answerer (server) MUST not be located behind a NAT (RFC6135).
|
||||
//
|
||||
// The answerer MUST use either a setup attribute value of setup:active or setup:passive.
|
||||
// Note that if the answerer uses setup:passive, then the DTLS handshake will not begin until
|
||||
// the answerer is received, which adds additional latency. setup:active allows the answer and
|
||||
// the DTLS handshake to occur in parallel. Thus, setup:active is RECOMMENDED.
|
||||
//
|
||||
// a=candidate:<foundation> <component-id> <transport> <priority> <connection-address> <port> <cand-type>
|
||||
//
|
||||
// A transport address for a candidate that can be used for connectivity checks (RFC8839).
|
||||
//
|
||||
// a=end-of-candidates
|
||||
const SERVER_SESSION_DESCRIPTION: &str = "v=0
|
||||
o=- 0 0 IN {ip_version} {target_ip}
|
||||
s=-
|
||||
t=0 0
|
||||
a=ice-lite
|
||||
m=application {target_port} UDP/DTLS/SCTP webrtc-datachannel
|
||||
c=IN {ip_version} {target_ip}
|
||||
a=mid:0
|
||||
a=ice-options:ice2
|
||||
a=ice-ufrag:{ufrag}
|
||||
a=ice-pwd:{pwd}
|
||||
a=fingerprint:{fingerprint_algorithm} {fingerprint_value}
|
||||
a=setup:passive
|
||||
a=sctp-port:5000
|
||||
a=max-message-size:16384
|
||||
a=candidate:1467250027 1 UDP 1467250027 {target_ip} {target_port} typ host
|
||||
a=end-of-candidates
|
||||
";
|
||||
|
||||
/// Indicates the IP version used in WebRTC: `IP4` or `IP6`.
|
||||
#[derive(Serialize)]
|
||||
enum IpVersion {
|
||||
IP4,
|
||||
IP6,
|
||||
}
|
||||
|
||||
/// Context passed to the templating engine, which replaces the above placeholders (e.g.
|
||||
/// `{IP_VERSION}`) with real values.
|
||||
#[derive(Serialize)]
|
||||
struct DescriptionContext {
|
||||
pub(crate) ip_version: IpVersion,
|
||||
pub(crate) target_ip: IpAddr,
|
||||
pub(crate) target_port: u16,
|
||||
pub(crate) fingerprint_algorithm: String,
|
||||
pub(crate) fingerprint_value: String,
|
||||
pub(crate) ufrag: String,
|
||||
pub(crate) pwd: String,
|
||||
}
|
||||
|
||||
/// Renders a [`TinyTemplate`] description using the provided arguments.
|
||||
pub fn render_description(
|
||||
description: &str,
|
||||
addr: SocketAddr,
|
||||
fingerprint: Fingerprint,
|
||||
ufrag: &str,
|
||||
) -> String {
|
||||
let mut tt = TinyTemplate::new();
|
||||
tt.add_template("description", description).unwrap();
|
||||
|
||||
let context = DescriptionContext {
|
||||
ip_version: {
|
||||
if addr.is_ipv4() {
|
||||
IpVersion::IP4
|
||||
} else {
|
||||
IpVersion::IP6
|
||||
}
|
||||
},
|
||||
target_ip: addr.ip(),
|
||||
target_port: addr.port(),
|
||||
fingerprint_algorithm: fingerprint.algorithm(),
|
||||
fingerprint_value: fingerprint.to_sdp_format(),
|
||||
// NOTE: ufrag is equal to pwd.
|
||||
ufrag: ufrag.to_owned(),
|
||||
pwd: ufrag.to_owned(),
|
||||
};
|
||||
tt.render("description", &context).unwrap()
|
||||
}
|
||||
|
||||
/// Generates a random ufrag and adds a prefix according to the spec.
|
||||
pub fn random_ufrag() -> String {
|
||||
format!(
|
||||
"libp2p+webrtc+v1/{}",
|
||||
thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(64)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
)
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
// Copyright 2022 Parity Technologies (UK) Ltd.
|
||||
// Copyright 2023 Protocol Labs.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
@ -18,24 +19,20 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
use asynchronous_codec::Framed;
|
||||
use bytes::Bytes;
|
||||
use futures::{channel::oneshot, prelude::*, ready};
|
||||
use tokio_util::compat::Compat;
|
||||
use webrtc::data::data_channel::{DataChannel, PollDataChannel};
|
||||
|
||||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use crate::proto::{Flag, Message};
|
||||
use crate::tokio::{
|
||||
substream::drop_listener::GracefullyClosed,
|
||||
substream::framed_dc::FramedDc,
|
||||
substream::state::{Closing, State},
|
||||
use crate::{
|
||||
stream::drop_listener::GracefullyClosed,
|
||||
stream::framed_dc::FramedDc,
|
||||
stream::state::{Closing, State},
|
||||
};
|
||||
|
||||
mod drop_listener;
|
||||
@ -47,7 +44,7 @@ mod state;
|
||||
/// "As long as message interleaving is not supported, the sender SHOULD limit the maximum message
|
||||
/// size to 16 KB to avoid monopolization."
|
||||
/// Source: <https://www.rfc-editor.org/rfc/rfc8831#name-transferring-user-data-on-a>
|
||||
const MAX_MSG_LEN: usize = 16384; // 16kiB
|
||||
pub const MAX_MSG_LEN: usize = 16 * 1024;
|
||||
/// Length of varint, in bytes.
|
||||
const VARINT_LEN: usize = 2;
|
||||
/// Overhead of the protobuf encoding, in bytes.
|
||||
@ -55,26 +52,28 @@ const PROTO_OVERHEAD: usize = 5;
|
||||
/// Maximum length of data, in bytes.
|
||||
const MAX_DATA_LEN: usize = MAX_MSG_LEN - VARINT_LEN - PROTO_OVERHEAD;
|
||||
|
||||
pub(crate) use drop_listener::DropListener;
|
||||
/// A substream on top of a WebRTC data channel.
|
||||
pub use drop_listener::DropListener;
|
||||
/// A stream backed by a WebRTC data channel.
|
||||
///
|
||||
/// To be a proper libp2p substream, we need to implement [`AsyncRead`] and [`AsyncWrite`] as well
|
||||
/// To be a proper libp2p stream, we need to implement [`AsyncRead`] and [`AsyncWrite`] as well
|
||||
/// as support a half-closed state which we do by framing messages in a protobuf envelope.
|
||||
pub struct Substream {
|
||||
io: FramedDc,
|
||||
pub struct Stream<T> {
|
||||
io: FramedDc<T>,
|
||||
state: State,
|
||||
read_buffer: Bytes,
|
||||
/// Dropping this will close the oneshot and notify the receiver by emitting `Canceled`.
|
||||
drop_notifier: Option<oneshot::Sender<GracefullyClosed>>,
|
||||
}
|
||||
|
||||
impl Substream {
|
||||
/// Returns a new `Substream` and a listener, which will notify the receiver when/if the substream
|
||||
/// is dropped.
|
||||
pub(crate) fn new(data_channel: Arc<DataChannel>) -> (Self, DropListener) {
|
||||
impl<T> Stream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + Clone,
|
||||
{
|
||||
/// Returns a new [`Stream`] and a [`DropListener`], which will notify the receiver when/if the stream is dropped.
|
||||
pub fn new(data_channel: T) -> (Self, DropListener<T>) {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
let substream = Self {
|
||||
let stream = Self {
|
||||
io: framed_dc::new(data_channel.clone()),
|
||||
state: State::Open,
|
||||
read_buffer: Bytes::default(),
|
||||
@ -82,10 +81,10 @@ impl Substream {
|
||||
};
|
||||
let listener = DropListener::new(framed_dc::new(data_channel), receiver);
|
||||
|
||||
(substream, listener)
|
||||
(stream, listener)
|
||||
}
|
||||
|
||||
/// Gracefully closes the "read-half" of the substream.
|
||||
/// Gracefully closes the "read-half" of the stream.
|
||||
pub fn poll_close_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
loop {
|
||||
match self.state.close_read_barrier()? {
|
||||
@ -113,7 +112,10 @@ impl Substream {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Substream {
|
||||
impl<T> AsyncRead for Stream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
@ -157,7 +159,10 @@ impl AsyncRead for Substream {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Substream {
|
||||
impl<T> AsyncWrite for Stream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
@ -236,10 +241,13 @@ impl AsyncWrite for Substream {
|
||||
}
|
||||
}
|
||||
|
||||
fn io_poll_next(
|
||||
io: &mut Framed<Compat<PollDataChannel>, quick_protobuf_codec::Codec<Message>>,
|
||||
fn io_poll_next<T>(
|
||||
io: &mut FramedDc<T>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<io::Result<Option<(Option<Flag>, Option<Vec<u8>>)>>> {
|
||||
) -> Poll<io::Result<Option<(Option<Flag>, Option<Vec<u8>>)>>>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
match ready!(io.poll_next_unpin(cx))
|
||||
.transpose()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
||||
@ -262,8 +270,8 @@ mod tests {
|
||||
// Largest possible message.
|
||||
let message = [0; MAX_DATA_LEN];
|
||||
|
||||
let protobuf = crate::proto::Message {
|
||||
flag: Some(crate::proto::Flag::FIN),
|
||||
let protobuf = Message {
|
||||
flag: Some(Flag::FIN),
|
||||
message: Some(message.to_vec()),
|
||||
};
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use futures::channel::oneshot::Canceled;
|
||||
use futures::{FutureExt, SinkExt};
|
||||
use futures::{AsyncRead, AsyncWrite, FutureExt, SinkExt};
|
||||
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
@ -28,46 +28,42 @@ use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use crate::proto::{Flag, Message};
|
||||
use crate::tokio::substream::framed_dc::FramedDc;
|
||||
use crate::stream::framed_dc::FramedDc;
|
||||
|
||||
#[must_use]
|
||||
pub(crate) struct DropListener {
|
||||
state: State,
|
||||
pub struct DropListener<T> {
|
||||
state: State<T>,
|
||||
}
|
||||
|
||||
impl DropListener {
|
||||
pub(crate) fn new(stream: FramedDc, receiver: oneshot::Receiver<GracefullyClosed>) -> Self {
|
||||
let substream_id = stream.get_ref().stream_identifier();
|
||||
|
||||
impl<T> DropListener<T> {
|
||||
pub fn new(stream: FramedDc<T>, receiver: oneshot::Receiver<GracefullyClosed>) -> Self {
|
||||
Self {
|
||||
state: State::Idle {
|
||||
stream,
|
||||
receiver,
|
||||
substream_id,
|
||||
},
|
||||
state: State::Idle { stream, receiver },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum State {
|
||||
enum State<T> {
|
||||
/// The [`DropListener`] is idle and waiting to be activated.
|
||||
Idle {
|
||||
stream: FramedDc,
|
||||
stream: FramedDc<T>,
|
||||
receiver: oneshot::Receiver<GracefullyClosed>,
|
||||
substream_id: u16,
|
||||
},
|
||||
/// The stream got dropped and we are sending a reset flag.
|
||||
SendingReset {
|
||||
stream: FramedDc,
|
||||
stream: FramedDc<T>,
|
||||
},
|
||||
Flushing {
|
||||
stream: FramedDc,
|
||||
stream: FramedDc<T>,
|
||||
},
|
||||
/// Bad state transition.
|
||||
Poisoned,
|
||||
}
|
||||
|
||||
impl Future for DropListener {
|
||||
impl<T> Future for DropListener<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Output = io::Result<()>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
@ -77,23 +73,18 @@ impl Future for DropListener {
|
||||
match std::mem::replace(state, State::Poisoned) {
|
||||
State::Idle {
|
||||
stream,
|
||||
substream_id,
|
||||
mut receiver,
|
||||
} => match receiver.poll_unpin(cx) {
|
||||
Poll::Ready(Ok(GracefullyClosed {})) => {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Poll::Ready(Err(Canceled)) => {
|
||||
log::info!("Substream {substream_id} dropped without graceful close, sending Reset");
|
||||
log::info!("Stream dropped without graceful close, sending Reset");
|
||||
*state = State::SendingReset { stream };
|
||||
continue;
|
||||
}
|
||||
Poll::Pending => {
|
||||
*state = State::Idle {
|
||||
stream,
|
||||
substream_id,
|
||||
receiver,
|
||||
};
|
||||
*state = State::Idle { stream, receiver };
|
||||
return Poll::Pending;
|
||||
}
|
||||
},
|
||||
@ -126,5 +117,5 @@ impl Future for DropListener {
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates that our substream got gracefully closed.
|
||||
pub(crate) struct GracefullyClosed {}
|
||||
/// Indicates that our stream got gracefully closed.
|
||||
pub struct GracefullyClosed {}
|
@ -19,22 +19,18 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
use asynchronous_codec::Framed;
|
||||
use tokio_util::compat::Compat;
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
use webrtc::data::data_channel::{DataChannel, PollDataChannel};
|
||||
use futures::{AsyncRead, AsyncWrite};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{MAX_DATA_LEN, MAX_MSG_LEN, VARINT_LEN};
|
||||
use crate::proto::Message;
|
||||
use crate::stream::{MAX_DATA_LEN, MAX_MSG_LEN, VARINT_LEN};
|
||||
|
||||
pub(crate) type FramedDc = Framed<Compat<PollDataChannel>, quick_protobuf_codec::Codec<Message>>;
|
||||
pub(crate) fn new(data_channel: Arc<DataChannel>) -> FramedDc {
|
||||
let mut inner = PollDataChannel::new(data_channel);
|
||||
inner.set_read_buf_capacity(MAX_MSG_LEN);
|
||||
|
||||
pub(crate) type FramedDc<T> = Framed<T, quick_protobuf_codec::Codec<Message>>;
|
||||
pub(crate) fn new<T>(inner: T) -> FramedDc<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
{
|
||||
let mut framed = Framed::new(
|
||||
inner.compat(),
|
||||
inner,
|
||||
quick_protobuf_codec::Codec::new(MAX_MSG_LEN - VARINT_LEN),
|
||||
);
|
||||
// If not set, `Framed` buffers up to 131kB of data before sending, which leads to "outbound
|
@ -277,7 +277,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// Acts as a "barrier" for [`Substream::poll_close_read`](super::Substream::poll_close_read).
|
||||
/// Acts as a "barrier" for [`Stream::poll_close_read`](super::Stream::poll_close_read).
|
||||
pub(crate) fn close_read_barrier(&mut self) -> io::Result<Option<Closing>> {
|
||||
loop {
|
||||
match self {
|
101
misc/webrtc-utils/src/transport.rs
Normal file
101
misc/webrtc-utils/src/transport.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use crate::fingerprint::Fingerprint;
|
||||
use libp2p_core::{multiaddr::Protocol, Multiaddr};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
/// Parse the given [`Multiaddr`] into a [`SocketAddr`] and a [`Fingerprint`] for dialing.
|
||||
pub fn parse_webrtc_dial_addr(addr: &Multiaddr) -> Option<(SocketAddr, Fingerprint)> {
|
||||
let mut iter = addr.iter();
|
||||
|
||||
let ip = match iter.next()? {
|
||||
Protocol::Ip4(ip) => IpAddr::from(ip),
|
||||
Protocol::Ip6(ip) => IpAddr::from(ip),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let port = iter.next()?;
|
||||
let webrtc = iter.next()?;
|
||||
let certhash = iter.next()?;
|
||||
|
||||
let (port, fingerprint) = match (port, webrtc, certhash) {
|
||||
(Protocol::Udp(port), Protocol::WebRTCDirect, Protocol::Certhash(cert_hash)) => {
|
||||
let fingerprint = Fingerprint::try_from_multihash(cert_hash)?;
|
||||
|
||||
(port, fingerprint)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
match iter.next() {
|
||||
Some(Protocol::P2p(_)) => {}
|
||||
// peer ID is optional
|
||||
None => {}
|
||||
// unexpected protocol
|
||||
Some(_) => return None,
|
||||
}
|
||||
|
||||
Some((SocketAddr::new(ip, port), fingerprint))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
#[test]
|
||||
fn parse_valid_address_with_certhash_and_p2p() {
|
||||
let addr = "/ip4/127.0.0.1/udp/39901/webrtc-direct/certhash/uEiDikp5KVUgkLta1EjUN-IKbHk-dUBg8VzKgf5nXxLK46w/p2p/12D3KooWNpDk9w6WrEEcdsEH1y47W71S36yFjw4sd3j7omzgCSMS"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let maybe_parsed = parse_webrtc_dial_addr(&addr);
|
||||
|
||||
assert_eq!(
|
||||
maybe_parsed,
|
||||
Some((
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 39901),
|
||||
Fingerprint::raw(hex_literal::hex!(
|
||||
"e2929e4a5548242ed6b512350df8829b1e4f9d50183c5732a07f99d7c4b2b8eb"
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_id_is_not_required() {
|
||||
let addr = "/ip4/127.0.0.1/udp/39901/webrtc-direct/certhash/uEiDikp5KVUgkLta1EjUN-IKbHk-dUBg8VzKgf5nXxLK46w"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let maybe_parsed = parse_webrtc_dial_addr(&addr);
|
||||
|
||||
assert_eq!(
|
||||
maybe_parsed,
|
||||
Some((
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 39901),
|
||||
Fingerprint::raw(hex_literal::hex!(
|
||||
"e2929e4a5548242ed6b512350df8829b1e4f9d50183c5732a07f99d7c4b2b8eb"
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ipv6() {
|
||||
let addr =
|
||||
"/ip6/::1/udp/12345/webrtc-direct/certhash/uEiDikp5KVUgkLta1EjUN-IKbHk-dUBg8VzKgf5nXxLK46w/p2p/12D3KooWNpDk9w6WrEEcdsEH1y47W71S36yFjw4sd3j7omzgCSMS"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let maybe_parsed = parse_webrtc_dial_addr(&addr);
|
||||
|
||||
assert_eq!(
|
||||
maybe_parsed,
|
||||
Some((
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 12345),
|
||||
Fingerprint::raw(hex_literal::hex!(
|
||||
"e2929e4a5548242ed6b512350df8829b1e4f9d50183c5732a07f99d7c4b2b8eb"
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
6
transports/webrtc-websys/CHANGELOG.md
Normal file
6
transports/webrtc-websys/CHANGELOG.md
Normal file
@ -0,0 +1,6 @@
|
||||
## 0.1.0-alpha - unreleased
|
||||
|
||||
- Initial alpha release.
|
||||
See [PR 4248].
|
||||
|
||||
[PR 4248]: https://github.com/libp2p/rust-libp2p/pull/4248
|
36
transports/webrtc-websys/Cargo.toml
Normal file
36
transports/webrtc-websys/Cargo.toml
Normal file
@ -0,0 +1,36 @@
|
||||
[package]
|
||||
authors = ["Doug Anderson <DougAnderson444@peerpiper.io>"]
|
||||
categories = ["asynchronous", "network-programming", "wasm", "web-programming"]
|
||||
description = "WebRTC for libp2p under WASM environment"
|
||||
edition = "2021"
|
||||
keywords = ["libp2p", "networking", "peer-to-peer"]
|
||||
license = "MIT"
|
||||
name = "libp2p-webrtc-websys"
|
||||
repository = "https://github.com/libp2p/rust-libp2p"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.1.0-alpha"
|
||||
publish = false # TEMP fix for https://github.com/obi1kenobi/cargo-semver-checks-action/issues/53.
|
||||
|
||||
[dependencies]
|
||||
bytes = "1"
|
||||
futures = "0.3"
|
||||
futures-timer = "3"
|
||||
getrandom = { version = "0.2.9", features = ["js"] }
|
||||
hex = "0.4.3"
|
||||
js-sys = { version = "0.3" }
|
||||
libp2p-core = { workspace = true }
|
||||
libp2p-identity = { workspace = true }
|
||||
libp2p-noise = { workspace = true }
|
||||
libp2p-webrtc-utils = { workspace = true }
|
||||
log = "0.4.19"
|
||||
send_wrapper = { version = "0.6.0", features = ["futures"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
wasm-bindgen = { version = "0.2.87" }
|
||||
wasm-bindgen-futures = { version = "0.4.37" }
|
||||
web-sys = { version = "0.3.64", features = ["Document", "Location", "MessageEvent", "Navigator", "RtcCertificate", "RtcConfiguration", "RtcDataChannel", "RtcDataChannelEvent", "RtcDataChannelInit", "RtcDataChannelState", "RtcDataChannelType", "RtcPeerConnection", "RtcSdpType", "RtcSessionDescription", "RtcSessionDescriptionInit", "Window"] }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.4"
|
||||
libp2p-ping = { workspace = true }
|
||||
libp2p-swarm = { workspace = true, features = ["wasm-bindgen"] }
|
9
transports/webrtc-websys/README.md
Normal file
9
transports/webrtc-websys/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Rust `libp2p-webrtc-websys`
|
||||
|
||||
Browser Transport made available through `web-sys` bindings.
|
||||
|
||||
## Usage
|
||||
|
||||
Use with `Swarm::with_wasm_executor` to enable the `wasm-bindgen` executor for the `Swarm`.
|
||||
|
||||
See the [browser-webrtc](../../examples/browser-webrtc) example for a full example.
|
308
transports/webrtc-websys/src/connection.rs
Normal file
308
transports/webrtc-websys/src/connection.rs
Normal file
@ -0,0 +1,308 @@
|
||||
//! A libp2p connection backed by an [RtcPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection).
|
||||
|
||||
use super::{Error, Stream};
|
||||
use crate::stream::DropListener;
|
||||
use futures::channel::mpsc;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use js_sys::{Object, Reflect};
|
||||
use libp2p_core::muxing::{StreamMuxer, StreamMuxerEvent};
|
||||
use libp2p_webrtc_utils::Fingerprint;
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::pin::Pin;
|
||||
use std::task::Waker;
|
||||
use std::task::{ready, Context, Poll};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{
|
||||
RtcConfiguration, RtcDataChannel, RtcDataChannelEvent, RtcDataChannelInit, RtcDataChannelType,
|
||||
RtcSessionDescriptionInit,
|
||||
};
|
||||
|
||||
/// A WebRTC Connection.
|
||||
///
|
||||
/// All connections need to be [`Send`] which is why some fields are wrapped in [`SendWrapper`].
|
||||
/// This is safe because WASM is single-threaded.
|
||||
pub struct Connection {
|
||||
/// The [RtcPeerConnection] that is used for the WebRTC Connection
|
||||
inner: SendWrapper<RtcPeerConnection>,
|
||||
|
||||
/// Whether the connection is closed
|
||||
closed: bool,
|
||||
/// An [`mpsc::channel`] for all inbound data channels.
|
||||
///
|
||||
/// Because the browser's WebRTC API is event-based, we need to use a channel to obtain all inbound data channels.
|
||||
inbound_data_channels: SendWrapper<mpsc::Receiver<RtcDataChannel>>,
|
||||
/// A list of futures, which, once completed, signal that a [`Stream`] has been dropped.
|
||||
drop_listeners: FuturesUnordered<DropListener>,
|
||||
no_drop_listeners_waker: Option<Waker>,
|
||||
|
||||
_ondatachannel_closure: SendWrapper<Closure<dyn FnMut(RtcDataChannelEvent)>>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// Create a new inner WebRTC Connection
|
||||
pub(crate) fn new(peer_connection: RtcPeerConnection) -> Self {
|
||||
// An ondatachannel Future enables us to poll for incoming data channel events in poll_incoming
|
||||
let (mut tx_ondatachannel, rx_ondatachannel) = mpsc::channel(4); // we may get more than one data channel opened on a single peer connection
|
||||
|
||||
let ondatachannel_closure = Closure::new(move |ev: RtcDataChannelEvent| {
|
||||
log::trace!("New data channel");
|
||||
|
||||
if let Err(e) = tx_ondatachannel.try_send(ev.channel()) {
|
||||
if e.is_full() {
|
||||
log::warn!("Remote is opening too many data channels, we can't keep up!");
|
||||
return;
|
||||
}
|
||||
|
||||
if e.is_disconnected() {
|
||||
log::warn!("Receiver is gone, are we shutting down?");
|
||||
}
|
||||
}
|
||||
});
|
||||
peer_connection
|
||||
.inner
|
||||
.set_ondatachannel(Some(ondatachannel_closure.as_ref().unchecked_ref()));
|
||||
|
||||
Self {
|
||||
inner: SendWrapper::new(peer_connection),
|
||||
closed: false,
|
||||
drop_listeners: FuturesUnordered::default(),
|
||||
no_drop_listeners_waker: None,
|
||||
inbound_data_channels: SendWrapper::new(rx_ondatachannel),
|
||||
_ondatachannel_closure: SendWrapper::new(ondatachannel_closure),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_stream_from_data_channel(&mut self, data_channel: RtcDataChannel) -> Stream {
|
||||
let (stream, drop_listener) = Stream::new(data_channel);
|
||||
|
||||
self.drop_listeners.push(drop_listener);
|
||||
if let Some(waker) = self.no_drop_listeners_waker.take() {
|
||||
waker.wake()
|
||||
}
|
||||
stream
|
||||
}
|
||||
|
||||
/// Closes the Peer Connection.
|
||||
///
|
||||
/// This closes the data channels also and they will return an error
|
||||
/// if they are used.
|
||||
fn close_connection(&mut self) {
|
||||
if !self.closed {
|
||||
log::trace!("connection::close_connection");
|
||||
self.inner.inner.close();
|
||||
self.closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
fn drop(&mut self) {
|
||||
self.close_connection();
|
||||
}
|
||||
}
|
||||
|
||||
/// WebRTC native multiplexing
|
||||
/// Allows users to open substreams
|
||||
impl StreamMuxer for Connection {
|
||||
type Substream = Stream;
|
||||
type Error = Error;
|
||||
|
||||
fn poll_inbound(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<Self::Substream, Self::Error>> {
|
||||
match ready!(self.inbound_data_channels.poll_next_unpin(cx)) {
|
||||
Some(data_channel) => {
|
||||
let stream = self.new_stream_from_data_channel(data_channel);
|
||||
|
||||
Poll::Ready(Ok(stream))
|
||||
}
|
||||
None => {
|
||||
// This only happens if the [`RtcPeerConnection::ondatachannel`] closure gets freed which means we are most likely shutting down the connection.
|
||||
log::debug!("`Sender` for inbound data channels has been dropped");
|
||||
Poll::Ready(Err(Error::Connection("connection closed".to_owned())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_outbound(
|
||||
mut self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
) -> Poll<Result<Self::Substream, Self::Error>> {
|
||||
log::trace!("Creating outbound data channel");
|
||||
|
||||
let data_channel = self.inner.new_regular_data_channel();
|
||||
let stream = self.new_stream_from_data_channel(data_channel);
|
||||
|
||||
Poll::Ready(Ok(stream))
|
||||
}
|
||||
|
||||
/// Closes the Peer Connection.
|
||||
fn poll_close(
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
log::trace!("connection::poll_close");
|
||||
|
||||
self.close_connection();
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<StreamMuxerEvent, Self::Error>> {
|
||||
loop {
|
||||
match ready!(self.drop_listeners.poll_next_unpin(cx)) {
|
||||
Some(Ok(())) => {}
|
||||
Some(Err(e)) => {
|
||||
log::debug!("a DropListener failed: {e}")
|
||||
}
|
||||
None => {
|
||||
self.no_drop_listeners_waker = Some(cx.waker().clone());
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RtcPeerConnection {
|
||||
inner: web_sys::RtcPeerConnection,
|
||||
}
|
||||
|
||||
impl RtcPeerConnection {
|
||||
pub(crate) async fn new(algorithm: String) -> Result<Self, Error> {
|
||||
let algo: Object = Object::new();
|
||||
Reflect::set(&algo, &"name".into(), &"ECDSA".into()).unwrap();
|
||||
Reflect::set(&algo, &"namedCurve".into(), &"P-256".into()).unwrap();
|
||||
Reflect::set(&algo, &"hash".into(), &algorithm.into()).unwrap();
|
||||
|
||||
let certificate_promise =
|
||||
web_sys::RtcPeerConnection::generate_certificate_with_object(&algo)
|
||||
.expect("certificate to be valid");
|
||||
|
||||
let certificate = JsFuture::from(certificate_promise).await?;
|
||||
|
||||
let mut config = RtcConfiguration::default();
|
||||
// wrap certificate in a js Array first before adding it to the config object
|
||||
let certificate_arr = js_sys::Array::new();
|
||||
certificate_arr.push(&certificate);
|
||||
config.certificates(&certificate_arr);
|
||||
|
||||
let inner = web_sys::RtcPeerConnection::new_with_configuration(&config)?;
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/// Creates the stream for the initial noise handshake.
|
||||
///
|
||||
/// The underlying data channel MUST have `negotiated` set to `true` and carry the ID 0.
|
||||
pub(crate) fn new_handshake_stream(&self) -> (Stream, DropListener) {
|
||||
Stream::new(self.new_data_channel(true))
|
||||
}
|
||||
|
||||
/// Creates a regular data channel for when the connection is already established.
|
||||
pub(crate) fn new_regular_data_channel(&self) -> RtcDataChannel {
|
||||
self.new_data_channel(false)
|
||||
}
|
||||
|
||||
fn new_data_channel(&self, negotiated: bool) -> RtcDataChannel {
|
||||
const LABEL: &str = "";
|
||||
|
||||
let dc = match negotiated {
|
||||
true => {
|
||||
let mut options = RtcDataChannelInit::new();
|
||||
options.negotiated(true).id(0); // id is only ever set to zero when negotiated is true
|
||||
|
||||
self.inner
|
||||
.create_data_channel_with_data_channel_dict(LABEL, &options)
|
||||
}
|
||||
false => self.inner.create_data_channel(LABEL),
|
||||
};
|
||||
dc.set_binary_type(RtcDataChannelType::Arraybuffer); // Hardcoded here, it's the only type we use
|
||||
|
||||
dc
|
||||
}
|
||||
|
||||
pub(crate) async fn create_offer(&self) -> Result<String, Error> {
|
||||
let offer = JsFuture::from(self.inner.create_offer()).await?;
|
||||
|
||||
let offer = Reflect::get(&offer, &JsValue::from_str("sdp"))
|
||||
.expect("sdp should be valid")
|
||||
.as_string()
|
||||
.expect("sdp string should be valid string");
|
||||
|
||||
Ok(offer)
|
||||
}
|
||||
|
||||
pub(crate) async fn set_local_description(
|
||||
&self,
|
||||
sdp: RtcSessionDescriptionInit,
|
||||
) -> Result<(), Error> {
|
||||
let promise = self.inner.set_local_description(&sdp);
|
||||
JsFuture::from(promise).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn local_fingerprint(&self) -> Result<Fingerprint, Error> {
|
||||
let sdp = &self
|
||||
.inner
|
||||
.local_description()
|
||||
.ok_or_else(|| Error::JsError("No local description".to_string()))?
|
||||
.sdp();
|
||||
|
||||
let fingerprint = parse_fingerprint(sdp)
|
||||
.ok_or_else(|| Error::JsError("No fingerprint in SDP".to_string()))?;
|
||||
|
||||
Ok(fingerprint)
|
||||
}
|
||||
|
||||
pub(crate) async fn set_remote_description(
|
||||
&self,
|
||||
sdp: RtcSessionDescriptionInit,
|
||||
) -> Result<(), Error> {
|
||||
let promise = self.inner.set_remote_description(&sdp);
|
||||
JsFuture::from(promise).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse Fingerprint from a SDP.
|
||||
fn parse_fingerprint(sdp: &str) -> Option<Fingerprint> {
|
||||
// split the sdp by new lines / carriage returns
|
||||
let lines = sdp.split("\r\n");
|
||||
|
||||
// iterate through the lines to find the one starting with a=fingerprint:
|
||||
// get the value after the first space
|
||||
// return the value as a Fingerprint
|
||||
for line in lines {
|
||||
if line.starts_with("a=fingerprint:") {
|
||||
let fingerprint = line.split(' ').nth(1).unwrap();
|
||||
let bytes = hex::decode(fingerprint.replace(':', "")).unwrap();
|
||||
let arr: [u8; 32] = bytes.as_slice().try_into().unwrap();
|
||||
return Some(Fingerprint::raw(arr));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sdp_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_fingerprint() {
|
||||
let sdp: &str = "v=0\r\no=- 0 0 IN IP6 ::1\r\ns=-\r\nc=IN IP6 ::1\r\nt=0 0\r\na=ice-lite\r\nm=application 61885 UDP/DTLS/SCTP webrtc-datachannel\r\na=mid:0\r\na=setup:passive\r\na=ice-ufrag:libp2p+webrtc+v1/YwapWySn6fE6L9i47PhlB6X4gzNXcgFs\r\na=ice-pwd:libp2p+webrtc+v1/YwapWySn6fE6L9i47PhlB6X4gzNXcgFs\r\na=fingerprint:sha-256 A8:17:77:1E:02:7E:D1:2B:53:92:70:A6:8E:F9:02:CC:21:72:3A:92:5D:F4:97:5F:27:C4:5E:75:D4:F4:31:89\r\na=sctp-port:5000\r\na=max-message-size:16384\r\na=candidate:1467250027 1 UDP 1467250027 ::1 61885 typ host\r\n";
|
||||
let fingerprint = match parse_fingerprint(sdp) {
|
||||
Some(fingerprint) => fingerprint,
|
||||
None => panic!("No fingerprint found"),
|
||||
};
|
||||
assert_eq!(fingerprint.algorithm(), "sha-256");
|
||||
assert_eq!(fingerprint.to_sdp_format(), "A8:17:77:1E:02:7E:D1:2B:53:92:70:A6:8E:F9:02:CC:21:72:3A:92:5D:F4:97:5F:27:C4:5E:75:D4:F4:31:89");
|
||||
}
|
||||
}
|
57
transports/webrtc-websys/src/error.rs
Normal file
57
transports/webrtc-websys/src/error.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
||||
/// Errors that may happen on the [`Transport`](crate::Transport) or the
|
||||
/// [`Connection`](crate::Connection).
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Invalid multiaddr: {0}")]
|
||||
InvalidMultiaddr(&'static str),
|
||||
|
||||
#[error("JavaScript error: {0}")]
|
||||
JsError(String),
|
||||
|
||||
#[error("JavaScript typecasting failed")]
|
||||
JsCastFailed,
|
||||
|
||||
#[error("Unknown remote peer ID")]
|
||||
UnknownRemotePeerId,
|
||||
|
||||
#[error("Connection error: {0}")]
|
||||
Connection(String),
|
||||
|
||||
#[error("Authentication error")]
|
||||
Authentication(#[from] libp2p_noise::Error),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn from_js_value(value: JsValue) -> Self {
|
||||
let s = if value.is_instance_of::<js_sys::Error>() {
|
||||
js_sys::Error::from(value)
|
||||
.to_string()
|
||||
.as_string()
|
||||
.unwrap_or_else(|| "Unknown error".to_string())
|
||||
} else {
|
||||
"Unknown error".to_string()
|
||||
};
|
||||
|
||||
Error::JsError(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<wasm_bindgen::JsValue> for Error {
|
||||
fn from(value: JsValue) -> Self {
|
||||
Error::from_js_value(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(value: String) -> Self {
|
||||
Error::JsError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Error::JsError(value.to_string())
|
||||
}
|
||||
}
|
13
transports/webrtc-websys/src/lib.rs
Normal file
13
transports/webrtc-websys/src/lib.rs
Normal file
@ -0,0 +1,13 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod connection;
|
||||
mod error;
|
||||
mod sdp;
|
||||
mod stream;
|
||||
mod transport;
|
||||
mod upgrade;
|
||||
|
||||
pub use self::connection::Connection;
|
||||
pub use self::error::Error;
|
||||
pub use self::stream::Stream;
|
||||
pub use self::transport::{Config, Transport};
|
55
transports/webrtc-websys/src/sdp.rs
Normal file
55
transports/webrtc-websys/src/sdp.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use libp2p_webrtc_utils::Fingerprint;
|
||||
use std::net::SocketAddr;
|
||||
use web_sys::{RtcSdpType, RtcSessionDescriptionInit};
|
||||
|
||||
/// Creates the SDP answer used by the client.
|
||||
pub(crate) fn answer(
|
||||
addr: SocketAddr,
|
||||
server_fingerprint: Fingerprint,
|
||||
client_ufrag: &str,
|
||||
) -> RtcSessionDescriptionInit {
|
||||
let mut answer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Answer);
|
||||
answer_obj.sdp(&libp2p_webrtc_utils::sdp::answer(
|
||||
addr,
|
||||
server_fingerprint,
|
||||
client_ufrag,
|
||||
));
|
||||
answer_obj
|
||||
}
|
||||
|
||||
/// Creates the munged SDP offer from the Browser's given SDP offer
|
||||
///
|
||||
/// Certificate verification is disabled which is why we hardcode a dummy fingerprint here.
|
||||
pub(crate) fn offer(offer: String, client_ufrag: &str) -> RtcSessionDescriptionInit {
|
||||
// find line and replace a=ice-ufrag: with "\r\na=ice-ufrag:{client_ufrag}\r\n"
|
||||
// find line and replace a=ice-pwd: with "\r\na=ice-ufrag:{client_ufrag}\r\n"
|
||||
|
||||
let mut munged_sdp_offer = String::new();
|
||||
|
||||
for line in offer.split("\r\n") {
|
||||
if line.starts_with("a=ice-ufrag:") {
|
||||
munged_sdp_offer.push_str(&format!("a=ice-ufrag:{client_ufrag}\r\n"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with("a=ice-pwd:") {
|
||||
munged_sdp_offer.push_str(&format!("a=ice-pwd:{client_ufrag}\r\n"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if !line.is_empty() {
|
||||
munged_sdp_offer.push_str(&format!("{}\r\n", line));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// remove any double \r\n
|
||||
let munged_sdp_offer = munged_sdp_offer.replace("\r\n\r\n", "\r\n");
|
||||
|
||||
log::trace!("Created SDP offer: {munged_sdp_offer}");
|
||||
|
||||
let mut offer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Offer);
|
||||
offer_obj.sdp(&munged_sdp_offer);
|
||||
|
||||
offer_obj
|
||||
}
|
61
transports/webrtc-websys/src/stream.rs
Normal file
61
transports/webrtc-websys/src/stream.rs
Normal file
@ -0,0 +1,61 @@
|
||||
//! The WebRTC [Stream] over the Connection
|
||||
use self::poll_data_channel::PollDataChannel;
|
||||
use futures::{AsyncRead, AsyncWrite};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use web_sys::RtcDataChannel;
|
||||
|
||||
mod poll_data_channel;
|
||||
|
||||
/// A stream over a WebRTC connection.
|
||||
///
|
||||
/// Backed by a WebRTC data channel.
|
||||
pub struct Stream {
|
||||
/// Wrapper for the inner stream to make it Send
|
||||
inner: SendWrapper<libp2p_webrtc_utils::Stream<PollDataChannel>>,
|
||||
}
|
||||
|
||||
pub(crate) type DropListener = SendWrapper<libp2p_webrtc_utils::DropListener<PollDataChannel>>;
|
||||
|
||||
impl Stream {
|
||||
pub(crate) fn new(data_channel: RtcDataChannel) -> (Self, DropListener) {
|
||||
let (inner, drop_listener) =
|
||||
libp2p_webrtc_utils::Stream::new(PollDataChannel::new(data_channel));
|
||||
|
||||
(
|
||||
Self {
|
||||
inner: SendWrapper::new(inner),
|
||||
},
|
||||
SendWrapper::new(drop_listener),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Stream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
Pin::new(&mut *self.get_mut().inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Stream {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
Pin::new(&mut *self.get_mut().inner).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut *self.get_mut().inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut *self.get_mut().inner).poll_close(cx)
|
||||
}
|
||||
}
|
242
transports/webrtc-websys/src/stream/poll_data_channel.rs
Normal file
242
transports/webrtc-websys/src/stream/poll_data_channel.rs
Normal file
@ -0,0 +1,242 @@
|
||||
use std::cmp::min;
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use futures::task::AtomicWaker;
|
||||
use futures::{AsyncRead, AsyncWrite};
|
||||
use libp2p_webrtc_utils::MAX_MSG_LEN;
|
||||
use wasm_bindgen::{prelude::*, JsCast};
|
||||
use web_sys::{Event, MessageEvent, RtcDataChannel, RtcDataChannelEvent, RtcDataChannelState};
|
||||
|
||||
/// [`PollDataChannel`] is a wrapper around around [`RtcDataChannel`] which implements [`AsyncRead`] and [`AsyncWrite`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PollDataChannel {
|
||||
/// The [`RtcDataChannel`] being wrapped.
|
||||
inner: RtcDataChannel,
|
||||
|
||||
new_data_waker: Rc<AtomicWaker>,
|
||||
read_buffer: Rc<Mutex<BytesMut>>,
|
||||
|
||||
/// Waker for when we are waiting for the DC to be opened.
|
||||
open_waker: Rc<AtomicWaker>,
|
||||
|
||||
/// Waker for when we are waiting to write (again) to the DC because we previously exceeded the [`MAX_MSG_LEN`] threshold.
|
||||
write_waker: Rc<AtomicWaker>,
|
||||
|
||||
/// Waker for when we are waiting for the DC to be closed.
|
||||
close_waker: Rc<AtomicWaker>,
|
||||
|
||||
/// Whether we've been overloaded with data by the remote.
|
||||
///
|
||||
/// This is set to `true` in case `read_buffer` overflows, i.e. the remote is sending us messages faster than we can read them.
|
||||
/// In that case, we return an [`std::io::Error`] from [`AsyncRead`] or [`AsyncWrite`], depending which one gets called earlier.
|
||||
/// Failing these will (very likely), cause the application developer to drop the stream which resets it.
|
||||
overloaded: Rc<AtomicBool>,
|
||||
|
||||
// Store the closures for proper garbage collection.
|
||||
// These are wrapped in an [`Rc`] so we can implement [`Clone`].
|
||||
_on_open_closure: Rc<Closure<dyn FnMut(RtcDataChannelEvent)>>,
|
||||
_on_write_closure: Rc<Closure<dyn FnMut(Event)>>,
|
||||
_on_close_closure: Rc<Closure<dyn FnMut(Event)>>,
|
||||
_on_message_closure: Rc<Closure<dyn FnMut(MessageEvent)>>,
|
||||
}
|
||||
|
||||
impl PollDataChannel {
|
||||
pub(crate) fn new(inner: RtcDataChannel) -> Self {
|
||||
let open_waker = Rc::new(AtomicWaker::new());
|
||||
let on_open_closure = Closure::new({
|
||||
let open_waker = open_waker.clone();
|
||||
|
||||
move |_: RtcDataChannelEvent| {
|
||||
log::trace!("DataChannel opened");
|
||||
open_waker.wake();
|
||||
}
|
||||
});
|
||||
inner.set_onopen(Some(on_open_closure.as_ref().unchecked_ref()));
|
||||
|
||||
let write_waker = Rc::new(AtomicWaker::new());
|
||||
inner.set_buffered_amount_low_threshold(0);
|
||||
let on_write_closure = Closure::new({
|
||||
let write_waker = write_waker.clone();
|
||||
|
||||
move |_: Event| {
|
||||
log::trace!("DataChannel available for writing (again)");
|
||||
write_waker.wake();
|
||||
}
|
||||
});
|
||||
inner.set_onbufferedamountlow(Some(on_write_closure.as_ref().unchecked_ref()));
|
||||
|
||||
let close_waker = Rc::new(AtomicWaker::new());
|
||||
let on_close_closure = Closure::new({
|
||||
let close_waker = close_waker.clone();
|
||||
|
||||
move |_: Event| {
|
||||
log::trace!("DataChannel closed");
|
||||
close_waker.wake();
|
||||
}
|
||||
});
|
||||
inner.set_onclose(Some(on_close_closure.as_ref().unchecked_ref()));
|
||||
|
||||
let new_data_waker = Rc::new(AtomicWaker::new());
|
||||
let read_buffer = Rc::new(Mutex::new(BytesMut::new())); // We purposely don't use `with_capacity` so we don't eagerly allocate `MAX_READ_BUFFER` per stream.
|
||||
let overloaded = Rc::new(AtomicBool::new(false));
|
||||
|
||||
let on_message_closure = Closure::<dyn FnMut(_)>::new({
|
||||
let new_data_waker = new_data_waker.clone();
|
||||
let read_buffer = read_buffer.clone();
|
||||
let overloaded = overloaded.clone();
|
||||
|
||||
move |ev: MessageEvent| {
|
||||
let data = js_sys::Uint8Array::new(&ev.data());
|
||||
|
||||
let mut read_buffer = read_buffer.lock().unwrap();
|
||||
|
||||
if read_buffer.len() + data.length() as usize > MAX_MSG_LEN {
|
||||
overloaded.store(true, Ordering::SeqCst);
|
||||
log::warn!("Remote is overloading us with messages, resetting stream",);
|
||||
return;
|
||||
}
|
||||
|
||||
read_buffer.extend_from_slice(&data.to_vec());
|
||||
new_data_waker.wake();
|
||||
}
|
||||
});
|
||||
inner.set_onmessage(Some(on_message_closure.as_ref().unchecked_ref()));
|
||||
|
||||
Self {
|
||||
inner,
|
||||
new_data_waker,
|
||||
read_buffer,
|
||||
open_waker,
|
||||
write_waker,
|
||||
close_waker,
|
||||
overloaded,
|
||||
_on_open_closure: Rc::new(on_open_closure),
|
||||
_on_write_closure: Rc::new(on_write_closure),
|
||||
_on_close_closure: Rc::new(on_close_closure),
|
||||
_on_message_closure: Rc::new(on_message_closure),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [RtcDataChannelState] of the [RtcDataChannel]
|
||||
fn ready_state(&self) -> RtcDataChannelState {
|
||||
self.inner.ready_state()
|
||||
}
|
||||
|
||||
/// Returns the current [RtcDataChannel] BufferedAmount
|
||||
fn buffered_amount(&self) -> usize {
|
||||
self.inner.buffered_amount() as usize
|
||||
}
|
||||
|
||||
/// Whether the data channel is ready for reading or writing.
|
||||
fn poll_ready(&mut self, cx: &mut Context) -> Poll<io::Result<()>> {
|
||||
match self.ready_state() {
|
||||
RtcDataChannelState::Connecting => {
|
||||
self.open_waker.register(cx.waker());
|
||||
return Poll::Pending;
|
||||
}
|
||||
RtcDataChannelState::Closing | RtcDataChannelState::Closed => {
|
||||
return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into()))
|
||||
}
|
||||
RtcDataChannelState::Open | RtcDataChannelState::__Nonexhaustive => {}
|
||||
}
|
||||
|
||||
if self.overloaded.load(Ordering::SeqCst) {
|
||||
return Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"remote overloaded us with messages",
|
||||
)));
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for PollDataChannel {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
let this = self.get_mut();
|
||||
|
||||
futures::ready!(this.poll_ready(cx))?;
|
||||
|
||||
let mut read_buffer = this.read_buffer.lock().unwrap();
|
||||
|
||||
if read_buffer.is_empty() {
|
||||
this.new_data_waker.register(cx.waker());
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
// Ensure that we:
|
||||
// - at most return what the caller can read (`buf.len()`)
|
||||
// - at most what we have (`read_buffer.len()`)
|
||||
let split_index = min(buf.len(), read_buffer.len());
|
||||
|
||||
let bytes_to_return = read_buffer.split_to(split_index);
|
||||
let len = bytes_to_return.len();
|
||||
buf[..len].copy_from_slice(&bytes_to_return);
|
||||
|
||||
Poll::Ready(Ok(len))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for PollDataChannel {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
let this = self.get_mut();
|
||||
|
||||
futures::ready!(this.poll_ready(cx))?;
|
||||
|
||||
debug_assert!(this.buffered_amount() <= MAX_MSG_LEN);
|
||||
let remaining_space = MAX_MSG_LEN - this.buffered_amount();
|
||||
|
||||
if remaining_space == 0 {
|
||||
this.write_waker.register(cx.waker());
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
let bytes_to_send = min(buf.len(), remaining_space);
|
||||
|
||||
if this
|
||||
.inner
|
||||
.send_with_u8_array(&buf[..bytes_to_send])
|
||||
.is_err()
|
||||
{
|
||||
return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into()));
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(bytes_to_send))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
if self.buffered_amount() == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
self.write_waker.register(cx.waker());
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
if self.ready_state() == RtcDataChannelState::Closed {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if self.ready_state() != RtcDataChannelState::Closing {
|
||||
self.inner.close();
|
||||
}
|
||||
|
||||
self.close_waker.register(cx.waker());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
140
transports/webrtc-websys/src/transport.rs
Normal file
140
transports/webrtc-websys/src/transport.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use super::upgrade;
|
||||
use super::Connection;
|
||||
use super::Error;
|
||||
use futures::future::FutureExt;
|
||||
use libp2p_core::multiaddr::Multiaddr;
|
||||
use libp2p_core::muxing::StreamMuxerBox;
|
||||
use libp2p_core::transport::{Boxed, ListenerId, Transport as _, TransportError, TransportEvent};
|
||||
use libp2p_identity::{Keypair, PeerId};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
/// Config for the [`Transport`].
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
keypair: Keypair,
|
||||
}
|
||||
|
||||
/// A WebTransport [`Transport`](libp2p_core::Transport) that works with `web-sys`.
|
||||
pub struct Transport {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Constructs a new configuration for the [`Transport`].
|
||||
pub fn new(keypair: &Keypair) -> Self {
|
||||
Config {
|
||||
keypair: keypair.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
/// Constructs a new `Transport` with the given [`Config`].
|
||||
pub fn new(config: Config) -> Transport {
|
||||
Transport { config }
|
||||
}
|
||||
|
||||
/// Wraps `Transport` in [`Boxed`] and makes it ready to be consumed by
|
||||
/// SwarmBuilder.
|
||||
pub fn boxed(self) -> Boxed<(PeerId, StreamMuxerBox)> {
|
||||
self.map(|(peer_id, muxer), _| (peer_id, StreamMuxerBox::new(muxer)))
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl libp2p_core::Transport for Transport {
|
||||
type Output = (PeerId, Connection);
|
||||
type Error = Error;
|
||||
type ListenerUpgrade = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
type Dial = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
|
||||
fn listen_on(
|
||||
&mut self,
|
||||
_id: ListenerId,
|
||||
addr: Multiaddr,
|
||||
) -> Result<(), TransportError<Self::Error>> {
|
||||
Err(TransportError::MultiaddrNotSupported(addr))
|
||||
}
|
||||
|
||||
fn remove_listener(&mut self, _id: ListenerId) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn dial(&mut self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
|
||||
if maybe_local_firefox() {
|
||||
return Err(TransportError::Other(
|
||||
"Firefox does not support WebRTC over localhost or 127.0.0.1"
|
||||
.to_string()
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
let (sock_addr, server_fingerprint) = libp2p_webrtc_utils::parse_webrtc_dial_addr(&addr)
|
||||
.ok_or_else(|| TransportError::MultiaddrNotSupported(addr.clone()))?;
|
||||
|
||||
if sock_addr.port() == 0 || sock_addr.ip().is_unspecified() {
|
||||
return Err(TransportError::MultiaddrNotSupported(addr));
|
||||
}
|
||||
|
||||
let config = self.config.clone();
|
||||
|
||||
Ok(async move {
|
||||
let (peer_id, connection) =
|
||||
upgrade::outbound(sock_addr, server_fingerprint, config.keypair.clone()).await?;
|
||||
|
||||
Ok((peer_id, connection))
|
||||
}
|
||||
.boxed())
|
||||
}
|
||||
|
||||
fn dial_as_listener(
|
||||
&mut self,
|
||||
addr: Multiaddr,
|
||||
) -> Result<Self::Dial, TransportError<Self::Error>> {
|
||||
Err(TransportError::MultiaddrNotSupported(addr))
|
||||
}
|
||||
|
||||
fn poll(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option<Multiaddr> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if local Firefox.
|
||||
///
|
||||
/// See: `<https://bugzilla.mozilla.org/show_bug.cgi?id=1659672>` for more details
|
||||
fn maybe_local_firefox() -> bool {
|
||||
let window = &web_sys::window().expect("window should be available");
|
||||
let ua = match window.navigator().user_agent() {
|
||||
Ok(agent) => agent.to_lowercase(),
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let hostname = match window
|
||||
.document()
|
||||
.expect("should be valid document")
|
||||
.location()
|
||||
{
|
||||
Some(location) => match location.hostname() {
|
||||
Ok(hostname) => hostname,
|
||||
Err(_) => return false,
|
||||
},
|
||||
None => return false,
|
||||
};
|
||||
|
||||
// check if web_sys::Navigator::user_agent() matches any of the following:
|
||||
// - firefox
|
||||
// - seamonkey
|
||||
// - iceape
|
||||
// AND hostname is either localhost or "127.0.0.1"
|
||||
(ua.contains("firefox") || ua.contains("seamonkey") || ua.contains("iceape"))
|
||||
&& (hostname == "localhost" || hostname == "127.0.0.1" || hostname == "[::1]")
|
||||
}
|
56
transports/webrtc-websys/src/upgrade.rs
Normal file
56
transports/webrtc-websys/src/upgrade.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use super::Error;
|
||||
use crate::connection::RtcPeerConnection;
|
||||
use crate::sdp;
|
||||
use crate::Connection;
|
||||
use libp2p_identity::{Keypair, PeerId};
|
||||
use libp2p_webrtc_utils::noise;
|
||||
use libp2p_webrtc_utils::Fingerprint;
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
/// Upgrades an outbound WebRTC connection by creating the data channel
|
||||
/// and conducting a Noise handshake
|
||||
pub(crate) async fn outbound(
|
||||
sock_addr: SocketAddr,
|
||||
remote_fingerprint: Fingerprint,
|
||||
id_keys: Keypair,
|
||||
) -> Result<(PeerId, Connection), Error> {
|
||||
let fut = SendWrapper::new(outbound_inner(sock_addr, remote_fingerprint, id_keys));
|
||||
fut.await
|
||||
}
|
||||
|
||||
/// Inner outbound function that is wrapped in [SendWrapper]
|
||||
async fn outbound_inner(
|
||||
sock_addr: SocketAddr,
|
||||
remote_fingerprint: Fingerprint,
|
||||
id_keys: Keypair,
|
||||
) -> Result<(PeerId, Connection), Error> {
|
||||
let rtc_peer_connection = RtcPeerConnection::new(remote_fingerprint.algorithm()).await?;
|
||||
|
||||
// Create stream for Noise handshake
|
||||
// Must create data channel before Offer is created for it to be included in the SDP
|
||||
let (channel, listener) = rtc_peer_connection.new_handshake_stream();
|
||||
drop(listener);
|
||||
|
||||
let ufrag = libp2p_webrtc_utils::sdp::random_ufrag();
|
||||
|
||||
let offer = rtc_peer_connection.create_offer().await?;
|
||||
let munged_offer = sdp::offer(offer, &ufrag);
|
||||
rtc_peer_connection
|
||||
.set_local_description(munged_offer)
|
||||
.await?;
|
||||
|
||||
let answer = sdp::answer(sock_addr, remote_fingerprint, &ufrag);
|
||||
rtc_peer_connection.set_remote_description(answer).await?;
|
||||
|
||||
let local_fingerprint = rtc_peer_connection.local_fingerprint()?;
|
||||
|
||||
log::trace!("local_fingerprint: {:?}", local_fingerprint);
|
||||
log::trace!("remote_fingerprint: {:?}", remote_fingerprint);
|
||||
|
||||
let peer_id = noise::outbound(id_keys, channel, remote_fingerprint, local_fingerprint).await?;
|
||||
|
||||
log::debug!("Remote peer identified as {peer_id}");
|
||||
|
||||
Ok((peer_id, Connection::new(rtc_peer_connection)))
|
||||
}
|
@ -1,3 +1,10 @@
|
||||
## 0.6.1-alpha - unreleased
|
||||
|
||||
- Move common dependencies to `libp2p-webrtc-utils` crate.
|
||||
See [PR 4248].
|
||||
|
||||
[PR 4248]: https://github.com/libp2p/rust-libp2p/pull/4248
|
||||
|
||||
## 0.6.0-alpha
|
||||
|
||||
- Update `webrtc` dependency to `v0.8.0`.
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libp2p-webrtc"
|
||||
version = "0.6.0-alpha"
|
||||
version = "0.6.1-alpha"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
description = "WebRTC transport for libp2p"
|
||||
repository = "https://github.com/libp2p/rust-libp2p"
|
||||
@ -12,7 +12,6 @@ categories = ["network-programming", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
asynchronous-codec = "0.6.2"
|
||||
bytes = "1"
|
||||
futures = "0.3"
|
||||
futures-timer = "3"
|
||||
@ -21,11 +20,9 @@ if-watch = "3.0"
|
||||
libp2p-core = { workspace = true }
|
||||
libp2p-noise = { workspace = true }
|
||||
libp2p-identity = { workspace = true }
|
||||
libp2p-webrtc-utils = { workspace = true }
|
||||
log = "0.4"
|
||||
sha2 = "0.10.7"
|
||||
multihash = { workspace = true }
|
||||
quick-protobuf = "0.8"
|
||||
quick-protobuf-codec = { workspace = true }
|
||||
multihash = { workspace = true }
|
||||
rand = "0.8"
|
||||
rcgen = "0.11.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -79,13 +79,5 @@
|
||||
//! hand-crate the SDP answer generated by the remote, this is problematic. A way to solve this
|
||||
//! is to make the hash a part of the remote's multiaddr. On the server side, we turn
|
||||
//! certificate verification off.
|
||||
|
||||
mod proto {
|
||||
#![allow(unreachable_pub)]
|
||||
include!("generated/mod.rs");
|
||||
#[cfg(feature = "tokio")]
|
||||
pub(crate) use self::webrtc::pb::{mod_Message::Flag, Message};
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
pub mod tokio;
|
||||
|
@ -40,7 +40,7 @@ use std::{
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use crate::tokio::{error::Error, substream, substream::Substream};
|
||||
use crate::tokio::{error::Error, stream, stream::Stream};
|
||||
|
||||
/// Maximum number of unprocessed data channels.
|
||||
/// See [`Connection::poll_inbound`].
|
||||
@ -56,14 +56,14 @@ pub struct Connection {
|
||||
/// Channel onto which incoming data channels are put.
|
||||
incoming_data_channels_rx: mpsc::Receiver<Arc<DetachedDataChannel>>,
|
||||
|
||||
/// Future, which, once polled, will result in an outbound substream.
|
||||
/// Future, which, once polled, will result in an outbound stream.
|
||||
outbound_fut: Option<BoxFuture<'static, Result<Arc<DetachedDataChannel>, Error>>>,
|
||||
|
||||
/// Future, which, once polled, will result in closing the entire connection.
|
||||
close_fut: Option<BoxFuture<'static, Result<(), Error>>>,
|
||||
|
||||
/// A list of futures, which, once completed, signal that a [`Substream`] has been dropped.
|
||||
drop_listeners: FuturesUnordered<substream::DropListener>,
|
||||
/// A list of futures, which, once completed, signal that a [`Stream`] has been dropped.
|
||||
drop_listeners: FuturesUnordered<stream::DropListener>,
|
||||
no_drop_listeners_waker: Option<Waker>,
|
||||
}
|
||||
|
||||
@ -147,7 +147,7 @@ impl Connection {
|
||||
}
|
||||
|
||||
impl StreamMuxer for Connection {
|
||||
type Substream = Substream;
|
||||
type Substream = Stream;
|
||||
type Error = Error;
|
||||
|
||||
fn poll_inbound(
|
||||
@ -156,15 +156,15 @@ impl StreamMuxer for Connection {
|
||||
) -> Poll<Result<Self::Substream, Self::Error>> {
|
||||
match ready!(self.incoming_data_channels_rx.poll_next_unpin(cx)) {
|
||||
Some(detached) => {
|
||||
log::trace!("Incoming substream {}", detached.stream_identifier());
|
||||
log::trace!("Incoming stream {}", detached.stream_identifier());
|
||||
|
||||
let (substream, drop_listener) = Substream::new(detached);
|
||||
let (stream, drop_listener) = Stream::new(detached);
|
||||
self.drop_listeners.push(drop_listener);
|
||||
if let Some(waker) = self.no_drop_listeners_waker.take() {
|
||||
waker.wake()
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(substream))
|
||||
Poll::Ready(Ok(stream))
|
||||
}
|
||||
None => {
|
||||
debug_assert!(
|
||||
@ -226,15 +226,15 @@ impl StreamMuxer for Connection {
|
||||
Ok(detached) => {
|
||||
self.outbound_fut = None;
|
||||
|
||||
log::trace!("Outbound substream {}", detached.stream_identifier());
|
||||
log::trace!("Outbound stream {}", detached.stream_identifier());
|
||||
|
||||
let (substream, drop_listener) = Substream::new(detached);
|
||||
let (stream, drop_listener) = Stream::new(detached);
|
||||
self.drop_listeners.push(drop_listener);
|
||||
if let Some(waker) = self.no_drop_listeners_waker.take() {
|
||||
waker.wake()
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(substream))
|
||||
Poll::Ready(Ok(stream))
|
||||
}
|
||||
Err(e) => {
|
||||
self.outbound_fut = None;
|
||||
|
@ -18,30 +18,25 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
use sha2::Digest as _;
|
||||
use std::fmt;
|
||||
use webrtc::dtls_transport::dtls_fingerprint::RTCDtlsFingerprint;
|
||||
|
||||
const SHA256: &str = "sha-256";
|
||||
const MULTIHASH_SHA256_CODE: u64 = 0x12;
|
||||
|
||||
type Multihash = multihash::Multihash<64>;
|
||||
|
||||
/// A certificate fingerprint that is assumed to be created using the SHA256 hash algorithm.
|
||||
#[derive(Eq, PartialEq, Copy, Clone)]
|
||||
pub struct Fingerprint([u8; 32]);
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub struct Fingerprint(libp2p_webrtc_utils::Fingerprint);
|
||||
|
||||
impl Fingerprint {
|
||||
pub(crate) const FF: Fingerprint = Fingerprint([0xFF; 32]);
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn raw(bytes: [u8; 32]) -> Self {
|
||||
Self(bytes)
|
||||
Self(libp2p_webrtc_utils::Fingerprint::raw(bytes))
|
||||
}
|
||||
|
||||
/// Creates a fingerprint from a raw certificate.
|
||||
pub fn from_certificate(bytes: &[u8]) -> Self {
|
||||
Fingerprint(sha2::Sha256::digest(bytes).into())
|
||||
Fingerprint(libp2p_webrtc_utils::Fingerprint::from_certificate(bytes))
|
||||
}
|
||||
|
||||
/// Converts [`RTCDtlsFingerprint`] to [`Fingerprint`].
|
||||
@ -53,58 +48,35 @@ impl Fingerprint {
|
||||
let mut buf = [0; 32];
|
||||
hex::decode_to_slice(fp.value.replace(':', ""), &mut buf).ok()?;
|
||||
|
||||
Some(Self(buf))
|
||||
Some(Self(libp2p_webrtc_utils::Fingerprint::raw(buf)))
|
||||
}
|
||||
|
||||
/// Converts [`Multihash`](multihash::Multihash) to [`Fingerprint`].
|
||||
pub fn try_from_multihash(hash: Multihash) -> Option<Self> {
|
||||
if hash.code() != MULTIHASH_SHA256_CODE {
|
||||
// Only support SHA256 for now.
|
||||
return None;
|
||||
}
|
||||
|
||||
let bytes = hash.digest().try_into().ok()?;
|
||||
|
||||
Some(Self(bytes))
|
||||
Some(Self(libp2p_webrtc_utils::Fingerprint::try_from_multihash(
|
||||
hash,
|
||||
)?))
|
||||
}
|
||||
|
||||
/// Converts this fingerprint to [`Multihash`](multihash::Multihash).
|
||||
pub fn to_multihash(self) -> Multihash {
|
||||
Multihash::wrap(MULTIHASH_SHA256_CODE, &self.0).expect("fingerprint's len to be 32 bytes")
|
||||
self.0.to_multihash()
|
||||
}
|
||||
|
||||
/// Formats this fingerprint as uppercase hex, separated by colons (`:`).
|
||||
///
|
||||
/// This is the format described in <https://www.rfc-editor.org/rfc/rfc4572#section-5>.
|
||||
pub fn to_sdp_format(self) -> String {
|
||||
self.0.map(|byte| format!("{byte:02X}")).join(":")
|
||||
self.0.to_sdp_format()
|
||||
}
|
||||
|
||||
/// Returns the algorithm used (e.g. "sha-256").
|
||||
/// See <https://datatracker.ietf.org/doc/html/rfc8122#section-5>
|
||||
pub fn algorithm(&self) -> String {
|
||||
SHA256.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Fingerprint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&hex::encode(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sdp_format() {
|
||||
let fp = Fingerprint::raw(hex_literal::hex!(
|
||||
"7DE3D83F81A680592A471E6B6ABB0747ABD35385A8093FDFE112C1EEBB6CC6AC"
|
||||
));
|
||||
|
||||
let sdp_format = fp.to_sdp_format();
|
||||
|
||||
assert_eq!(sdp_format, "7D:E3:D8:3F:81:A6:80:59:2A:47:1E:6B:6A:BB:07:47:AB:D3:53:85:A8:09:3F:DF:E1:12:C1:EE:BB:6C:C6:AC")
|
||||
self.0.algorithm()
|
||||
}
|
||||
|
||||
pub(crate) fn into_inner(self) -> libp2p_webrtc_utils::Fingerprint {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ mod error;
|
||||
mod fingerprint;
|
||||
mod req_res_chan;
|
||||
mod sdp;
|
||||
mod substream;
|
||||
mod stream;
|
||||
mod transport;
|
||||
mod udp_mux;
|
||||
mod upgrade;
|
||||
|
@ -18,22 +18,19 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
use serde::Serialize;
|
||||
use tinytemplate::TinyTemplate;
|
||||
pub(crate) use libp2p_webrtc_utils::sdp::random_ufrag;
|
||||
use libp2p_webrtc_utils::sdp::render_description;
|
||||
use libp2p_webrtc_utils::Fingerprint;
|
||||
use std::net::SocketAddr;
|
||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use crate::tokio::fingerprint::Fingerprint;
|
||||
|
||||
/// Creates the SDP answer used by the client.
|
||||
pub(crate) fn answer(
|
||||
addr: SocketAddr,
|
||||
server_fingerprint: &Fingerprint,
|
||||
server_fingerprint: Fingerprint,
|
||||
client_ufrag: &str,
|
||||
) -> RTCSessionDescription {
|
||||
RTCSessionDescription::answer(render_description(
|
||||
SERVER_SESSION_DESCRIPTION,
|
||||
RTCSessionDescription::answer(libp2p_webrtc_utils::sdp::answer(
|
||||
addr,
|
||||
server_fingerprint,
|
||||
client_ufrag,
|
||||
@ -45,13 +42,16 @@ pub(crate) fn answer(
|
||||
///
|
||||
/// Certificate verification is disabled which is why we hardcode a dummy fingerprint here.
|
||||
pub(crate) fn offer(addr: SocketAddr, client_ufrag: &str) -> RTCSessionDescription {
|
||||
RTCSessionDescription::offer(render_description(
|
||||
let offer = render_description(
|
||||
CLIENT_SESSION_DESCRIPTION,
|
||||
addr,
|
||||
&Fingerprint::FF,
|
||||
Fingerprint::FF,
|
||||
client_ufrag,
|
||||
))
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
log::trace!("Created SDP offer: {offer}");
|
||||
|
||||
RTCSessionDescription::offer(offer).unwrap()
|
||||
}
|
||||
|
||||
// An SDP message that constitutes the offer.
|
||||
@ -142,111 +142,3 @@ a=setup:actpass
|
||||
a=sctp-port:5000
|
||||
a=max-message-size:16384
|
||||
";
|
||||
|
||||
// See [`CLIENT_SESSION_DESCRIPTION`].
|
||||
//
|
||||
// a=ice-lite
|
||||
//
|
||||
// A lite implementation is only appropriate for devices that will *always* be connected to
|
||||
// the public Internet and have a public IP address at which it can receive packets from any
|
||||
// correspondent. ICE will not function when a lite implementation is placed behind a NAT
|
||||
// (RFC8445).
|
||||
//
|
||||
// a=tls-id:<id>
|
||||
//
|
||||
// "TLS ID" uniquely identifies a TLS association.
|
||||
// The ICE protocol uses a "TLS ID" system to indicate whether a fresh DTLS connection
|
||||
// must be reopened in case of ICE renegotiation. Considering that ICE renegotiations
|
||||
// never happen in our use case, we can simply put a random value and not care about
|
||||
// it. Note however that the TLS ID in the answer must be present if and only if the
|
||||
// offer contains one. (RFC8842)
|
||||
// TODO: is it true that renegotiations never happen? what about a connection closing?
|
||||
// "tls-id" attribute MUST be present in the initial offer and respective answer (RFC8839).
|
||||
// XXX: but right now browsers don't send it.
|
||||
//
|
||||
// a=setup:passive
|
||||
//
|
||||
// "passive" indicates that the remote DTLS server will only listen for incoming
|
||||
// connections. (RFC5763)
|
||||
// The answerer (server) MUST not be located behind a NAT (RFC6135).
|
||||
//
|
||||
// The answerer MUST use either a setup attribute value of setup:active or setup:passive.
|
||||
// Note that if the answerer uses setup:passive, then the DTLS handshake will not begin until
|
||||
// the answerer is received, which adds additional latency. setup:active allows the answer and
|
||||
// the DTLS handshake to occur in parallel. Thus, setup:active is RECOMMENDED.
|
||||
//
|
||||
// a=candidate:<foundation> <component-id> <transport> <priority> <connection-address> <port> <cand-type>
|
||||
//
|
||||
// A transport address for a candidate that can be used for connectivity checks (RFC8839).
|
||||
//
|
||||
// a=end-of-candidates
|
||||
//
|
||||
// Indicate that no more candidates will ever be sent (RFC8838).
|
||||
const SERVER_SESSION_DESCRIPTION: &str = "v=0
|
||||
o=- 0 0 IN {ip_version} {target_ip}
|
||||
s=-
|
||||
t=0 0
|
||||
a=ice-lite
|
||||
m=application {target_port} UDP/DTLS/SCTP webrtc-datachannel
|
||||
c=IN {ip_version} {target_ip}
|
||||
a=mid:0
|
||||
a=ice-options:ice2
|
||||
a=ice-ufrag:{ufrag}
|
||||
a=ice-pwd:{pwd}
|
||||
a=fingerprint:{fingerprint_algorithm} {fingerprint_value}
|
||||
|
||||
a=setup:passive
|
||||
a=sctp-port:5000
|
||||
a=max-message-size:16384
|
||||
a=candidate:1 1 UDP 1 {target_ip} {target_port} typ host
|
||||
a=end-of-candidates
|
||||
";
|
||||
|
||||
/// Indicates the IP version used in WebRTC: `IP4` or `IP6`.
|
||||
#[derive(Serialize)]
|
||||
enum IpVersion {
|
||||
IP4,
|
||||
IP6,
|
||||
}
|
||||
|
||||
/// Context passed to the templating engine, which replaces the above placeholders (e.g.
|
||||
/// `{IP_VERSION}`) with real values.
|
||||
#[derive(Serialize)]
|
||||
struct DescriptionContext {
|
||||
pub(crate) ip_version: IpVersion,
|
||||
pub(crate) target_ip: IpAddr,
|
||||
pub(crate) target_port: u16,
|
||||
pub(crate) fingerprint_algorithm: String,
|
||||
pub(crate) fingerprint_value: String,
|
||||
pub(crate) ufrag: String,
|
||||
pub(crate) pwd: String,
|
||||
}
|
||||
|
||||
/// Renders a [`TinyTemplate`] description using the provided arguments.
|
||||
fn render_description(
|
||||
description: &str,
|
||||
addr: SocketAddr,
|
||||
fingerprint: &Fingerprint,
|
||||
ufrag: &str,
|
||||
) -> String {
|
||||
let mut tt = TinyTemplate::new();
|
||||
tt.add_template("description", description).unwrap();
|
||||
|
||||
let context = DescriptionContext {
|
||||
ip_version: {
|
||||
if addr.is_ipv4() {
|
||||
IpVersion::IP4
|
||||
} else {
|
||||
IpVersion::IP6
|
||||
}
|
||||
},
|
||||
target_ip: addr.ip(),
|
||||
target_port: addr.port(),
|
||||
fingerprint_algorithm: fingerprint.algorithm(),
|
||||
fingerprint_value: fingerprint.to_sdp_format(),
|
||||
// NOTE: ufrag is equal to pwd.
|
||||
ufrag: ufrag.to_owned(),
|
||||
pwd: ufrag.to_owned(),
|
||||
};
|
||||
tt.render("description", &context).unwrap()
|
||||
}
|
||||
|
80
transports/webrtc/src/tokio/stream.rs
Normal file
80
transports/webrtc/src/tokio/stream.rs
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright 2023 Protocol Labs.
|
||||
//
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures::prelude::*;
|
||||
use libp2p_webrtc_utils::MAX_MSG_LEN;
|
||||
use tokio_util::compat::{Compat, TokioAsyncReadCompatExt};
|
||||
use webrtc::data::data_channel::{DataChannel, PollDataChannel};
|
||||
|
||||
/// A substream on top of a WebRTC data channel.
|
||||
///
|
||||
/// To be a proper libp2p substream, we need to implement [`AsyncRead`] and [`AsyncWrite`] as well
|
||||
/// as support a half-closed state which we do by framing messages in a protobuf envelope.
|
||||
pub struct Stream {
|
||||
inner: libp2p_webrtc_utils::Stream<Compat<PollDataChannel>>,
|
||||
}
|
||||
|
||||
pub(crate) type DropListener = libp2p_webrtc_utils::DropListener<Compat<PollDataChannel>>;
|
||||
|
||||
impl Stream {
|
||||
/// Returns a new `Substream` and a listener, which will notify the receiver when/if the substream
|
||||
/// is dropped.
|
||||
pub(crate) fn new(data_channel: Arc<DataChannel>) -> (Self, DropListener) {
|
||||
let mut data_channel = PollDataChannel::new(data_channel).compat();
|
||||
data_channel.get_mut().set_read_buf_capacity(MAX_MSG_LEN);
|
||||
|
||||
let (inner, drop_listener) = libp2p_webrtc_utils::Stream::new(data_channel);
|
||||
|
||||
(Self { inner }, drop_listener)
|
||||
}
|
||||
}
|
||||
impl AsyncRead for Stream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Stream {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
Pin::new(&mut self.get_mut().inner).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.get_mut().inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.get_mut().inner).poll_close(cx)
|
||||
}
|
||||
}
|
@ -119,7 +119,7 @@ impl libp2p_core::Transport for Transport {
|
||||
}
|
||||
|
||||
fn dial(&mut self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
|
||||
let (sock_addr, server_fingerprint) = parse_webrtc_dial_addr(&addr)
|
||||
let (sock_addr, server_fingerprint) = libp2p_webrtc_utils::parse_webrtc_dial_addr(&addr)
|
||||
.ok_or_else(|| TransportError::MultiaddrNotSupported(addr.clone()))?;
|
||||
if sock_addr.port() == 0 || sock_addr.ip().is_unspecified() {
|
||||
return Err(TransportError::MultiaddrNotSupported(addr));
|
||||
@ -140,7 +140,7 @@ impl libp2p_core::Transport for Transport {
|
||||
sock_addr,
|
||||
config.inner,
|
||||
udp_mux,
|
||||
client_fingerprint,
|
||||
client_fingerprint.into_inner(),
|
||||
server_fingerprint,
|
||||
config.id_keys,
|
||||
)
|
||||
@ -337,7 +337,7 @@ impl Stream for ListenStream {
|
||||
new_addr.addr,
|
||||
self.config.inner.clone(),
|
||||
self.udp_mux.udp_mux_handle(),
|
||||
self.config.fingerprint,
|
||||
self.config.fingerprint.into_inner(),
|
||||
new_addr.ufrag,
|
||||
self.config.id_keys.clone(),
|
||||
)
|
||||
@ -427,40 +427,6 @@ fn parse_webrtc_listen_addr(addr: &Multiaddr) -> Option<SocketAddr> {
|
||||
Some(SocketAddr::new(ip, port))
|
||||
}
|
||||
|
||||
/// Parse the given [`Multiaddr`] into a [`SocketAddr`] and a [`Fingerprint`] for dialing.
|
||||
fn parse_webrtc_dial_addr(addr: &Multiaddr) -> Option<(SocketAddr, Fingerprint)> {
|
||||
let mut iter = addr.iter();
|
||||
|
||||
let ip = match iter.next()? {
|
||||
Protocol::Ip4(ip) => IpAddr::from(ip),
|
||||
Protocol::Ip6(ip) => IpAddr::from(ip),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let port = iter.next()?;
|
||||
let webrtc = iter.next()?;
|
||||
let certhash = iter.next()?;
|
||||
|
||||
let (port, fingerprint) = match (port, webrtc, certhash) {
|
||||
(Protocol::Udp(port), Protocol::WebRTCDirect, Protocol::Certhash(cert_hash)) => {
|
||||
let fingerprint = Fingerprint::try_from_multihash(cert_hash)?;
|
||||
|
||||
(port, fingerprint)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
match iter.next() {
|
||||
Some(Protocol::P2p(_)) => {}
|
||||
// peer ID is optional
|
||||
None => {}
|
||||
// unexpected protocol
|
||||
Some(_) => return None,
|
||||
}
|
||||
|
||||
Some((SocketAddr::new(ip, port), fingerprint))
|
||||
}
|
||||
|
||||
// Tests //////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#[cfg(test)]
|
||||
@ -469,7 +435,7 @@ mod tests {
|
||||
use futures::future::poll_fn;
|
||||
use libp2p_core::{multiaddr::Protocol, Transport as _};
|
||||
use rand::thread_rng;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::net::{IpAddr, Ipv6Addr};
|
||||
|
||||
#[test]
|
||||
fn missing_webrtc_protocol() {
|
||||
@ -480,44 +446,6 @@ mod tests {
|
||||
assert!(maybe_parsed.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_valid_address_with_certhash_and_p2p() {
|
||||
let addr = "/ip4/127.0.0.1/udp/39901/webrtc-direct/certhash/uEiDikp5KVUgkLta1EjUN-IKbHk-dUBg8VzKgf5nXxLK46w/p2p/12D3KooWNpDk9w6WrEEcdsEH1y47W71S36yFjw4sd3j7omzgCSMS"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let maybe_parsed = parse_webrtc_dial_addr(&addr);
|
||||
|
||||
assert_eq!(
|
||||
maybe_parsed,
|
||||
Some((
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 39901),
|
||||
Fingerprint::raw(hex_literal::hex!(
|
||||
"e2929e4a5548242ed6b512350df8829b1e4f9d50183c5732a07f99d7c4b2b8eb"
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_id_is_not_required() {
|
||||
let addr = "/ip4/127.0.0.1/udp/39901/webrtc-direct/certhash/uEiDikp5KVUgkLta1EjUN-IKbHk-dUBg8VzKgf5nXxLK46w"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let maybe_parsed = parse_webrtc_dial_addr(&addr);
|
||||
|
||||
assert_eq!(
|
||||
maybe_parsed,
|
||||
Some((
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 39901),
|
||||
Fingerprint::raw(hex_literal::hex!(
|
||||
"e2929e4a5548242ed6b512350df8829b1e4f9d50183c5732a07f99d7c4b2b8eb"
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcp_is_invalid_protocol() {
|
||||
let addr = "/ip4/127.0.0.1/tcp/12345/webrtc-direct/certhash/uEiDikp5KVUgkLta1EjUN-IKbHk-dUBg8VzKgf5nXxLK46w"
|
||||
@ -540,26 +468,6 @@ mod tests {
|
||||
assert!(maybe_parsed.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ipv6() {
|
||||
let addr =
|
||||
"/ip6/::1/udp/12345/webrtc-direct/certhash/uEiDikp5KVUgkLta1EjUN-IKbHk-dUBg8VzKgf5nXxLK46w/p2p/12D3KooWNpDk9w6WrEEcdsEH1y47W71S36yFjw4sd3j7omzgCSMS"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let maybe_parsed = parse_webrtc_dial_addr(&addr);
|
||||
|
||||
assert_eq!(
|
||||
maybe_parsed,
|
||||
Some((
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 12345),
|
||||
Fingerprint::raw(hex_literal::hex!(
|
||||
"e2929e4a5548242ed6b512350df8829b1e4f9d50183c5732a07f99d7c4b2b8eb"
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_valid_addr_without_certhash() {
|
||||
let addr = "/ip6/::1/udp/12345/webrtc-direct".parse().unwrap();
|
||||
|
@ -18,15 +18,14 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
mod noise;
|
||||
use libp2p_webrtc_utils::{noise, Fingerprint};
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::Either;
|
||||
use futures_timer::Delay;
|
||||
use libp2p_identity as identity;
|
||||
use libp2p_identity::PeerId;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||
use webrtc::api::setting_engine::SettingEngine;
|
||||
use webrtc::api::APIBuilder;
|
||||
use webrtc::data::data_channel::DataChannel;
|
||||
@ -38,9 +37,8 @@ use webrtc::ice::udp_network::UDPNetwork;
|
||||
use webrtc::peer_connection::configuration::RTCConfiguration;
|
||||
use webrtc::peer_connection::RTCPeerConnection;
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use crate::tokio::{error::Error, fingerprint::Fingerprint, sdp, substream::Substream, Connection};
|
||||
use crate::tokio::sdp::random_ufrag;
|
||||
use crate::tokio::{error::Error, sdp, stream::Stream, Connection};
|
||||
|
||||
/// Creates a new outbound WebRTC connection.
|
||||
pub(crate) async fn outbound(
|
||||
@ -59,7 +57,7 @@ pub(crate) async fn outbound(
|
||||
log::debug!("created SDP offer for outbound connection: {:?}", offer.sdp);
|
||||
peer_connection.set_local_description(offer).await?;
|
||||
|
||||
let answer = sdp::answer(addr, &server_fingerprint, &ufrag);
|
||||
let answer = sdp::answer(addr, server_fingerprint, &ufrag);
|
||||
log::debug!(
|
||||
"calculated SDP answer for outbound connection: {:?}",
|
||||
answer
|
||||
@ -155,18 +153,6 @@ async fn new_inbound_connection(
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
/// Generates a random ufrag and adds a prefix according to the spec.
|
||||
fn random_ufrag() -> String {
|
||||
format!(
|
||||
"libp2p+webrtc+v1/{}",
|
||||
thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(64)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
)
|
||||
}
|
||||
|
||||
fn setting_engine(
|
||||
udp_mux: Arc<dyn UDPMux + Send + Sync>,
|
||||
ufrag: &str,
|
||||
@ -203,9 +189,7 @@ async fn get_remote_fingerprint(conn: &RTCPeerConnection) -> Fingerprint {
|
||||
Fingerprint::from_certificate(&cert_bytes)
|
||||
}
|
||||
|
||||
async fn create_substream_for_noise_handshake(
|
||||
conn: &RTCPeerConnection,
|
||||
) -> Result<Substream, Error> {
|
||||
async fn create_substream_for_noise_handshake(conn: &RTCPeerConnection) -> Result<Stream, Error> {
|
||||
// NOTE: the data channel w/ `negotiated` flag set to `true` MUST be created on both ends.
|
||||
let data_channel = conn
|
||||
.create_data_channel(
|
||||
@ -234,7 +218,7 @@ async fn create_substream_for_noise_handshake(
|
||||
}
|
||||
};
|
||||
|
||||
let (substream, drop_listener) = Substream::new(channel);
|
||||
let (substream, drop_listener) = Stream::new(channel);
|
||||
drop(drop_listener); // Don't care about cancelled substreams during initial handshake.
|
||||
|
||||
Ok(substream)
|
||||
|
Loading…
x
Reference in New Issue
Block a user