mirror of
https://github.com/fluencelabs/rust-libp2p
synced 2025-05-30 03:01:21 +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",
|
"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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@ -652,6 +661,32 @@ dependencies = [
|
|||||||
"log",
|
"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]]
|
[[package]]
|
||||||
name = "bs58"
|
name = "bs58"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -661,6 +696,16 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.13.0"
|
version = "3.13.0"
|
||||||
@ -1239,6 +1284,26 @@ dependencies = [
|
|||||||
"subtle",
|
"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]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@ -1731,6 +1796,19 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
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]]
|
[[package]]
|
||||||
name = "gloo-timers"
|
name = "gloo-timers"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@ -2119,12 +2197,13 @@ dependencies = [
|
|||||||
"libp2p",
|
"libp2p",
|
||||||
"libp2p-mplex",
|
"libp2p-mplex",
|
||||||
"libp2p-webrtc",
|
"libp2p-webrtc",
|
||||||
|
"libp2p-webrtc-websys",
|
||||||
"log",
|
"log",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"redis",
|
"redis",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rust-embed",
|
"rust-embed 8.0.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thirtyfour",
|
"thirtyfour",
|
||||||
@ -3089,11 +3168,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libp2p-webrtc"
|
name = "libp2p-webrtc"
|
||||||
version = "0.6.0-alpha"
|
version = "0.6.1-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"asynchronous-codec",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"env_logger 0.10.0",
|
"env_logger 0.10.0",
|
||||||
"futures",
|
"futures",
|
||||||
@ -3106,15 +3184,13 @@ dependencies = [
|
|||||||
"libp2p-noise",
|
"libp2p-noise",
|
||||||
"libp2p-ping",
|
"libp2p-ping",
|
||||||
"libp2p-swarm",
|
"libp2p-swarm",
|
||||||
|
"libp2p-webrtc-utils",
|
||||||
"log",
|
"log",
|
||||||
"multihash",
|
"multihash",
|
||||||
"quick-protobuf",
|
|
||||||
"quick-protobuf-codec",
|
|
||||||
"quickcheck",
|
"quickcheck",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rcgen 0.11.1",
|
"rcgen 0.11.1",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2 0.10.7",
|
|
||||||
"stun",
|
"stun",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tinytemplate",
|
"tinytemplate",
|
||||||
@ -3125,6 +3201,55 @@ dependencies = [
|
|||||||
"webrtc",
|
"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]]
|
[[package]]
|
||||||
name = "libp2p-websocket"
|
name = "libp2p-websocket"
|
||||||
version = "0.42.1"
|
version = "0.42.1"
|
||||||
@ -3782,7 +3907,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.3.5",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
@ -4321,6 +4446,15 @@ dependencies = [
|
|||||||
"url",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@ -4330,13 +4464,24 @@ dependencies = [
|
|||||||
"bitflags 1.3.2",
|
"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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.9.5"
|
version = "1.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
|
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick 1.0.2",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata 0.3.8",
|
"regex-automata 0.3.8",
|
||||||
"regex-syntax 0.7.5",
|
"regex-syntax 0.7.5",
|
||||||
@ -4357,7 +4502,7 @@ version = "0.3.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
|
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick 1.0.2",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax 0.7.5",
|
"regex-syntax 0.7.5",
|
||||||
]
|
]
|
||||||
@ -4542,14 +4687,39 @@ dependencies = [
|
|||||||
"webrtc-util",
|
"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]]
|
[[package]]
|
||||||
name = "rust-embed"
|
name = "rust-embed"
|
||||||
version = "8.0.0"
|
version = "8.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
|
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rust-embed-impl",
|
"rust-embed-impl 8.0.0",
|
||||||
"rust-embed-utils",
|
"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",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4561,11 +4731,22 @@ checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils 8.0.0",
|
||||||
"syn 2.0.32",
|
"syn 2.0.32",
|
||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "rust-embed-utils"
|
name = "rust-embed-utils"
|
||||||
version = "8.0.0"
|
version = "8.0.0"
|
||||||
@ -4961,6 +5142,15 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellexpand"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@ -5238,7 +5428,7 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand 2.0.0",
|
"fastrand 2.0.0",
|
||||||
"redox_syscall",
|
"redox_syscall 0.3.5",
|
||||||
"rustix 0.38.4",
|
"rustix 0.38.4",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
members = [
|
members = [
|
||||||
"core",
|
"core",
|
||||||
"examples/autonat",
|
"examples/autonat",
|
||||||
|
"examples/browser-webrtc",
|
||||||
"examples/chat-example",
|
"examples/chat-example",
|
||||||
"examples/dcutr",
|
"examples/dcutr",
|
||||||
"examples/distributed-key-value-store",
|
"examples/distributed-key-value-store",
|
||||||
@ -26,6 +27,7 @@ members = [
|
|||||||
"misc/quickcheck-ext",
|
"misc/quickcheck-ext",
|
||||||
"misc/rw-stream-sink",
|
"misc/rw-stream-sink",
|
||||||
"misc/server",
|
"misc/server",
|
||||||
|
"misc/webrtc-utils",
|
||||||
"muxers/mplex",
|
"muxers/mplex",
|
||||||
"muxers/test-harness",
|
"muxers/test-harness",
|
||||||
"muxers/yamux",
|
"muxers/yamux",
|
||||||
@ -56,6 +58,7 @@ members = [
|
|||||||
"transports/uds",
|
"transports/uds",
|
||||||
"transports/wasm-ext",
|
"transports/wasm-ext",
|
||||||
"transports/webrtc",
|
"transports/webrtc",
|
||||||
|
"transports/webrtc-websys",
|
||||||
"transports/websocket",
|
"transports/websocket",
|
||||||
"transports/webtransport-websys",
|
"transports/webtransport-websys",
|
||||||
"wasm-tests/webtransport-tests",
|
"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-tls = { version = "0.2.1", path = "transports/tls" }
|
||||||
libp2p-uds = { version = "0.39.0", path = "transports/uds" }
|
libp2p-uds = { version = "0.39.0", path = "transports/uds" }
|
||||||
libp2p-wasm-ext = { version = "0.40.0", path = "transports/wasm-ext" }
|
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-websocket = { version = "0.42.1", path = "transports/websocket" }
|
||||||
libp2p-webtransport-websys = { version = "0.1.0", path = "transports/webtransport-websys" }
|
libp2p-webtransport-websys = { version = "0.1.0", path = "transports/webtransport-websys" }
|
||||||
libp2p-yamux = { version = "0.44.1", path = "muxers/yamux" }
|
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"
|
multiaddr = "0.18.0"
|
||||||
multihash = "0.19.1"
|
multihash = "0.19.1"
|
||||||
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|
||||||
# Patch away `libp2p-idnentity` in our dependency tree with the workspace version.
|
# 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]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
libp2p = { path = "../libp2p", features = ["ping", "macros", "webtransport-websys", "wasm-bindgen", "identify"] }
|
libp2p = { path = "../libp2p", features = ["ping", "macros", "webtransport-websys", "wasm-bindgen", "identify"] }
|
||||||
|
libp2p-webrtc-websys = { workspace = true }
|
||||||
wasm-bindgen = { version = "0.2" }
|
wasm-bindgen = { version = "0.2" }
|
||||||
wasm-bindgen-futures = { version = "0.4" }
|
wasm-bindgen-futures = { version = "0.4" }
|
||||||
wasm-logger = { version = "0.2.0" }
|
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
|
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:
|
can dial/listen for ourselves we can do the following:
|
||||||
|
|
||||||
1. Start redis (needed by the tests): `docker run --rm -it -p 6379:6379
|
1. Start redis (needed by the tests): `docker run --rm -p 6379:6379 redis:7-alpine`.
|
||||||
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`
|
||||||
2. In one terminal run the dialer: `redis_addr=localhost:6379 ip="0.0.0.0"
|
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`
|
||||||
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
|
To test the interop with other versions do something similar, except replace one
|
||||||
of these nodes with the other version's interop test.
|
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`
|
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`
|
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
|
# Running all interop tests locally with Compose
|
||||||
|
|
||||||
To run this test against all released libp2p versions you'll need to have the
|
To run this test against all released libp2p versions you'll need to have the
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "chromium-rust-libp2p-head",
|
"id": "chromium-rust-libp2p-head",
|
||||||
"containerImageID": "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": [],
|
"secureChannels": [],
|
||||||
"muxers": []
|
"muxers": []
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,7 @@ pub(crate) mod wasm {
|
|||||||
use libp2p::identity::Keypair;
|
use libp2p::identity::Keypair;
|
||||||
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder};
|
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder};
|
||||||
use libp2p::PeerId;
|
use libp2p::PeerId;
|
||||||
|
use libp2p_webrtc_websys as webrtc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{BlpopRequest, Transport};
|
use crate::{BlpopRequest, Transport};
|
||||||
@ -181,16 +182,19 @@ pub(crate) mod wasm {
|
|||||||
ip: &str,
|
ip: &str,
|
||||||
transport: Transport,
|
transport: Transport,
|
||||||
) -> Result<(BoxedTransport, String)> {
|
) -> Result<(BoxedTransport, String)> {
|
||||||
if let Transport::Webtransport = transport {
|
match transport {
|
||||||
Ok((
|
Transport::Webtransport => Ok((
|
||||||
libp2p::webtransport_websys::Transport::new(
|
libp2p::webtransport_websys::Transport::new(
|
||||||
libp2p::webtransport_websys::Config::new(&local_key),
|
libp2p::webtransport_websys::Config::new(&local_key),
|
||||||
)
|
)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
format!("/ip4/{ip}/udp/0/quic/webtransport"),
|
format!("/ip4/{ip}/udp/0/quic/webtransport"),
|
||||||
))
|
)),
|
||||||
} else {
|
Transport::WebRtcDirect => Ok((
|
||||||
bail!("Only webtransport supported with wasm")
|
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::process::Stdio;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ pub async fn run_test(
|
|||||||
let mut maybe_id = None;
|
let mut maybe_id = None;
|
||||||
|
|
||||||
// See https://github.com/libp2p/rust-libp2p/issues/4071.
|
// See https://github.com/libp2p/rust-libp2p/issues/4071.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if transport == Transport::WebRtcDirect {
|
if transport == Transport::WebRtcDirect {
|
||||||
maybe_id = Some(swarm.listen_on(local_addr.parse()?)?);
|
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_identity::PeerId;
|
||||||
use libp2p_noise as noise;
|
use libp2p_noise as noise;
|
||||||
|
|
||||||
use crate::tokio::fingerprint::Fingerprint;
|
use crate::fingerprint::Fingerprint;
|
||||||
use crate::tokio::Error;
|
|
||||||
|
|
||||||
pub(crate) async fn inbound<T>(
|
pub async fn inbound<T>(
|
||||||
id_keys: identity::Keypair,
|
id_keys: identity::Keypair,
|
||||||
stream: T,
|
stream: T,
|
||||||
client_fingerprint: Fingerprint,
|
client_fingerprint: Fingerprint,
|
||||||
server_fingerprint: Fingerprint,
|
server_fingerprint: Fingerprint,
|
||||||
) -> Result<PeerId, Error>
|
) -> Result<PeerId, libp2p_noise::Error>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
{
|
{
|
||||||
@ -49,12 +48,12 @@ where
|
|||||||
Ok(peer_id)
|
Ok(peer_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn outbound<T>(
|
pub async fn outbound<T>(
|
||||||
id_keys: identity::Keypair,
|
id_keys: identity::Keypair,
|
||||||
stream: T,
|
stream: T,
|
||||||
server_fingerprint: Fingerprint,
|
server_fingerprint: Fingerprint,
|
||||||
client_fingerprint: Fingerprint,
|
client_fingerprint: Fingerprint,
|
||||||
) -> Result<PeerId, Error>
|
) -> Result<PeerId, libp2p_noise::Error>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
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 2022 Parity Technologies (UK) Ltd.
|
||||||
|
// Copyright 2023 Protocol Labs.
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
// copy of this software and associated documentation files (the "Software"),
|
// 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
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
// DEALINGS IN THE SOFTWARE.
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
use asynchronous_codec::Framed;
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::{channel::oneshot, prelude::*, ready};
|
use futures::{channel::oneshot, prelude::*, ready};
|
||||||
use tokio_util::compat::Compat;
|
|
||||||
use webrtc::data::data_channel::{DataChannel, PollDataChannel};
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::Arc,
|
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::proto::{Flag, Message};
|
use crate::proto::{Flag, Message};
|
||||||
use crate::tokio::{
|
use crate::{
|
||||||
substream::drop_listener::GracefullyClosed,
|
stream::drop_listener::GracefullyClosed,
|
||||||
substream::framed_dc::FramedDc,
|
stream::framed_dc::FramedDc,
|
||||||
substream::state::{Closing, State},
|
stream::state::{Closing, State},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod drop_listener;
|
mod drop_listener;
|
||||||
@ -47,7 +44,7 @@ mod state;
|
|||||||
/// "As long as message interleaving is not supported, the sender SHOULD limit the maximum message
|
/// "As long as message interleaving is not supported, the sender SHOULD limit the maximum message
|
||||||
/// size to 16 KB to avoid monopolization."
|
/// size to 16 KB to avoid monopolization."
|
||||||
/// Source: <https://www.rfc-editor.org/rfc/rfc8831#name-transferring-user-data-on-a>
|
/// 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.
|
/// Length of varint, in bytes.
|
||||||
const VARINT_LEN: usize = 2;
|
const VARINT_LEN: usize = 2;
|
||||||
/// Overhead of the protobuf encoding, in bytes.
|
/// Overhead of the protobuf encoding, in bytes.
|
||||||
@ -55,26 +52,28 @@ const PROTO_OVERHEAD: usize = 5;
|
|||||||
/// Maximum length of data, in bytes.
|
/// Maximum length of data, in bytes.
|
||||||
const MAX_DATA_LEN: usize = MAX_MSG_LEN - VARINT_LEN - PROTO_OVERHEAD;
|
const MAX_DATA_LEN: usize = MAX_MSG_LEN - VARINT_LEN - PROTO_OVERHEAD;
|
||||||
|
|
||||||
pub(crate) use drop_listener::DropListener;
|
pub use drop_listener::DropListener;
|
||||||
/// A substream on top of a WebRTC data channel.
|
/// 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.
|
/// as support a half-closed state which we do by framing messages in a protobuf envelope.
|
||||||
pub struct Substream {
|
pub struct Stream<T> {
|
||||||
io: FramedDc,
|
io: FramedDc<T>,
|
||||||
state: State,
|
state: State,
|
||||||
read_buffer: Bytes,
|
read_buffer: Bytes,
|
||||||
/// Dropping this will close the oneshot and notify the receiver by emitting `Canceled`.
|
/// Dropping this will close the oneshot and notify the receiver by emitting `Canceled`.
|
||||||
drop_notifier: Option<oneshot::Sender<GracefullyClosed>>,
|
drop_notifier: Option<oneshot::Sender<GracefullyClosed>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Substream {
|
impl<T> Stream<T>
|
||||||
/// Returns a new `Substream` and a listener, which will notify the receiver when/if the substream
|
where
|
||||||
/// is dropped.
|
T: AsyncRead + AsyncWrite + Unpin + Clone,
|
||||||
pub(crate) fn new(data_channel: Arc<DataChannel>) -> (Self, DropListener) {
|
{
|
||||||
|
/// 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 (sender, receiver) = oneshot::channel();
|
||||||
|
|
||||||
let substream = Self {
|
let stream = Self {
|
||||||
io: framed_dc::new(data_channel.clone()),
|
io: framed_dc::new(data_channel.clone()),
|
||||||
state: State::Open,
|
state: State::Open,
|
||||||
read_buffer: Bytes::default(),
|
read_buffer: Bytes::default(),
|
||||||
@ -82,10 +81,10 @@ impl Substream {
|
|||||||
};
|
};
|
||||||
let listener = DropListener::new(framed_dc::new(data_channel), receiver);
|
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<()>> {
|
pub fn poll_close_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||||
loop {
|
loop {
|
||||||
match self.state.close_read_barrier()? {
|
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(
|
fn poll_read(
|
||||||
mut self: Pin<&mut Self>,
|
mut self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
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(
|
fn poll_write(
|
||||||
mut self: Pin<&mut Self>,
|
mut self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
@ -236,10 +241,13 @@ impl AsyncWrite for Substream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn io_poll_next(
|
fn io_poll_next<T>(
|
||||||
io: &mut Framed<Compat<PollDataChannel>, quick_protobuf_codec::Codec<Message>>,
|
io: &mut FramedDc<T>,
|
||||||
cx: &mut Context<'_>,
|
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))
|
match ready!(io.poll_next_unpin(cx))
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
||||||
@ -262,8 +270,8 @@ mod tests {
|
|||||||
// Largest possible message.
|
// Largest possible message.
|
||||||
let message = [0; MAX_DATA_LEN];
|
let message = [0; MAX_DATA_LEN];
|
||||||
|
|
||||||
let protobuf = crate::proto::Message {
|
let protobuf = Message {
|
||||||
flag: Some(crate::proto::Flag::FIN),
|
flag: Some(Flag::FIN),
|
||||||
message: Some(message.to_vec()),
|
message: Some(message.to_vec()),
|
||||||
};
|
};
|
||||||
|
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use futures::channel::oneshot::Canceled;
|
use futures::channel::oneshot::Canceled;
|
||||||
use futures::{FutureExt, SinkExt};
|
use futures::{AsyncRead, AsyncWrite, FutureExt, SinkExt};
|
||||||
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::io;
|
use std::io;
|
||||||
@ -28,46 +28,42 @@ use std::pin::Pin;
|
|||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
use crate::proto::{Flag, Message};
|
use crate::proto::{Flag, Message};
|
||||||
use crate::tokio::substream::framed_dc::FramedDc;
|
use crate::stream::framed_dc::FramedDc;
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) struct DropListener {
|
pub struct DropListener<T> {
|
||||||
state: State,
|
state: State<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DropListener {
|
impl<T> DropListener<T> {
|
||||||
pub(crate) fn new(stream: FramedDc, receiver: oneshot::Receiver<GracefullyClosed>) -> Self {
|
pub fn new(stream: FramedDc<T>, receiver: oneshot::Receiver<GracefullyClosed>) -> Self {
|
||||||
let substream_id = stream.get_ref().stream_identifier();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state: State::Idle {
|
state: State::Idle { stream, receiver },
|
||||||
stream,
|
|
||||||
receiver,
|
|
||||||
substream_id,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum State {
|
enum State<T> {
|
||||||
/// The [`DropListener`] is idle and waiting to be activated.
|
/// The [`DropListener`] is idle and waiting to be activated.
|
||||||
Idle {
|
Idle {
|
||||||
stream: FramedDc,
|
stream: FramedDc<T>,
|
||||||
receiver: oneshot::Receiver<GracefullyClosed>,
|
receiver: oneshot::Receiver<GracefullyClosed>,
|
||||||
substream_id: u16,
|
|
||||||
},
|
},
|
||||||
/// The stream got dropped and we are sending a reset flag.
|
/// The stream got dropped and we are sending a reset flag.
|
||||||
SendingReset {
|
SendingReset {
|
||||||
stream: FramedDc,
|
stream: FramedDc<T>,
|
||||||
},
|
},
|
||||||
Flushing {
|
Flushing {
|
||||||
stream: FramedDc,
|
stream: FramedDc<T>,
|
||||||
},
|
},
|
||||||
/// Bad state transition.
|
/// Bad state transition.
|
||||||
Poisoned,
|
Poisoned,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Future for DropListener {
|
impl<T> Future for DropListener<T>
|
||||||
|
where
|
||||||
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
type Output = io::Result<()>;
|
type Output = io::Result<()>;
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
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) {
|
match std::mem::replace(state, State::Poisoned) {
|
||||||
State::Idle {
|
State::Idle {
|
||||||
stream,
|
stream,
|
||||||
substream_id,
|
|
||||||
mut receiver,
|
mut receiver,
|
||||||
} => match receiver.poll_unpin(cx) {
|
} => match receiver.poll_unpin(cx) {
|
||||||
Poll::Ready(Ok(GracefullyClosed {})) => {
|
Poll::Ready(Ok(GracefullyClosed {})) => {
|
||||||
return Poll::Ready(Ok(()));
|
return Poll::Ready(Ok(()));
|
||||||
}
|
}
|
||||||
Poll::Ready(Err(Canceled)) => {
|
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 };
|
*state = State::SendingReset { stream };
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Poll::Pending => {
|
Poll::Pending => {
|
||||||
*state = State::Idle {
|
*state = State::Idle { stream, receiver };
|
||||||
stream,
|
|
||||||
substream_id,
|
|
||||||
receiver,
|
|
||||||
};
|
|
||||||
return Poll::Pending;
|
return Poll::Pending;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -126,5 +117,5 @@ impl Future for DropListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indicates that our substream got gracefully closed.
|
/// Indicates that our stream got gracefully closed.
|
||||||
pub(crate) struct GracefullyClosed {}
|
pub struct GracefullyClosed {}
|
@ -19,22 +19,18 @@
|
|||||||
// DEALINGS IN THE SOFTWARE.
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
use asynchronous_codec::Framed;
|
use asynchronous_codec::Framed;
|
||||||
use tokio_util::compat::Compat;
|
use futures::{AsyncRead, AsyncWrite};
|
||||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
|
||||||
use webrtc::data::data_channel::{DataChannel, PollDataChannel};
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use super::{MAX_DATA_LEN, MAX_MSG_LEN, VARINT_LEN};
|
|
||||||
use crate::proto::Message;
|
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) type FramedDc<T> = Framed<T, quick_protobuf_codec::Codec<Message>>;
|
||||||
pub(crate) fn new(data_channel: Arc<DataChannel>) -> FramedDc {
|
pub(crate) fn new<T>(inner: T) -> FramedDc<T>
|
||||||
let mut inner = PollDataChannel::new(data_channel);
|
where
|
||||||
inner.set_read_buf_capacity(MAX_MSG_LEN);
|
T: AsyncRead + AsyncWrite,
|
||||||
|
{
|
||||||
let mut framed = Framed::new(
|
let mut framed = Framed::new(
|
||||||
inner.compat(),
|
inner,
|
||||||
quick_protobuf_codec::Codec::new(MAX_MSG_LEN - VARINT_LEN),
|
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
|
// 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>> {
|
pub(crate) fn close_read_barrier(&mut self) -> io::Result<Option<Closing>> {
|
||||||
loop {
|
loop {
|
||||||
match self {
|
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
|
## 0.6.0-alpha
|
||||||
|
|
||||||
- Update `webrtc` dependency to `v0.8.0`.
|
- Update `webrtc` dependency to `v0.8.0`.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libp2p-webrtc"
|
name = "libp2p-webrtc"
|
||||||
version = "0.6.0-alpha"
|
version = "0.6.1-alpha"
|
||||||
authors = ["Parity Technologies <admin@parity.io>"]
|
authors = ["Parity Technologies <admin@parity.io>"]
|
||||||
description = "WebRTC transport for libp2p"
|
description = "WebRTC transport for libp2p"
|
||||||
repository = "https://github.com/libp2p/rust-libp2p"
|
repository = "https://github.com/libp2p/rust-libp2p"
|
||||||
@ -12,7 +12,6 @@ categories = ["network-programming", "asynchronous"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
asynchronous-codec = "0.6.2"
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-timer = "3"
|
futures-timer = "3"
|
||||||
@ -21,11 +20,9 @@ if-watch = "3.0"
|
|||||||
libp2p-core = { workspace = true }
|
libp2p-core = { workspace = true }
|
||||||
libp2p-noise = { workspace = true }
|
libp2p-noise = { workspace = true }
|
||||||
libp2p-identity = { workspace = true }
|
libp2p-identity = { workspace = true }
|
||||||
|
libp2p-webrtc-utils = { workspace = true }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
sha2 = "0.10.7"
|
multihash = { workspace = true }
|
||||||
multihash = { workspace = true }
|
|
||||||
quick-protobuf = "0.8"
|
|
||||||
quick-protobuf-codec = { workspace = true }
|
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rcgen = "0.11.1"
|
rcgen = "0.11.1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
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
|
//! 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
|
//! is to make the hash a part of the remote's multiaddr. On the server side, we turn
|
||||||
//! certificate verification off.
|
//! 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")]
|
#[cfg(feature = "tokio")]
|
||||||
pub mod tokio;
|
pub mod tokio;
|
||||||
|
@ -40,7 +40,7 @@ use std::{
|
|||||||
task::{Context, Poll},
|
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.
|
/// Maximum number of unprocessed data channels.
|
||||||
/// See [`Connection::poll_inbound`].
|
/// See [`Connection::poll_inbound`].
|
||||||
@ -56,14 +56,14 @@ pub struct Connection {
|
|||||||
/// Channel onto which incoming data channels are put.
|
/// Channel onto which incoming data channels are put.
|
||||||
incoming_data_channels_rx: mpsc::Receiver<Arc<DetachedDataChannel>>,
|
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>>>,
|
outbound_fut: Option<BoxFuture<'static, Result<Arc<DetachedDataChannel>, Error>>>,
|
||||||
|
|
||||||
/// Future, which, once polled, will result in closing the entire connection.
|
/// Future, which, once polled, will result in closing the entire connection.
|
||||||
close_fut: Option<BoxFuture<'static, Result<(), Error>>>,
|
close_fut: Option<BoxFuture<'static, Result<(), Error>>>,
|
||||||
|
|
||||||
/// A list of futures, which, once completed, signal that a [`Substream`] has been dropped.
|
/// A list of futures, which, once completed, signal that a [`Stream`] has been dropped.
|
||||||
drop_listeners: FuturesUnordered<substream::DropListener>,
|
drop_listeners: FuturesUnordered<stream::DropListener>,
|
||||||
no_drop_listeners_waker: Option<Waker>,
|
no_drop_listeners_waker: Option<Waker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StreamMuxer for Connection {
|
impl StreamMuxer for Connection {
|
||||||
type Substream = Substream;
|
type Substream = Stream;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn poll_inbound(
|
fn poll_inbound(
|
||||||
@ -156,15 +156,15 @@ impl StreamMuxer for Connection {
|
|||||||
) -> Poll<Result<Self::Substream, Self::Error>> {
|
) -> Poll<Result<Self::Substream, Self::Error>> {
|
||||||
match ready!(self.incoming_data_channels_rx.poll_next_unpin(cx)) {
|
match ready!(self.incoming_data_channels_rx.poll_next_unpin(cx)) {
|
||||||
Some(detached) => {
|
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);
|
self.drop_listeners.push(drop_listener);
|
||||||
if let Some(waker) = self.no_drop_listeners_waker.take() {
|
if let Some(waker) = self.no_drop_listeners_waker.take() {
|
||||||
waker.wake()
|
waker.wake()
|
||||||
}
|
}
|
||||||
|
|
||||||
Poll::Ready(Ok(substream))
|
Poll::Ready(Ok(stream))
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
@ -226,15 +226,15 @@ impl StreamMuxer for Connection {
|
|||||||
Ok(detached) => {
|
Ok(detached) => {
|
||||||
self.outbound_fut = None;
|
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);
|
self.drop_listeners.push(drop_listener);
|
||||||
if let Some(waker) = self.no_drop_listeners_waker.take() {
|
if let Some(waker) = self.no_drop_listeners_waker.take() {
|
||||||
waker.wake()
|
waker.wake()
|
||||||
}
|
}
|
||||||
|
|
||||||
Poll::Ready(Ok(substream))
|
Poll::Ready(Ok(stream))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.outbound_fut = None;
|
self.outbound_fut = None;
|
||||||
|
@ -18,30 +18,25 @@
|
|||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
// DEALINGS IN THE SOFTWARE.
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
use sha2::Digest as _;
|
|
||||||
use std::fmt;
|
|
||||||
use webrtc::dtls_transport::dtls_fingerprint::RTCDtlsFingerprint;
|
use webrtc::dtls_transport::dtls_fingerprint::RTCDtlsFingerprint;
|
||||||
|
|
||||||
const SHA256: &str = "sha-256";
|
const SHA256: &str = "sha-256";
|
||||||
const MULTIHASH_SHA256_CODE: u64 = 0x12;
|
|
||||||
|
|
||||||
type Multihash = multihash::Multihash<64>;
|
type Multihash = multihash::Multihash<64>;
|
||||||
|
|
||||||
/// A certificate fingerprint that is assumed to be created using the SHA256 hash algorithm.
|
/// A certificate fingerprint that is assumed to be created using the SHA256 hash algorithm.
|
||||||
#[derive(Eq, PartialEq, Copy, Clone)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
pub struct Fingerprint([u8; 32]);
|
pub struct Fingerprint(libp2p_webrtc_utils::Fingerprint);
|
||||||
|
|
||||||
impl Fingerprint {
|
impl Fingerprint {
|
||||||
pub(crate) const FF: Fingerprint = Fingerprint([0xFF; 32]);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn raw(bytes: [u8; 32]) -> Self {
|
pub fn raw(bytes: [u8; 32]) -> Self {
|
||||||
Self(bytes)
|
Self(libp2p_webrtc_utils::Fingerprint::raw(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a fingerprint from a raw certificate.
|
/// Creates a fingerprint from a raw certificate.
|
||||||
pub fn from_certificate(bytes: &[u8]) -> Self {
|
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`].
|
/// Converts [`RTCDtlsFingerprint`] to [`Fingerprint`].
|
||||||
@ -53,58 +48,35 @@ impl Fingerprint {
|
|||||||
let mut buf = [0; 32];
|
let mut buf = [0; 32];
|
||||||
hex::decode_to_slice(fp.value.replace(':', ""), &mut buf).ok()?;
|
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`].
|
/// Converts [`Multihash`](multihash::Multihash) to [`Fingerprint`].
|
||||||
pub fn try_from_multihash(hash: Multihash) -> Option<Self> {
|
pub fn try_from_multihash(hash: Multihash) -> Option<Self> {
|
||||||
if hash.code() != MULTIHASH_SHA256_CODE {
|
Some(Self(libp2p_webrtc_utils::Fingerprint::try_from_multihash(
|
||||||
// Only support SHA256 for now.
|
hash,
|
||||||
return None;
|
)?))
|
||||||
}
|
|
||||||
|
|
||||||
let bytes = hash.digest().try_into().ok()?;
|
|
||||||
|
|
||||||
Some(Self(bytes))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts this fingerprint to [`Multihash`](multihash::Multihash).
|
/// Converts this fingerprint to [`Multihash`](multihash::Multihash).
|
||||||
pub fn to_multihash(self) -> 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 (`:`).
|
/// Formats this fingerprint as uppercase hex, separated by colons (`:`).
|
||||||
///
|
///
|
||||||
/// This is the format described in <https://www.rfc-editor.org/rfc/rfc4572#section-5>.
|
/// This is the format described in <https://www.rfc-editor.org/rfc/rfc4572#section-5>.
|
||||||
pub fn to_sdp_format(self) -> String {
|
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").
|
/// Returns the algorithm used (e.g. "sha-256").
|
||||||
/// See <https://datatracker.ietf.org/doc/html/rfc8122#section-5>
|
/// See <https://datatracker.ietf.org/doc/html/rfc8122#section-5>
|
||||||
pub fn algorithm(&self) -> String {
|
pub fn algorithm(&self) -> String {
|
||||||
SHA256.to_owned()
|
self.0.algorithm()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
pub(crate) fn into_inner(self) -> libp2p_webrtc_utils::Fingerprint {
|
||||||
impl fmt::Debug for Fingerprint {
|
self.0
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ mod error;
|
|||||||
mod fingerprint;
|
mod fingerprint;
|
||||||
mod req_res_chan;
|
mod req_res_chan;
|
||||||
mod sdp;
|
mod sdp;
|
||||||
mod substream;
|
mod stream;
|
||||||
mod transport;
|
mod transport;
|
||||||
mod udp_mux;
|
mod udp_mux;
|
||||||
mod upgrade;
|
mod upgrade;
|
||||||
|
@ -18,22 +18,19 @@
|
|||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
// DEALINGS IN THE SOFTWARE.
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
use serde::Serialize;
|
pub(crate) use libp2p_webrtc_utils::sdp::random_ufrag;
|
||||||
use tinytemplate::TinyTemplate;
|
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 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.
|
/// Creates the SDP answer used by the client.
|
||||||
pub(crate) fn answer(
|
pub(crate) fn answer(
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
server_fingerprint: &Fingerprint,
|
server_fingerprint: Fingerprint,
|
||||||
client_ufrag: &str,
|
client_ufrag: &str,
|
||||||
) -> RTCSessionDescription {
|
) -> RTCSessionDescription {
|
||||||
RTCSessionDescription::answer(render_description(
|
RTCSessionDescription::answer(libp2p_webrtc_utils::sdp::answer(
|
||||||
SERVER_SESSION_DESCRIPTION,
|
|
||||||
addr,
|
addr,
|
||||||
server_fingerprint,
|
server_fingerprint,
|
||||||
client_ufrag,
|
client_ufrag,
|
||||||
@ -45,13 +42,16 @@ pub(crate) fn answer(
|
|||||||
///
|
///
|
||||||
/// Certificate verification is disabled which is why we hardcode a dummy fingerprint here.
|
/// Certificate verification is disabled which is why we hardcode a dummy fingerprint here.
|
||||||
pub(crate) fn offer(addr: SocketAddr, client_ufrag: &str) -> RTCSessionDescription {
|
pub(crate) fn offer(addr: SocketAddr, client_ufrag: &str) -> RTCSessionDescription {
|
||||||
RTCSessionDescription::offer(render_description(
|
let offer = render_description(
|
||||||
CLIENT_SESSION_DESCRIPTION,
|
CLIENT_SESSION_DESCRIPTION,
|
||||||
addr,
|
addr,
|
||||||
&Fingerprint::FF,
|
Fingerprint::FF,
|
||||||
client_ufrag,
|
client_ufrag,
|
||||||
))
|
);
|
||||||
.unwrap()
|
|
||||||
|
log::trace!("Created SDP offer: {offer}");
|
||||||
|
|
||||||
|
RTCSessionDescription::offer(offer).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// An SDP message that constitutes the offer.
|
// An SDP message that constitutes the offer.
|
||||||
@ -142,111 +142,3 @@ a=setup:actpass
|
|||||||
a=sctp-port:5000
|
a=sctp-port:5000
|
||||||
a=max-message-size:16384
|
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>> {
|
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()))?;
|
.ok_or_else(|| TransportError::MultiaddrNotSupported(addr.clone()))?;
|
||||||
if sock_addr.port() == 0 || sock_addr.ip().is_unspecified() {
|
if sock_addr.port() == 0 || sock_addr.ip().is_unspecified() {
|
||||||
return Err(TransportError::MultiaddrNotSupported(addr));
|
return Err(TransportError::MultiaddrNotSupported(addr));
|
||||||
@ -140,7 +140,7 @@ impl libp2p_core::Transport for Transport {
|
|||||||
sock_addr,
|
sock_addr,
|
||||||
config.inner,
|
config.inner,
|
||||||
udp_mux,
|
udp_mux,
|
||||||
client_fingerprint,
|
client_fingerprint.into_inner(),
|
||||||
server_fingerprint,
|
server_fingerprint,
|
||||||
config.id_keys,
|
config.id_keys,
|
||||||
)
|
)
|
||||||
@ -337,7 +337,7 @@ impl Stream for ListenStream {
|
|||||||
new_addr.addr,
|
new_addr.addr,
|
||||||
self.config.inner.clone(),
|
self.config.inner.clone(),
|
||||||
self.udp_mux.udp_mux_handle(),
|
self.udp_mux.udp_mux_handle(),
|
||||||
self.config.fingerprint,
|
self.config.fingerprint.into_inner(),
|
||||||
new_addr.ufrag,
|
new_addr.ufrag,
|
||||||
self.config.id_keys.clone(),
|
self.config.id_keys.clone(),
|
||||||
)
|
)
|
||||||
@ -427,40 +427,6 @@ fn parse_webrtc_listen_addr(addr: &Multiaddr) -> Option<SocketAddr> {
|
|||||||
Some(SocketAddr::new(ip, port))
|
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 //////////////////////////////////////////////////////////////////////////////////////////
|
// Tests //////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -469,7 +435,7 @@ mod tests {
|
|||||||
use futures::future::poll_fn;
|
use futures::future::poll_fn;
|
||||||
use libp2p_core::{multiaddr::Protocol, Transport as _};
|
use libp2p_core::{multiaddr::Protocol, Transport as _};
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv6Addr};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_webrtc_protocol() {
|
fn missing_webrtc_protocol() {
|
||||||
@ -480,44 +446,6 @@ mod tests {
|
|||||||
assert!(maybe_parsed.is_none());
|
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]
|
#[test]
|
||||||
fn tcp_is_invalid_protocol() {
|
fn tcp_is_invalid_protocol() {
|
||||||
let addr = "/ip4/127.0.0.1/tcp/12345/webrtc-direct/certhash/uEiDikp5KVUgkLta1EjUN-IKbHk-dUBg8VzKgf5nXxLK46w"
|
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());
|
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]
|
#[test]
|
||||||
fn can_parse_valid_addr_without_certhash() {
|
fn can_parse_valid_addr_without_certhash() {
|
||||||
let addr = "/ip6/::1/udp/12345/webrtc-direct".parse().unwrap();
|
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
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
// DEALINGS IN THE SOFTWARE.
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
mod noise;
|
use libp2p_webrtc_utils::{noise, Fingerprint};
|
||||||
|
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use futures::future::Either;
|
use futures::future::Either;
|
||||||
use futures_timer::Delay;
|
use futures_timer::Delay;
|
||||||
use libp2p_identity as identity;
|
use libp2p_identity as identity;
|
||||||
use libp2p_identity::PeerId;
|
use libp2p_identity::PeerId;
|
||||||
use rand::distributions::Alphanumeric;
|
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||||
use rand::{thread_rng, Rng};
|
|
||||||
use webrtc::api::setting_engine::SettingEngine;
|
use webrtc::api::setting_engine::SettingEngine;
|
||||||
use webrtc::api::APIBuilder;
|
use webrtc::api::APIBuilder;
|
||||||
use webrtc::data::data_channel::DataChannel;
|
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::configuration::RTCConfiguration;
|
||||||
use webrtc::peer_connection::RTCPeerConnection;
|
use webrtc::peer_connection::RTCPeerConnection;
|
||||||
|
|
||||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
use crate::tokio::sdp::random_ufrag;
|
||||||
|
use crate::tokio::{error::Error, sdp, stream::Stream, Connection};
|
||||||
use crate::tokio::{error::Error, fingerprint::Fingerprint, sdp, substream::Substream, Connection};
|
|
||||||
|
|
||||||
/// Creates a new outbound WebRTC connection.
|
/// Creates a new outbound WebRTC connection.
|
||||||
pub(crate) async fn outbound(
|
pub(crate) async fn outbound(
|
||||||
@ -59,7 +57,7 @@ pub(crate) async fn outbound(
|
|||||||
log::debug!("created SDP offer for outbound connection: {:?}", offer.sdp);
|
log::debug!("created SDP offer for outbound connection: {:?}", offer.sdp);
|
||||||
peer_connection.set_local_description(offer).await?;
|
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!(
|
log::debug!(
|
||||||
"calculated SDP answer for outbound connection: {:?}",
|
"calculated SDP answer for outbound connection: {:?}",
|
||||||
answer
|
answer
|
||||||
@ -155,18 +153,6 @@ async fn new_inbound_connection(
|
|||||||
Ok(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(
|
fn setting_engine(
|
||||||
udp_mux: Arc<dyn UDPMux + Send + Sync>,
|
udp_mux: Arc<dyn UDPMux + Send + Sync>,
|
||||||
ufrag: &str,
|
ufrag: &str,
|
||||||
@ -203,9 +189,7 @@ async fn get_remote_fingerprint(conn: &RTCPeerConnection) -> Fingerprint {
|
|||||||
Fingerprint::from_certificate(&cert_bytes)
|
Fingerprint::from_certificate(&cert_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_substream_for_noise_handshake(
|
async fn create_substream_for_noise_handshake(conn: &RTCPeerConnection) -> Result<Stream, Error> {
|
||||||
conn: &RTCPeerConnection,
|
|
||||||
) -> Result<Substream, Error> {
|
|
||||||
// NOTE: the data channel w/ `negotiated` flag set to `true` MUST be created on both ends.
|
// NOTE: the data channel w/ `negotiated` flag set to `true` MUST be created on both ends.
|
||||||
let data_channel = conn
|
let data_channel = conn
|
||||||
.create_data_channel(
|
.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.
|
drop(drop_listener); // Don't care about cancelled substreams during initial handshake.
|
||||||
|
|
||||||
Ok(substream)
|
Ok(substream)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user