├── examples ├── go-libp2p-server │ ├── .gitignore │ ├── main.go │ └── go.mod ├── README.md ├── browser-to-server │ ├── vite.config.js │ ├── package.json │ ├── index.html │ ├── README.md │ ├── index.js │ └── tests │ │ └── test.spec.js └── browser-to-browser │ ├── vite.config.js │ ├── relay.js │ ├── package.json │ ├── index.html │ ├── README.md │ ├── index.js │ └── tests │ └── test.spec.js ├── .gitignore ├── LICENSE ├── tsconfig.json ├── .github ├── workflows │ ├── stale.yml │ ├── semantic-pull-request.yml │ ├── automerge.yml │ ├── examples.yml │ └── js-test-and-release.yml └── dependabot.yml ├── src ├── private-to-public │ ├── options.ts │ ├── util.ts │ ├── sdp.ts │ └── transport.ts ├── util.ts ├── private-to-private │ ├── pb │ │ ├── message.proto │ │ └── message.ts │ ├── listener.ts │ ├── util.ts │ ├── handler.ts │ └── transport.ts ├── pb │ ├── message.proto │ └── message.ts ├── index.ts ├── maconn.ts ├── error.ts ├── muxer.ts └── stream.ts ├── test ├── util.ts ├── maconn.browser.spec.ts ├── listener.spec.ts ├── sdp.spec.ts ├── stream.spec.ts ├── basics.spec.ts ├── transport.browser.spec.ts ├── stream.browser.spec.ts └── peer.browser.spec.ts ├── LICENSE-APACHE ├── LICENSE-MIT ├── .aegir.js ├── package.json ├── README.md └── CHANGELOG.md /examples/go-libp2p-server/.gitignore: -------------------------------------------------------------------------------- 1 | go-libp2p-server -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | .docs 5 | .coverage 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | .vscode 10 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | * [Browser to Server Echo](browser-to-server/README.md): connect to a go-libp2p-webrtc server with a browser 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is dual licensed under MIT and Apache-2.0. 2 | 3 | MIT: https://www.opensource.org/licenses/mit 4 | Apache-2.0: https://www.apache.org/licenses/license-2.0 5 | -------------------------------------------------------------------------------- /examples/browser-to-server/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | build: { 3 | target: 'es2022' 4 | }, 5 | optimizeDeps: { 6 | esbuildOptions: { target: 'es2022', supported: { bigint: true } } 7 | }, 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src", 8 | "test", 9 | "proto_ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close and mark stale issue 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | stale: 9 | uses: pl-strflt/.github/.github/workflows/reusable-stale-issue.yml@v0.3 10 | -------------------------------------------------------------------------------- /src/private-to-public/options.ts: -------------------------------------------------------------------------------- 1 | import type { CreateListenerOptions, DialOptions } from '@libp2p/interface-transport' 2 | 3 | export interface WebRTCListenerOptions extends CreateListenerOptions {} 4 | export interface WebRTCDialOptions extends DialOptions {} 5 | -------------------------------------------------------------------------------- /examples/browser-to-browser/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | build: { 3 | target: 'es2022' 4 | }, 5 | optimizeDeps: { 6 | esbuildOptions: { target: 'es2022', supported: { bigint: true } } 7 | }, 8 | server: { 9 | open: true 10 | } 11 | } -------------------------------------------------------------------------------- /src/private-to-public/util.ts: -------------------------------------------------------------------------------- 1 | 2 | const charset = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') 3 | export const genUfrag = (len: number): string => [...Array(len)].map(() => charset.at(Math.floor(Math.random() * charset.length))).join('') 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | commit-message: 10 | prefix: "deps" 11 | prefix-development: "deps(dev)" 12 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Semantic PR 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | uses: pl-strflt/.github/.github/workflows/reusable-semantic-pull-request.yml@v0.3 13 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'aegir/chai' 2 | 3 | export const expectError = (error: unknown, message: string): void => { 4 | if (error instanceof Error) { 5 | expect(error.message).to.equal(message) 6 | } else { 7 | expect('Did not throw error:').to.equal(message) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # File managed by web3-bot. DO NOT EDIT. 2 | # See https://github.com/protocol/.github/ for details. 3 | 4 | name: Automerge 5 | on: [ pull_request ] 6 | 7 | jobs: 8 | automerge: 9 | uses: protocol/.github/.github/workflows/automerge.yml@master 10 | with: 11 | job: 'automerge' 12 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { detect } from 'detect-browser' 2 | 3 | const browser = detect() 4 | export const isFirefox = ((browser != null) && browser.name === 'firefox') 5 | 6 | export const nopSource = async function * nop (): AsyncGenerator {} 7 | 8 | export const nopSink = async (_: any): Promise => {} 9 | -------------------------------------------------------------------------------- /src/private-to-private/pb/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Message { 4 | // Specifies type in `data` field. 5 | enum Type { 6 | // String of `RTCSessionDescription.sdp` 7 | SDP_OFFER = 0; 8 | // String of `RTCSessionDescription.sdp` 9 | SDP_ANSWER = 1; 10 | // String of `RTCIceCandidate.toJSON()` 11 | ICE_CANDIDATE = 2; 12 | } 13 | 14 | optional Type type = 1; 15 | optional string data = 2; 16 | } -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /src/pb/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Message { 4 | enum Flag { 5 | // The sender will no longer send messages on the stream. 6 | FIN = 0; 7 | 8 | // The sender will no longer read messages on the stream. Incoming data is 9 | // being discarded on receipt. 10 | STOP_SENDING = 1; 11 | 12 | // The sender abruptly terminates the sending part of the stream. The 13 | // receiver can discard any data that it already received on that stream. 14 | RESET = 2; 15 | } 16 | 17 | optional Flag flag = 1; 18 | 19 | optional bytes message = 2; 20 | } 21 | -------------------------------------------------------------------------------- /examples/browser-to-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-libp2p-webrtc-browser-to-server", 3 | "version": "1.0.0", 4 | "description": "Connect a browser to a server", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "vite build", 9 | "go-libp2p-server": "cd ../go-libp2p-server && go run ./main.go", 10 | "test:chrome": "npm run build && playwright test tests", 11 | "test:firefox": "npm run build && playwright test --browser firefox tests", 12 | "test": "npm run build && test-browser-example tests" 13 | }, 14 | "dependencies": { 15 | "@chainsafe/libp2p-noise": "^12.0.0", 16 | "@libp2p/webrtc": "file:../../", 17 | "@multiformats/multiaddr": "^12.0.0", 18 | "it-pushable": "^3.1.0", 19 | "libp2p": "^0.45.0", 20 | "vite": "^4.2.1" 21 | }, 22 | "devDependencies": { 23 | "test-ipfs-example": "^1.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/browser-to-browser/relay.js: -------------------------------------------------------------------------------- 1 | import { mplex } from "@libp2p/mplex" 2 | import { createLibp2p } from "libp2p" 3 | import { noise } from "@chainsafe/libp2p-noise" 4 | import { circuitRelayServer } from 'libp2p/circuit-relay' 5 | import { webSockets } from '@libp2p/websockets' 6 | import * as filters from '@libp2p/websockets/filters' 7 | import { identifyService } from 'libp2p/identify' 8 | 9 | const server = await createLibp2p({ 10 | addresses: { 11 | listen: ['/ip4/127.0.0.1/tcp/0/ws'] 12 | }, 13 | transports: [ 14 | webSockets({ 15 | filter: filters.all 16 | }), 17 | ], 18 | connectionEncryption: [noise()], 19 | streamMuxers: [mplex()], 20 | services: { 21 | identify: identifyService(), 22 | relay: circuitRelayServer() 23 | } 24 | }) 25 | 26 | console.log("p2p addr: ", server.getMultiaddrs().map((ma) => ma.toString())) 27 | -------------------------------------------------------------------------------- /examples/browser-to-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-libp2p-webrtc-private-to-private", 3 | "version": "1.0.0", 4 | "description": "Connect a browser to another browser", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "vite build", 9 | "relay": "node relay.js", 10 | "test:firefox": "npm run build && playwright test --browser=firefox tests", 11 | "test:chrome": "npm run build && playwright test tests", 12 | "test": "npm run build && test-browser-example tests" 13 | }, 14 | "dependencies": { 15 | "@chainsafe/libp2p-noise": "^12.0.0", 16 | "@libp2p/websockets": "^6.0.1", 17 | "@libp2p/mplex": "^8.0.1", 18 | "@libp2p/webrtc": "file:../../", 19 | "@multiformats/multiaddr": "^12.0.0", 20 | "it-pushable": "^3.1.0", 21 | "libp2p": "^0.45.0", 22 | "vite": "^4.2.1" 23 | }, 24 | "devDependencies": { 25 | "test-ipfs-example": "^1.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/browser-to-server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | js-libp2p WebRTC 7 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 |
38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | concurrency: 11 | group: ${{ github.head_ref || github.ref_name }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | examples: 16 | runs-on: ubuntu-latest 17 | name: Test example ${{ matrix.project }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | project: 22 | - browser-to-server 23 | - browser-to-browser 24 | defaults: 25 | run: 26 | working-directory: examples/${{ matrix.project }} 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: lts/* 32 | - uses: actions/setup-go@v3 33 | with: 34 | go-version: '1.19' 35 | - name: Install dependencies 36 | run: npm install 37 | working-directory: . 38 | - name: Build root 39 | run: npm run build 40 | working-directory: . 41 | - name: Install dependencies 42 | run: npm install 43 | - name: Run tests 44 | run: npm run test 45 | env: 46 | CI: true 47 | -------------------------------------------------------------------------------- /test/maconn.browser.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-expressions */ 2 | 3 | import { multiaddr } from '@multiformats/multiaddr' 4 | import { expect } from 'aegir/chai' 5 | import { stubObject } from 'sinon-ts' 6 | import { WebRTCMultiaddrConnection } from './../src/maconn.js' 7 | import type { CounterGroup } from '@libp2p/interface-metrics' 8 | 9 | describe('Multiaddr Connection', () => { 10 | it('can open and close', async () => { 11 | const peerConnection = new RTCPeerConnection() 12 | peerConnection.createDataChannel('whatever', { negotiated: true, id: 91 }) 13 | const remoteAddr = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') 14 | const metrics = stubObject({ 15 | increment: () => {}, 16 | reset: () => {} 17 | }) 18 | const maConn = new WebRTCMultiaddrConnection({ 19 | peerConnection, 20 | remoteAddr, 21 | timeline: { 22 | open: (new Date()).getTime() 23 | }, 24 | metrics 25 | }) 26 | 27 | expect(maConn.timeline.close).to.be.undefined 28 | 29 | await maConn.close() 30 | 31 | expect(maConn.timeline.close).to.not.be.undefined 32 | expect(metrics.increment.calledWith({ close: true })).to.be.true 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /examples/browser-to-server/README.md: -------------------------------------------------------------------------------- 1 | # js-libp2p-webrtc Browser to Server 2 | 3 | This example leverages the [vite bundler](https://vitejs.dev/) to compile and serve the libp2p code in the browser. You can use other bundlers such as Webpack, but we will not be covering them here. 4 | 5 | ## Running the Go Server 6 | 7 | To run the Go LibP2P WebRTC server: 8 | 9 | ```shell 10 | npm run go-libp2p-server 11 | ``` 12 | 13 | Copy the multiaddress in the output. 14 | 15 | ## Running the Example 16 | 17 | In a separate console tab, install dependencies and start the Vite server: 18 | 19 | ```shell 20 | npm i && npm run start 21 | ``` 22 | 23 | The browser window will automatically open. 24 | Using the copied multiaddress from the Go server, paste it into the `Server MultiAddress` input and click the `Connect` button. 25 | Once the peer is connected, click the message section will appear. Enter a message and click the `Send` button. 26 | 27 | The output should look like: 28 | 29 | ```text 30 | Dialing /ip4/10.0.1.5/udp/54375/webrtc/certhash/uEiADy8JubdWrAzseyzfXFyCpdRN02eWZg86tjCrTCA5dbQ/p2p/12D3KooWEG7N4bnZfFBNZE7WG6xm2P4Sr6sonMwyD4HCAqApEthb 31 | Peer connected '/ip4/10.0.1.5/udp/54375/webrtc/certhash/uEiADy8JubdWrAzseyzfXFyCpdRN02eWZg86tjCrTCA5dbQ/p2p/12D3KooWEG7N4bnZfFBNZE7WG6xm2P4Sr6sonMwyD4HCAqApEthb' 32 | Sending message 'hello' 33 | Received message 'hello' 34 | ``` -------------------------------------------------------------------------------- /src/private-to-private/listener.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@libp2p/interfaces/events' 2 | import { Circuit } from '@multiformats/mafmt' 3 | import type { PeerId } from '@libp2p/interface-peer-id' 4 | import type { ListenerEvents, Listener, TransportManager } from '@libp2p/interface-transport' 5 | import type { Multiaddr } from '@multiformats/multiaddr' 6 | 7 | export interface ListenerOptions { 8 | peerId: PeerId 9 | transportManager: TransportManager 10 | } 11 | 12 | export class WebRTCPeerListener extends EventEmitter implements Listener { 13 | private readonly peerId: PeerId 14 | private readonly transportManager: TransportManager 15 | 16 | constructor (opts: ListenerOptions) { 17 | super() 18 | 19 | this.peerId = opts.peerId 20 | this.transportManager = opts.transportManager 21 | } 22 | 23 | async listen (): Promise { 24 | this.safeDispatchEvent('listening', {}) 25 | } 26 | 27 | getAddrs (): Multiaddr[] { 28 | return this.transportManager 29 | .getListeners() 30 | .filter(l => l !== this) 31 | .map(l => l.getAddrs() 32 | .filter(ma => Circuit.matches(ma)) 33 | .map(ma => { 34 | return ma.encapsulate(`/webrtc/p2p/${this.peerId}`) 35 | }) 36 | ) 37 | .flat() 38 | } 39 | 40 | async close (): Promise { 41 | this.safeDispatchEvent('close', {}) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/browser-to-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | js-libp2p WebRTC 7 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 |
38 |

Active Connections:

39 |
    40 |
    41 |
    42 |

    Listening addresses:

    43 |
      44 |
      45 |
      46 |
      47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/listener.spec.ts: -------------------------------------------------------------------------------- 1 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 2 | import { multiaddr } from '@multiformats/multiaddr' 3 | import { expect } from 'aegir/chai' 4 | import { stubInterface } from 'sinon-ts' 5 | import { WebRTCPeerListener } from '../src/private-to-private/listener' 6 | import type { Listener, TransportManager } from '@libp2p/interface-transport' 7 | 8 | describe('webrtc private-to-private listener', () => { 9 | it('should only return relay addresses as webrtc listen addresses', async () => { 10 | const relayedAddress = '/ip4/127.0.0.1/tcp/4034/ws/p2p-circuit' 11 | const otherListenAddress = '/ip4/127.0.0.1/tcp/4001' 12 | const peerId = await createEd25519PeerId() 13 | const transportManager = stubInterface() 14 | 15 | const listener = new WebRTCPeerListener({ 16 | peerId, 17 | transportManager 18 | }) 19 | 20 | const otherListener = stubInterface({ 21 | getAddrs: [multiaddr(otherListenAddress)] 22 | }) 23 | 24 | const relayListener = stubInterface({ 25 | getAddrs: [multiaddr(relayedAddress)] 26 | }) 27 | 28 | transportManager.getListeners.returns([ 29 | listener, 30 | otherListener, 31 | relayListener 32 | ]) 33 | 34 | const addresses = listener.getAddrs() 35 | 36 | expect(addresses.map(ma => ma.toString())).to.deep.equal([ 37 | `${relayedAddress}/webrtc/p2p/${peerId}` 38 | ]) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /.aegir.js: -------------------------------------------------------------------------------- 1 | import { createLibp2p } from 'libp2p' 2 | import { circuitRelayServer } from 'libp2p/circuit-relay' 3 | import { identifyService } from 'libp2p/identify' 4 | import { webSockets } from '@libp2p/websockets' 5 | import { noise } from '@chainsafe/libp2p-noise' 6 | import { yamux } from '@chainsafe/libp2p-yamux' 7 | 8 | export default { 9 | build: { 10 | config: { 11 | platform: 'node' 12 | }, 13 | bundlesizeMax: '117KB' 14 | }, 15 | test: { 16 | before: async () => { 17 | // start a relay node for use in the tests 18 | const relay = await createLibp2p({ 19 | addresses: { 20 | listen: [ 21 | '/ip4/127.0.0.1/tcp/0/ws' 22 | ] 23 | }, 24 | transports: [ 25 | webSockets() 26 | ], 27 | connectionEncryption: [ 28 | noise() 29 | ], 30 | streamMuxers: [ 31 | yamux() 32 | ], 33 | services: { 34 | relay: circuitRelayServer({ 35 | reservations: { 36 | maxReservations: Infinity 37 | } 38 | }), 39 | identify: identifyService() 40 | }, 41 | connectionManager: { 42 | minConnections: 0 43 | } 44 | }) 45 | 46 | const multiaddrs = relay.getMultiaddrs().map(ma => ma.toString()) 47 | 48 | return { 49 | relay, 50 | env: { 51 | RELAY_MULTIADDR: multiaddrs[0] 52 | } 53 | } 54 | }, 55 | after: async (_, before) => { 56 | await before.relay.stop() 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/private-to-private/util.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@libp2p/logger' 2 | import { isFirefox } from '../util.js' 3 | import { Message } from './pb/message.js' 4 | import type { DeferredPromise } from 'p-defer' 5 | 6 | interface MessageStream { 7 | read: () => Promise 8 | write: (d: Message) => void | Promise 9 | } 10 | 11 | const log = logger('libp2p:webrtc:peer:util') 12 | 13 | export const readCandidatesUntilConnected = async (connectedPromise: DeferredPromise, pc: RTCPeerConnection, stream: MessageStream): Promise => { 14 | while (true) { 15 | const readResult = await Promise.race([connectedPromise.promise, stream.read()]) 16 | // check if readResult is a message 17 | if (readResult instanceof Object) { 18 | const message = readResult 19 | if (message.type !== Message.Type.ICE_CANDIDATE) { 20 | throw new Error('expected only ice candidates') 21 | } 22 | // end of candidates has been signalled 23 | if (message.data == null || message.data === '') { 24 | log.trace('end-of-candidates received') 25 | break 26 | } 27 | 28 | log.trace('received new ICE candidate: %s', message.data) 29 | try { 30 | await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(message.data))) 31 | } catch (err) { 32 | log.error('bad candidate received: ', err) 33 | throw new Error('bad candidate received') 34 | } 35 | } else { 36 | // connected promise resolved 37 | break 38 | } 39 | } 40 | await connectedPromise.promise 41 | } 42 | 43 | export function resolveOnConnected (pc: RTCPeerConnection, promise: DeferredPromise): void { 44 | pc[isFirefox ? 'oniceconnectionstatechange' : 'onconnectionstatechange'] = (_) => { 45 | log.trace('receiver peerConnectionState state: ', pc.connectionState) 46 | switch (isFirefox ? pc.iceConnectionState : pc.connectionState) { 47 | case 'connected': 48 | promise.resolve() 49 | break 50 | case 'failed': 51 | case 'disconnected': 52 | case 'closed': 53 | promise.reject(new Error('RTCPeerConnection was closed')) 54 | break 55 | default: 56 | break 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/go-libp2p-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/libp2p/go-libp2p" 13 | "github.com/libp2p/go-libp2p/core/host" 14 | "github.com/libp2p/go-libp2p/core/network" 15 | "github.com/libp2p/go-libp2p/core/peer" 16 | webrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" 17 | ) 18 | 19 | var listenerIp = net.IPv4(127, 0, 0, 1) 20 | 21 | func init() { 22 | ifaces, err := net.Interfaces() 23 | if err != nil { 24 | return 25 | } 26 | for _, iface := range ifaces { 27 | if iface.Flags&net.FlagUp == 0 { 28 | continue 29 | } 30 | addrs, err := iface.Addrs() 31 | if err != nil { 32 | return 33 | } 34 | for _, addr := range addrs { 35 | // bind to private non-loopback ip 36 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.IsPrivate() { 37 | if ipnet.IP.To4() != nil { 38 | listenerIp = ipnet.IP.To4() 39 | return 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | func echoHandler(stream network.Stream) { 47 | for { 48 | reader := bufio.NewReader(stream) 49 | str, err := reader.ReadString('\n') 50 | log.Printf("err: %s", err) 51 | if err != nil { 52 | return 53 | } 54 | log.Printf("echo: %s", str) 55 | _, err = stream.Write([]byte(str)) 56 | if err != nil { 57 | log.Printf("err: %v", err) 58 | return 59 | } 60 | } 61 | } 62 | 63 | func main() { 64 | host := createHost() 65 | host.SetStreamHandler("/echo/1.0.0", echoHandler) 66 | defer host.Close() 67 | remoteInfo := peer.AddrInfo{ 68 | ID: host.ID(), 69 | Addrs: host.Network().ListenAddresses(), 70 | } 71 | 72 | remoteAddrs, _ := peer.AddrInfoToP2pAddrs(&remoteInfo) 73 | fmt.Println("p2p addr: ", remoteAddrs[0]) 74 | 75 | fmt.Println("press Ctrl+C to quit") 76 | ch := make(chan os.Signal, 1) 77 | signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) 78 | <-ch 79 | } 80 | 81 | func createHost() host.Host { 82 | h, err := libp2p.New( 83 | libp2p.Transport(webrtc.New), 84 | libp2p.ListenAddrStrings( 85 | fmt.Sprintf("/ip4/%s/udp/0/webrtc-direct", listenerIp), 86 | ), 87 | libp2p.DisableRelay(), 88 | libp2p.Ping(true), 89 | ) 90 | if err != nil { 91 | panic(err) 92 | } 93 | 94 | return h 95 | } 96 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { WebRTCTransport } from './private-to-private/transport.js' 2 | import { WebRTCDirectTransport, type WebRTCTransportDirectInit, type WebRTCDirectTransportComponents } from './private-to-public/transport.js' 3 | import type { WebRTCTransportComponents, WebRTCTransportInit } from './private-to-private/transport.js' 4 | import type { Transport } from '@libp2p/interface-transport' 5 | 6 | /** 7 | * @param {WebRTCTransportDirectInit} init - WebRTC direct transport configuration 8 | * @param init.dataChannel - DataChannel configurations 9 | * @param {number} init.dataChannel.maxMessageSize - Max message size that can be sent through the DataChannel. Larger messages will be chunked into smaller messages below this size (default 16kb) 10 | * @param {number} init.dataChannel.maxBufferedAmount - Max buffered amount a DataChannel can have (default 16mb) 11 | * @param {number} init.dataChannel.bufferedAmountLowEventTimeout - If max buffered amount is reached, this is the max time that is waited before the buffer is cleared (default 30 seconds) 12 | * @returns 13 | */ 14 | function webRTCDirect (init?: WebRTCTransportDirectInit): (components: WebRTCDirectTransportComponents) => Transport { 15 | return (components: WebRTCDirectTransportComponents) => new WebRTCDirectTransport(components, init) 16 | } 17 | 18 | /** 19 | * @param {WebRTCTransportInit} init - WebRTC transport configuration 20 | * @param {RTCConfiguration} init.rtcConfiguration - RTCConfiguration 21 | * @param init.dataChannel - DataChannel configurations 22 | * @param {number} init.dataChannel.maxMessageSize - Max message size that can be sent through the DataChannel. Larger messages will be chunked into smaller messages below this size (default 16kb) 23 | * @param {number} init.dataChannel.maxBufferedAmount - Max buffered amount a DataChannel can have (default 16mb) 24 | * @param {number} init.dataChannel.bufferedAmountLowEventTimeout - If max buffered amount is reached, this is the max time that is waited before the buffer is cleared (default 30 seconds) 25 | * @returns 26 | */ 27 | function webRTC (init?: WebRTCTransportInit): (components: WebRTCTransportComponents) => Transport { 28 | return (components: WebRTCTransportComponents) => new WebRTCTransport(components, init) 29 | } 30 | 31 | export { webRTC, webRTCDirect } 32 | -------------------------------------------------------------------------------- /examples/browser-to-server/index.js: -------------------------------------------------------------------------------- 1 | import { createLibp2p } from 'libp2p' 2 | import { noise } from '@chainsafe/libp2p-noise' 3 | import { multiaddr } from '@multiformats/multiaddr' 4 | import { pipe } from "it-pipe"; 5 | import { fromString, toString } from "uint8arrays"; 6 | import { webRTCDirect } from '@libp2p/webrtc' 7 | import { pushable } from 'it-pushable'; 8 | 9 | let stream; 10 | const output = document.getElementById('output') 11 | const sendSection = document.getElementById('send-section') 12 | const appendOutput = (line) => { 13 | const div = document.createElement("div") 14 | div.appendChild(document.createTextNode(line)) 15 | output.append(div) 16 | } 17 | const clean = (line) => line.replaceAll('\n', '') 18 | const sender = pushable() 19 | 20 | const node = await createLibp2p({ 21 | transports: [webRTCDirect()], 22 | connectionEncryption: [noise()], 23 | connectionGater: { 24 | denyDialMultiaddr: () => { 25 | // by default we refuse to dial local addresses from the browser since they 26 | // are usually sent by remote peers broadcasting undialable multiaddrs but 27 | // here we are explicitly connecting to a local node so do not deny dialing 28 | // any discovered address 29 | return false 30 | } 31 | } 32 | }); 33 | 34 | await node.start() 35 | 36 | node.addEventListener('peer:connect', (connection) => { 37 | appendOutput(`Peer connected '${node.getConnections().map(c => c.remoteAddr.toString())}'`) 38 | sendSection.style.display = 'block' 39 | }) 40 | 41 | window.connect.onclick = async () => { 42 | // TODO!!(ckousik): hack until webrtc is renamed in Go. Remove once 43 | // complete 44 | let candidateMa = window.peer.value 45 | candidateMa = candidateMa.replace(/\/webrtc\/certhash/, "/webrtc-direct/certhash") 46 | const ma = multiaddr(candidateMa) 47 | 48 | appendOutput(`Dialing '${ma}'`) 49 | stream = await node.dialProtocol(ma, ['/echo/1.0.0']) 50 | pipe(sender, stream, async (src) => { 51 | for await(const buf of src) { 52 | const response = toString(buf.subarray()) 53 | appendOutput(`Received message '${clean(response)}'`) 54 | } 55 | }) 56 | } 57 | 58 | window.send.onclick = async () => { 59 | const message = `${window.message.value}\n` 60 | appendOutput(`Sending message '${clean(message)}'`) 61 | sender.push(fromString(message)) 62 | } 63 | -------------------------------------------------------------------------------- /src/pb/message.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/export */ 2 | /* eslint-disable complexity */ 3 | /* eslint-disable @typescript-eslint/no-namespace */ 4 | /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ 5 | /* eslint-disable @typescript-eslint/no-empty-interface */ 6 | 7 | import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' 8 | import type { Codec } from 'protons-runtime' 9 | import type { Uint8ArrayList } from 'uint8arraylist' 10 | 11 | export interface Message { 12 | flag?: Message.Flag 13 | message?: Uint8Array 14 | } 15 | 16 | export namespace Message { 17 | export enum Flag { 18 | FIN = 'FIN', 19 | STOP_SENDING = 'STOP_SENDING', 20 | RESET = 'RESET' 21 | } 22 | 23 | enum __FlagValues { 24 | FIN = 0, 25 | STOP_SENDING = 1, 26 | RESET = 2 27 | } 28 | 29 | export namespace Flag { 30 | export const codec = (): Codec => { 31 | return enumeration(__FlagValues) 32 | } 33 | } 34 | 35 | let _codec: Codec 36 | 37 | export const codec = (): Codec => { 38 | if (_codec == null) { 39 | _codec = message((obj, w, opts = {}) => { 40 | if (opts.lengthDelimited !== false) { 41 | w.fork() 42 | } 43 | 44 | if (obj.flag != null) { 45 | w.uint32(8) 46 | Message.Flag.codec().encode(obj.flag, w) 47 | } 48 | 49 | if (obj.message != null) { 50 | w.uint32(18) 51 | w.bytes(obj.message) 52 | } 53 | 54 | if (opts.lengthDelimited !== false) { 55 | w.ldelim() 56 | } 57 | }, (reader, length) => { 58 | const obj: any = {} 59 | 60 | const end = length == null ? reader.len : reader.pos + length 61 | 62 | while (reader.pos < end) { 63 | const tag = reader.uint32() 64 | 65 | switch (tag >>> 3) { 66 | case 1: 67 | obj.flag = Message.Flag.codec().decode(reader) 68 | break 69 | case 2: 70 | obj.message = reader.bytes() 71 | break 72 | default: 73 | reader.skipType(tag & 7) 74 | break 75 | } 76 | } 77 | 78 | return obj 79 | }) 80 | } 81 | 82 | return _codec 83 | } 84 | 85 | export const encode = (obj: Partial): Uint8Array => { 86 | return encodeMessage(obj, Message.codec()) 87 | } 88 | 89 | export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { 90 | return decodeMessage(buf, Message.codec()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/private-to-private/pb/message.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/export */ 2 | /* eslint-disable complexity */ 3 | /* eslint-disable @typescript-eslint/no-namespace */ 4 | /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ 5 | /* eslint-disable @typescript-eslint/no-empty-interface */ 6 | 7 | import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' 8 | import type { Codec } from 'protons-runtime' 9 | import type { Uint8ArrayList } from 'uint8arraylist' 10 | 11 | export interface Message { 12 | type?: Message.Type 13 | data?: string 14 | } 15 | 16 | export namespace Message { 17 | export enum Type { 18 | SDP_OFFER = 'SDP_OFFER', 19 | SDP_ANSWER = 'SDP_ANSWER', 20 | ICE_CANDIDATE = 'ICE_CANDIDATE' 21 | } 22 | 23 | enum __TypeValues { 24 | SDP_OFFER = 0, 25 | SDP_ANSWER = 1, 26 | ICE_CANDIDATE = 2 27 | } 28 | 29 | export namespace Type { 30 | export const codec = (): Codec => { 31 | return enumeration(__TypeValues) 32 | } 33 | } 34 | 35 | let _codec: Codec 36 | 37 | export const codec = (): Codec => { 38 | if (_codec == null) { 39 | _codec = message((obj, w, opts = {}) => { 40 | if (opts.lengthDelimited !== false) { 41 | w.fork() 42 | } 43 | 44 | if (obj.type != null) { 45 | w.uint32(8) 46 | Message.Type.codec().encode(obj.type, w) 47 | } 48 | 49 | if (obj.data != null) { 50 | w.uint32(18) 51 | w.string(obj.data) 52 | } 53 | 54 | if (opts.lengthDelimited !== false) { 55 | w.ldelim() 56 | } 57 | }, (reader, length) => { 58 | const obj: any = {} 59 | 60 | const end = length == null ? reader.len : reader.pos + length 61 | 62 | while (reader.pos < end) { 63 | const tag = reader.uint32() 64 | 65 | switch (tag >>> 3) { 66 | case 1: 67 | obj.type = Message.Type.codec().decode(reader) 68 | break 69 | case 2: 70 | obj.data = reader.string() 71 | break 72 | default: 73 | reader.skipType(tag & 7) 74 | break 75 | } 76 | } 77 | 78 | return obj 79 | }) 80 | } 81 | 82 | return _codec 83 | } 84 | 85 | export const encode = (obj: Partial): Uint8Array => { 86 | return encodeMessage(obj, Message.codec()) 87 | } 88 | 89 | export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { 90 | return decodeMessage(buf, Message.codec()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/maconn.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@libp2p/logger' 2 | import { nopSink, nopSource } from './util.js' 3 | import type { MultiaddrConnection, MultiaddrConnectionTimeline } from '@libp2p/interface-connection' 4 | import type { CounterGroup } from '@libp2p/interface-metrics' 5 | import type { Multiaddr } from '@multiformats/multiaddr' 6 | import type { Source, Sink } from 'it-stream-types' 7 | 8 | const log = logger('libp2p:webrtc:connection') 9 | 10 | interface WebRTCMultiaddrConnectionInit { 11 | /** 12 | * WebRTC Peer Connection 13 | */ 14 | peerConnection: RTCPeerConnection 15 | 16 | /** 17 | * The multiaddr address used to communicate with the remote peer 18 | */ 19 | remoteAddr: Multiaddr 20 | 21 | /** 22 | * Holds the relevant events timestamps of the connection 23 | */ 24 | timeline: MultiaddrConnectionTimeline 25 | 26 | /** 27 | * Optional metrics counter group for this connection 28 | */ 29 | metrics?: CounterGroup 30 | } 31 | 32 | export class WebRTCMultiaddrConnection implements MultiaddrConnection { 33 | /** 34 | * WebRTC Peer Connection 35 | */ 36 | readonly peerConnection: RTCPeerConnection 37 | 38 | /** 39 | * The multiaddr address used to communicate with the remote peer 40 | */ 41 | remoteAddr: Multiaddr 42 | 43 | /** 44 | * Holds the lifecycle times of the connection 45 | */ 46 | timeline: MultiaddrConnectionTimeline 47 | 48 | /** 49 | * Optional metrics counter group for this connection 50 | */ 51 | metrics?: CounterGroup 52 | 53 | /** 54 | * The stream source, a no-op as the transport natively supports multiplexing 55 | */ 56 | source: AsyncGenerator = nopSource() 57 | 58 | /** 59 | * The stream destination, a no-op as the transport natively supports multiplexing 60 | */ 61 | sink: Sink, Promise> = nopSink 62 | 63 | constructor (init: WebRTCMultiaddrConnectionInit) { 64 | this.remoteAddr = init.remoteAddr 65 | this.timeline = init.timeline 66 | this.peerConnection = init.peerConnection 67 | 68 | this.peerConnection.onconnectionstatechange = () => { 69 | if (this.peerConnection.connectionState === 'closed' || this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed') { 70 | this.timeline.close = Date.now() 71 | } 72 | } 73 | } 74 | 75 | async close (err?: Error | undefined): Promise { 76 | if (err !== undefined) { 77 | log.error('error closing connection', err) 78 | } 79 | log.trace('closing connection') 80 | 81 | this.timeline.close = Date.now() 82 | this.peerConnection.close() 83 | this.metrics?.increment({ close: true }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/browser-to-server/tests/test.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { setup, expect } from 'test-ipfs-example/browser' 3 | import { spawn, exec } from 'child_process' 4 | import { existsSync } from 'fs' 5 | 6 | // Setup 7 | const test = setup() 8 | 9 | async function spawnGoLibp2p() { 10 | if (!existsSync('../../examples/go-libp2p-server/go-libp2p-server')) { 11 | await new Promise((resolve, reject) => { 12 | exec('go build', 13 | { cwd: '../../examples/go-libp2p-server' }, 14 | (error, stdout, stderr) => { 15 | if (error) { 16 | throw (`exec error: ${error}`) 17 | } 18 | resolve() 19 | }) 20 | }) 21 | } 22 | 23 | const server = spawn('./go-libp2p-server', [], { cwd: '../../examples/go-libp2p-server', killSignal: 'SIGINT' }) 24 | server.stderr.on('data', (data) => { 25 | console.log(`stderr: ${data}`, typeof data) 26 | }) 27 | const serverAddr = await (new Promise(resolve => { 28 | server.stdout.on('data', (data) => { 29 | console.log(`stdout: ${data}`, typeof data) 30 | const addr = String(data).match(/p2p addr: ([^\s]*)/) 31 | if (addr !== null && addr.length > 0) { 32 | resolve(addr[1]) 33 | } 34 | }) 35 | })) 36 | return { server, serverAddr } 37 | } 38 | 39 | test.describe('bundle ipfs with parceljs:', () => { 40 | // DOM 41 | const connectBtn = '#connect' 42 | const connectAddr = '#peer' 43 | const messageInput = '#message' 44 | const sendBtn = '#send' 45 | const output = '#output' 46 | 47 | let server 48 | let serverAddr 49 | 50 | // eslint-disable-next-line no-empty-pattern 51 | test.beforeAll(async ({ }, testInfo) => { 52 | testInfo.setTimeout(5 * 60_000) 53 | const s = await spawnGoLibp2p() 54 | server = s.server 55 | serverAddr = s.serverAddr 56 | console.log('Server addr:', serverAddr) 57 | }, {}) 58 | 59 | test.afterAll(() => { 60 | server.kill('SIGINT') 61 | }) 62 | 63 | test.beforeEach(async ({ servers, page }) => { 64 | await page.goto(servers[0].url) 65 | }) 66 | 67 | test('should connect to a go-libp2p node over webrtc', async ({ page }) => { 68 | const message = 'hello' 69 | 70 | // add the go libp2p multiaddress to the input field and submit 71 | await page.fill(connectAddr, serverAddr) 72 | await page.click(connectBtn) 73 | 74 | // send the relay message to the go libp2p server 75 | await page.fill(messageInput, message) 76 | await page.click(sendBtn) 77 | 78 | await page.waitForSelector('#output:has(div)') 79 | 80 | // Expected output: 81 | // 82 | // Dialing '${serverAddr}' 83 | // Peer connected '${serverAddr}' 84 | // Sending message '${message}' 85 | // Received message '${message}' 86 | const connections = await page.textContent(output) 87 | 88 | expect(connections).toContain(`Dialing '${serverAddr}'`) 89 | expect(connections).toContain(`Peer connected '${serverAddr}'`) 90 | 91 | expect(connections).toContain(`Sending message '${message}'`) 92 | expect(connections).toContain(`Received message '${message}'`) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /examples/browser-to-browser/README.md: -------------------------------------------------------------------------------- 1 | # js-libp2p-webrtc Browser to Browser 2 | 3 | This example leverages the [vite bundler](https://vitejs.dev/) to compile and serve the libp2p code in the browser. You can use other bundlers such as Webpack, but we will not be covering them here. 4 | 5 | ## Build the `@libp2p/webrtc` package 6 | 7 | Build the `@libp2p/webrtc` package by calling `npm i && npm run build` in the repository root. 8 | 9 | ## Running the Relay Server 10 | 11 | For browsers to communicate, we first need to run the LibP2P relay server: 12 | 13 | ```shell 14 | npm run relay 15 | ``` 16 | 17 | Copy one of the multiaddresses in the output. 18 | 19 | ## Running the Example 20 | 21 | In a separate console tab, install dependencies and start the Vite server: 22 | 23 | ```shell 24 | npm i && npm run start 25 | ``` 26 | 27 | The browser window will automatically open. Let's call this `Browser A`. 28 | Using the copied multiaddress from the Go or NodeJS relay server, paste it into the `Remote MultiAddress` input and click the `Connect` button. 29 | `Browser A` is now connected to the relay server. 30 | Copy the multiaddress located after the `Listening on` message. 31 | 32 | Now open a second browser with the url `http://localhost:5173/`. Let's call this `Browser B`. 33 | Using the copied multiaddress from `Listening on` section in `Browser A`, paste it into the `Remote MultiAddress` input and click the `Connect` button. 34 | `Browser B` is now connected to `Browser A`. 35 | Copy the multiaddress located after the `Listening on` message. 36 | 37 | Using the copied multiaddress from `Listening on` section in `Browser B`, paste it into the `Remote MultiAddress` input in `Browser A` and click the `Connect` button. 38 | `Browser A` is now connected to `Browser B`. 39 | 40 | The peers are now connected to each other. Enter a message and click the `Send` button in either/both browsers and see the echo'd messages. 41 | 42 | The output should look like: 43 | 44 | `Browser A` 45 | ```text 46 | Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk' 47 | Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-webrtc-direct/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC 48 | Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-webrtc-direct/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9' 49 | Sending message 'helloa' 50 | Received message 'helloa' 51 | Received message 'hellob' 52 | ``` 53 | 54 | `Browser B` 55 | ```text 56 | Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-webrtc-direct/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC' 57 | Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-webrtc-direct/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9 58 | Received message 'helloa' 59 | Sending message 'hellob' 60 | Received message 'hellob' 61 | ``` 62 | -------------------------------------------------------------------------------- /test/sdp.spec.ts: -------------------------------------------------------------------------------- 1 | import { multiaddr } from '@multiformats/multiaddr' 2 | import { expect } from 'aegir/chai' 3 | import * as underTest from '../src/private-to-public/sdp.js' 4 | 5 | const sampleMultiAddr = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ') 6 | const sampleCerthash = 'uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ' 7 | const sampleSdp = `v=0 8 | o=- 0 0 IN IP4 0.0.0.0 9 | s=- 10 | c=IN IP4 0.0.0.0 11 | t=0 0 12 | a=ice-lite 13 | m=application 56093 UDP/DTLS/SCTP webrtc-datachannel 14 | a=mid:0 15 | a=setup:passive 16 | a=ice-ufrag:MyUserFragment 17 | a=ice-pwd:MyUserFragment 18 | a=fingerprint:SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1 19 | a=sctp-port:5000 20 | a=max-message-size:100000 21 | a=candidate:1467250027 1 UDP 1467250027 0.0.0.0 56093 typ host` 22 | 23 | describe('SDP', () => { 24 | it('converts multiaddr with certhash to an answer SDP', async () => { 25 | const ufrag = 'MyUserFragment' 26 | const sdp = underTest.fromMultiAddr(sampleMultiAddr, ufrag) 27 | 28 | expect(sdp.sdp).to.contain(sampleSdp) 29 | }) 30 | 31 | it('extracts certhash from a multiaddr', () => { 32 | const certhash = underTest.certhash(sampleMultiAddr) 33 | 34 | expect(certhash).to.equal(sampleCerthash) 35 | }) 36 | 37 | it('decodes a certhash', () => { 38 | const decoded = underTest.decodeCerthash(sampleCerthash) 39 | 40 | // sha2-256 multihash 0x12 permanent 41 | // https://github.com/multiformats/multicodec/blob/master/table.csv 42 | expect(decoded.name).to.equal('sha2-256') 43 | expect(decoded.code).to.equal(0x12) 44 | expect(decoded.length).to.equal(32) 45 | expect(decoded.digest.toString()).to.equal('114,104,71,205,72,176,94,197,96,77,21,156,191,64,29,111,0,161,35,236,144,23,14,44,209,179,143,210,157,55,229,177') 46 | }) 47 | 48 | it('converts a multiaddr into a fingerprint', () => { 49 | const fingerpint = underTest.ma2Fingerprint(sampleMultiAddr) 50 | expect(fingerpint).to.deep.equal([ 51 | 'SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1', 52 | '726847cd48b05ec5604d159cbf401d6f00a123ec90170e2cd1b38fd29d37e5b1' 53 | ]) 54 | }) 55 | 56 | it('extracts a fingerprint from sdp', () => { 57 | const fingerprint = underTest.getFingerprintFromSdp(sampleSdp) 58 | expect(fingerprint).to.eq('72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1') 59 | }) 60 | 61 | it('munges the ufrag and pwd in a SDP', () => { 62 | const result = underTest.munge({ type: 'answer', sdp: sampleSdp }, 'someotheruserfragmentstring') 63 | const expected = `v=0 64 | o=- 0 0 IN IP4 0.0.0.0 65 | s=- 66 | c=IN IP4 0.0.0.0 67 | t=0 0 68 | a=ice-lite 69 | m=application 56093 UDP/DTLS/SCTP webrtc-datachannel 70 | a=mid:0 71 | a=setup:passive 72 | a=ice-ufrag:someotheruserfragmentstring 73 | a=ice-pwd:someotheruserfragmentstring 74 | a=fingerprint:SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1 75 | a=sctp-port:5000 76 | a=max-message-size:100000 77 | a=candidate:1467250027 1 UDP 1467250027 0.0.0.0 56093 typ host` 78 | 79 | expect(result.sdp).to.equal(expected) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /test/stream.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-assertions */ 2 | 3 | import { expect } from 'aegir/chai' 4 | import length from 'it-length' 5 | import * as lengthPrefixed from 'it-length-prefixed' 6 | import { pushable } from 'it-pushable' 7 | import { Uint8ArrayList } from 'uint8arraylist' 8 | import { Message } from '../src/pb/message.js' 9 | import { createStream } from '../src/stream.js' 10 | 11 | const mockDataChannel = (opts: { send: (bytes: Uint8Array) => void, bufferedAmount?: number }): RTCDataChannel => { 12 | return { 13 | readyState: 'open', 14 | close: () => {}, 15 | addEventListener: (_type: string, _listener: () => void) => {}, 16 | removeEventListener: (_type: string, _listener: () => void) => {}, 17 | ...opts 18 | } as RTCDataChannel 19 | } 20 | 21 | const MAX_MESSAGE_SIZE = 16 * 1024 22 | 23 | describe('Max message size', () => { 24 | it(`sends messages smaller or equal to ${MAX_MESSAGE_SIZE} bytes in one`, async () => { 25 | const sent: Uint8ArrayList = new Uint8ArrayList() 26 | const data = new Uint8Array(MAX_MESSAGE_SIZE - 5) 27 | const p = pushable() 28 | 29 | // Make sure that the data that ought to be sent will result in a message with exactly MAX_MESSAGE_SIZE 30 | const messageLengthEncoded = lengthPrefixed.encode.single(Message.encode({ message: data })) 31 | expect(messageLengthEncoded.length).eq(MAX_MESSAGE_SIZE) 32 | const webrtcStream = createStream({ 33 | channel: mockDataChannel({ 34 | send: (bytes) => { 35 | sent.append(bytes) 36 | } 37 | }), 38 | direction: 'outbound' 39 | }) 40 | 41 | p.push(data) 42 | p.end() 43 | await webrtcStream.sink(p) 44 | 45 | // length(message) + message + length(FIN) + FIN 46 | expect(length(sent)).to.equal(4) 47 | 48 | for (const buf of sent) { 49 | expect(buf.byteLength).to.be.lessThanOrEqual(MAX_MESSAGE_SIZE) 50 | } 51 | }) 52 | 53 | it(`sends messages greater than ${MAX_MESSAGE_SIZE} bytes in parts`, async () => { 54 | const sent: Uint8ArrayList = new Uint8ArrayList() 55 | const data = new Uint8Array(MAX_MESSAGE_SIZE) 56 | const p = pushable() 57 | 58 | // Make sure that the data that ought to be sent will result in a message with exactly MAX_MESSAGE_SIZE + 1 59 | // const messageLengthEncoded = lengthPrefixed.encode.single(Message.encode({ message: data })).subarray() 60 | // expect(messageLengthEncoded.length).eq(MAX_MESSAGE_SIZE + 1) 61 | 62 | const webrtcStream = createStream({ 63 | channel: mockDataChannel({ 64 | send: (bytes) => { 65 | sent.append(bytes) 66 | } 67 | }), 68 | direction: 'outbound' 69 | }) 70 | 71 | p.push(data) 72 | p.end() 73 | await webrtcStream.sink(p) 74 | 75 | expect(length(sent)).to.equal(6) 76 | 77 | for (const buf of sent) { 78 | expect(buf.byteLength).to.be.lessThanOrEqual(MAX_MESSAGE_SIZE) 79 | } 80 | }) 81 | 82 | it('closes the stream if bufferamountlow timeout', async () => { 83 | const MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024 + 1 84 | const timeout = 100 85 | let closed = false 86 | const webrtcStream = createStream({ 87 | dataChannelOptions: { 88 | bufferedAmountLowEventTimeout: timeout 89 | }, 90 | channel: mockDataChannel({ 91 | send: () => { 92 | throw new Error('Expected to not send') 93 | }, 94 | bufferedAmount: MAX_BUFFERED_AMOUNT 95 | }), 96 | direction: 'outbound', 97 | onEnd: () => { 98 | closed = true 99 | } 100 | }) 101 | 102 | const p = pushable() 103 | p.push(new Uint8Array(1)) 104 | p.end() 105 | 106 | const t0 = Date.now() 107 | 108 | await expect(webrtcStream.sink(p)).to.eventually.be.rejected 109 | .with.property('message', 'Timed out waiting for DataChannel buffer to clear') 110 | const t1 = Date.now() 111 | expect(t1 - t0).greaterThan(timeout) 112 | expect(t1 - t0).lessThan(timeout + 1000) // Some upper bound 113 | expect(closed).true() 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /test/basics.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-expressions */ 2 | 3 | import { noise } from '@chainsafe/libp2p-noise' 4 | import { yamux } from '@chainsafe/libp2p-yamux' 5 | import { webSockets } from '@libp2p/websockets' 6 | import * as filter from '@libp2p/websockets/filters' 7 | import { WebRTC } from '@multiformats/mafmt' 8 | import { multiaddr } from '@multiformats/multiaddr' 9 | import { expect } from 'aegir/chai' 10 | import map from 'it-map' 11 | import { pipe } from 'it-pipe' 12 | import toBuffer from 'it-to-buffer' 13 | import { createLibp2p } from 'libp2p' 14 | import { circuitRelayTransport } from 'libp2p/circuit-relay' 15 | import { identifyService } from 'libp2p/identify' 16 | import { webRTC } from '../src/index.js' 17 | import type { Connection } from '@libp2p/interface-connection' 18 | import type { Libp2p } from '@libp2p/interface-libp2p' 19 | 20 | async function createNode (): Promise { 21 | return createLibp2p({ 22 | addresses: { 23 | listen: [ 24 | '/webrtc', 25 | `${process.env.RELAY_MULTIADDR}/p2p-circuit` 26 | ] 27 | }, 28 | transports: [ 29 | webSockets({ 30 | filter: filter.all 31 | }), 32 | circuitRelayTransport(), 33 | webRTC() 34 | ], 35 | connectionEncryption: [ 36 | noise() 37 | ], 38 | streamMuxers: [ 39 | yamux() 40 | ], 41 | services: { 42 | identify: identifyService() 43 | }, 44 | connectionGater: { 45 | denyDialMultiaddr: () => false 46 | }, 47 | connectionManager: { 48 | minConnections: 0 49 | } 50 | }) 51 | } 52 | 53 | describe('basics', () => { 54 | const echo = '/echo/1.0.0' 55 | 56 | let localNode: Libp2p 57 | let remoteNode: Libp2p 58 | 59 | async function connectNodes (): Promise { 60 | const remoteAddr = remoteNode.getMultiaddrs() 61 | .filter(ma => WebRTC.matches(ma)).pop() 62 | 63 | if (remoteAddr == null) { 64 | throw new Error('Remote peer could not listen on relay') 65 | } 66 | 67 | await remoteNode.handle(echo, ({ stream }) => { 68 | void pipe( 69 | stream, 70 | stream 71 | ) 72 | }) 73 | 74 | const connection = await localNode.dial(remoteAddr) 75 | 76 | // disconnect both from relay 77 | await localNode.hangUp(multiaddr(process.env.RELAY_MULTIADDR)) 78 | await remoteNode.hangUp(multiaddr(process.env.RELAY_MULTIADDR)) 79 | 80 | return connection 81 | } 82 | 83 | beforeEach(async () => { 84 | localNode = await createNode() 85 | remoteNode = await createNode() 86 | }) 87 | 88 | afterEach(async () => { 89 | if (localNode != null) { 90 | await localNode.stop() 91 | } 92 | 93 | if (remoteNode != null) { 94 | await remoteNode.stop() 95 | } 96 | }) 97 | 98 | it('can dial through a relay', async () => { 99 | const connection = await connectNodes() 100 | 101 | // open a stream on the echo protocol 102 | const stream = await connection.newStream(echo) 103 | 104 | // send and receive some data 105 | const input = new Array(5).fill(0).map(() => new Uint8Array(10)) 106 | const output = await pipe( 107 | input, 108 | stream, 109 | (source) => map(source, list => list.subarray()), 110 | async (source) => toBuffer(source) 111 | ) 112 | 113 | // asset that we got the right data 114 | expect(output).to.equalBytes(toBuffer(input)) 115 | }) 116 | 117 | it('can send a large file', async () => { 118 | const connection = await connectNodes() 119 | 120 | // open a stream on the echo protocol 121 | const stream = await connection.newStream(echo) 122 | 123 | // send and receive some data 124 | const input = new Array(5).fill(0).map(() => new Uint8Array(1024 * 1024)) 125 | const output = await pipe( 126 | input, 127 | stream, 128 | (source) => map(source, list => list.subarray()), 129 | async (source) => toBuffer(source) 130 | ) 131 | 132 | // asset that we got the right data 133 | expect(output).to.equalBytes(toBuffer(input)) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /test/transport.browser.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-floating-promises */ 2 | 3 | import { mockMetrics, mockUpgrader } from '@libp2p/interface-mocks' 4 | import { type CreateListenerOptions, symbol } from '@libp2p/interface-transport' 5 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 6 | import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' 7 | import { expect, assert } from 'aegir/chai' 8 | import { UnimplementedError } from './../src/error.js' 9 | import * as underTest from './../src/private-to-public/transport.js' 10 | import { expectError } from './util.js' 11 | import type { Metrics } from '@libp2p/interface-metrics' 12 | 13 | function ignoredDialOption (): CreateListenerOptions { 14 | const upgrader = mockUpgrader({}) 15 | return { upgrader } 16 | } 17 | 18 | describe('WebRTC Transport', () => { 19 | let metrics: Metrics 20 | let components: underTest.WebRTCDirectTransportComponents 21 | 22 | before(async () => { 23 | metrics = mockMetrics()() 24 | components = { 25 | peerId: await createEd25519PeerId(), 26 | metrics 27 | } 28 | }) 29 | 30 | it('can construct', () => { 31 | const t = new underTest.WebRTCDirectTransport(components) 32 | expect(t.constructor.name).to.equal('WebRTCDirectTransport') 33 | }) 34 | 35 | it('can dial', async () => { 36 | const ma = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') 37 | const transport = new underTest.WebRTCDirectTransport(components) 38 | const options = ignoredDialOption() 39 | 40 | // don't await as this isn't an e2e test 41 | transport.dial(ma, options) 42 | }) 43 | 44 | it('createListner throws', () => { 45 | const t = new underTest.WebRTCDirectTransport(components) 46 | try { 47 | t.createListener(ignoredDialOption()) 48 | expect('Should have thrown').to.equal('but did not') 49 | } catch (e) { 50 | expect(e).to.be.instanceOf(UnimplementedError) 51 | } 52 | }) 53 | 54 | it('toString property getter', () => { 55 | const t = new underTest.WebRTCDirectTransport(components) 56 | const s = t[Symbol.toStringTag] 57 | expect(s).to.equal('@libp2p/webrtc-direct') 58 | }) 59 | 60 | it('symbol property getter', () => { 61 | const t = new underTest.WebRTCDirectTransport(components) 62 | const s = t[symbol] 63 | expect(s).to.equal(true) 64 | }) 65 | 66 | it('transport filter filters out invalid multiaddrs', async () => { 67 | const mas: Multiaddr[] = [ 68 | '/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ', 69 | '/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', 70 | '/ip4/1.2.3.4/udp/1234/webrtc-direct/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', 71 | '/ip4/1.2.3.4/udp/1234/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd' 72 | ].map((s) => multiaddr(s)) 73 | const t = new underTest.WebRTCDirectTransport(components) 74 | const result = t.filter(mas) 75 | const expected = 76 | multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') 77 | 78 | assert.isNotNull(result) 79 | expect(result.constructor.name).to.equal('Array') 80 | expect(result).to.have.length(1) 81 | expect(result[0].equals(expected)).to.be.true() 82 | }) 83 | 84 | it('throws WebRTC transport error when dialing a multiaddr without a PeerId', async () => { 85 | const ma = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') 86 | const transport = new underTest.WebRTCDirectTransport(components) 87 | 88 | try { 89 | await transport.dial(ma, ignoredDialOption()) 90 | } catch (error) { 91 | const expected = 'WebRTC transport error: There was a problem with the Multiaddr which was passed in: we need to have the remote\'s PeerId' 92 | expectError(error, expected) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /examples/browser-to-browser/index.js: -------------------------------------------------------------------------------- 1 | import { multiaddr, protocols } from "@multiformats/multiaddr" 2 | import { pipe } from "it-pipe" 3 | import { fromString, toString } from "uint8arrays" 4 | import { webRTC } from "@libp2p/webrtc" 5 | import { webSockets } from "@libp2p/websockets" 6 | import * as filters from "@libp2p/websockets/filters" 7 | import { pushable } from "it-pushable" 8 | import { mplex } from "@libp2p/mplex" 9 | import { createLibp2p } from "libp2p" 10 | import { circuitRelayTransport } from 'libp2p/circuit-relay' 11 | import { noise } from "@chainsafe/libp2p-noise" 12 | import { identifyService } from 'libp2p/identify' 13 | 14 | const WEBRTC_CODE = protocols('webrtc').code 15 | 16 | const output = document.getElementById("output") 17 | const sendSection = document.getElementById("send-section") 18 | const appendOutput = (line) => { 19 | const div = document.createElement("div") 20 | div.appendChild(document.createTextNode(line)) 21 | output.append(div) 22 | } 23 | const clean = (line) => line.replaceAll("\n", "") 24 | const sender = pushable() 25 | 26 | const node = await createLibp2p({ 27 | addresses: { 28 | listen: [ 29 | '/webrtc' 30 | ] 31 | }, 32 | transports: [ 33 | webSockets({ 34 | filter: filters.all, 35 | }), 36 | webRTC(), 37 | circuitRelayTransport({ 38 | discoverRelays: 1, 39 | }), 40 | ], 41 | connectionEncryption: [noise()], 42 | streamMuxers: [mplex()], 43 | connectionGater: { 44 | denyDialMultiaddr: () => { 45 | // by default we refuse to dial local addresses from the browser since they 46 | // are usually sent by remote peers broadcasting undialable multiaddrs but 47 | // here we are explicitly connecting to a local node so do not deny dialing 48 | // any discovered address 49 | return false 50 | } 51 | }, 52 | services: { 53 | identify: identifyService() 54 | } 55 | }) 56 | 57 | await node.start() 58 | 59 | // handle the echo protocol 60 | await node.handle("/echo/1.0.0", ({ stream }) => { 61 | pipe( 62 | stream, 63 | async function* (source) { 64 | for await (const buf of source) { 65 | const incoming = toString(buf.subarray()) 66 | appendOutput(`Received message '${clean(incoming)}'`) 67 | yield buf 68 | } 69 | }, 70 | stream 71 | ) 72 | }) 73 | 74 | function updateConnList() { 75 | // Update connections list 76 | const connListEls = node.getConnections() 77 | .map((connection) => { 78 | if (connection.remoteAddr.protoCodes().includes(WEBRTC_CODE)) { 79 | sendSection.style.display = "block" 80 | } 81 | 82 | const el = document.createElement("li") 83 | el.textContent = connection.remoteAddr.toString() 84 | return el 85 | }) 86 | document.getElementById("connections").replaceChildren(...connListEls) 87 | } 88 | 89 | node.addEventListener("connection:open", (event) => { 90 | updateConnList() 91 | }) 92 | node.addEventListener("connection:close", (event) => { 93 | updateConnList() 94 | }) 95 | 96 | node.addEventListener("self:peer:update", (event) => { 97 | // Update multiaddrs list 98 | const multiaddrs = node.getMultiaddrs() 99 | .map((ma) => { 100 | const el = document.createElement("li") 101 | el.textContent = ma.toString() 102 | return el 103 | }) 104 | document.getElementById("multiaddrs").replaceChildren(...multiaddrs) 105 | }) 106 | 107 | const isWebrtc = (ma) => { 108 | return ma.protoCodes().includes(WEBRTC_CODE) 109 | } 110 | 111 | window.connect.onclick = async () => { 112 | const ma = multiaddr(window.peer.value) 113 | appendOutput(`Dialing '${ma}'`) 114 | const connection = await node.dial(ma) 115 | 116 | if (isWebrtc(ma)) { 117 | const outgoing_stream = await connection.newStream(["/echo/1.0.0"]) 118 | pipe(sender, outgoing_stream, async (src) => { 119 | for await (const buf of src) { 120 | const response = toString(buf.subarray()) 121 | appendOutput(`Received message '${clean(response)}'`) 122 | } 123 | }) 124 | } 125 | 126 | appendOutput(`Connected to '${ma}'`) 127 | } 128 | 129 | window.send.onclick = async () => { 130 | const message = `${window.message.value}\n` 131 | appendOutput(`Sending message '${clean(message)}'`) 132 | sender.push(fromString(message)) 133 | } 134 | -------------------------------------------------------------------------------- /examples/browser-to-browser/tests/test.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { setup, expect } from 'test-ipfs-example/browser' 3 | import { createLibp2p } from 'libp2p' 4 | import { circuitRelayServer } from 'libp2p/circuit-relay' 5 | import { webSockets } from '@libp2p/websockets' 6 | import * as filters from '@libp2p/websockets/filters' 7 | import { mplex } from '@libp2p/mplex' 8 | import { noise } from '@chainsafe/libp2p-noise' 9 | import { identifyService } from 'libp2p/identify' 10 | 11 | // Setup 12 | const test = setup() 13 | 14 | // DOM 15 | const connectBtn = '#connect' 16 | const connectAddr = '#peer' 17 | const messageInput = '#message' 18 | const sendBtn = '#send' 19 | const output = '#output' 20 | const listeningAddresses = '#multiaddrs' 21 | 22 | let url 23 | 24 | // we spawn a js libp2p relay 25 | async function spawnRelay() { 26 | const relayNode = await createLibp2p({ 27 | addresses: { 28 | listen: ['/ip4/127.0.0.1/tcp/0/ws'] 29 | }, 30 | transports: [ 31 | webSockets({ 32 | filter: filters.all 33 | }), 34 | ], 35 | connectionEncryption: [noise()], 36 | streamMuxers: [mplex()], 37 | services: { 38 | identify: identifyService(), 39 | relay: circuitRelayServer() 40 | } 41 | }) 42 | 43 | const relayNodeAddr = relayNode.getMultiaddrs()[0].toString() 44 | 45 | return { relayNode, relayNodeAddr } 46 | } 47 | 48 | test.describe('browser to browser example:', () => { 49 | let relayNode 50 | let relayNodeAddr 51 | 52 | // eslint-disable-next-line no-empty-pattern 53 | test.beforeAll(async ({ servers }, testInfo) => { 54 | testInfo.setTimeout(5 * 60_000) 55 | const r = await spawnRelay() 56 | relayNode = r.relayNode 57 | relayNodeAddr = r.relayNodeAddr 58 | console.log('Server addr:', relayNodeAddr) 59 | url = servers[0].url 60 | }, {}) 61 | 62 | test.afterAll(() => { 63 | relayNode.stop() 64 | }) 65 | 66 | test.beforeEach(async ({ page }) => { 67 | await page.goto(url) 68 | }) 69 | 70 | test('should connect to a relay node', async ({ page: pageA, context }) => { 71 | // load second page 72 | const pageB = await context.newPage() 73 | await pageB.goto(url) 74 | 75 | // connect both pages to the relay 76 | const relayedAddressA = await dialRelay(pageA, relayNodeAddr) 77 | const relayedAddressB = await dialRelay(pageB, relayNodeAddr) 78 | 79 | // dial first page from second page over relay 80 | await dialPeerOverRelay(pageA, relayedAddressB) 81 | await dialPeerOverRelay(pageB, relayedAddressA) 82 | 83 | // stop the relay 84 | await relayNode.stop() 85 | 86 | await echoMessagePeer(pageB, 'hello B') 87 | 88 | await echoMessagePeer(pageA, 'hello A') 89 | }) 90 | }) 91 | 92 | async function echoMessagePeer (page, message) { 93 | // send the message to the peer over webRTC 94 | await page.fill(messageInput, message) 95 | await page.click(sendBtn) 96 | 97 | // check the message was echoed back 98 | const outputLocator = page.locator(output) 99 | await expect(outputLocator).toContainText(`Sending message '${message}'`) 100 | await expect(outputLocator).toContainText(`Received message '${message}'`) 101 | } 102 | 103 | async function dialRelay (page, address) { 104 | // add the go libp2p multiaddress to the input field and submit 105 | await page.fill(connectAddr, address) 106 | await page.click(connectBtn) 107 | 108 | const outputLocator = page.locator(output) 109 | await expect(outputLocator).toContainText(`Dialing '${address}'`) 110 | await expect(outputLocator).toContainText(`Connected to '${address}'`) 111 | 112 | const multiaddrsLocator = page.locator(listeningAddresses) 113 | await expect(multiaddrsLocator).toHaveText(/webrtc/) 114 | 115 | const multiaddrs = await page.textContent(listeningAddresses) 116 | const addr = multiaddrs.split(address).filter(str => str.includes('webrtc')).pop() 117 | 118 | return address + addr 119 | } 120 | 121 | async function dialPeerOverRelay (page, address) { 122 | // add the go libp2p multiaddr to the input field and submit 123 | await page.fill(connectAddr, address) 124 | await page.click(connectBtn) 125 | 126 | const outputLocator = page.locator(output) 127 | await expect(outputLocator).toContainText(`Dialing '${address}'`) 128 | await expect(outputLocator).toContainText(`Connected to '${address}'`) 129 | } 130 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import { CodeError } from '@libp2p/interfaces/errors' 2 | import type { Direction } from '@libp2p/interface-connection' 3 | 4 | export enum codes { 5 | ERR_ALREADY_ABORTED = 'ERR_ALREADY_ABORTED', 6 | ERR_DATA_CHANNEL = 'ERR_DATA_CHANNEL', 7 | ERR_CONNECTION_CLOSED = 'ERR_CONNECTION_CLOSED', 8 | ERR_HASH_NOT_SUPPORTED = 'ERR_HASH_NOT_SUPPORTED', 9 | ERR_INVALID_MULTIADDR = 'ERR_INVALID_MULTIADDR', 10 | ERR_INVALID_FINGERPRINT = 'ERR_INVALID_FINGERPRINT', 11 | ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS', 12 | ERR_NOT_IMPLEMENTED = 'ERR_NOT_IMPLEMENTED', 13 | ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS', 14 | ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS', 15 | } 16 | 17 | export class WebRTCTransportError extends CodeError { 18 | constructor (msg: string, code?: string) { 19 | super(`WebRTC transport error: ${msg}`, code ?? '') 20 | this.name = 'WebRTCTransportError' 21 | } 22 | } 23 | 24 | export class ConnectionClosedError extends WebRTCTransportError { 25 | constructor (state: RTCPeerConnectionState, msg: string) { 26 | super(`peerconnection moved to state: ${state}: ${msg}`, codes.ERR_CONNECTION_CLOSED) 27 | this.name = 'WebRTC/ConnectionClosed' 28 | } 29 | } 30 | 31 | export function connectionClosedError (state: RTCPeerConnectionState, msg: string): ConnectionClosedError { 32 | return new ConnectionClosedError(state, msg) 33 | } 34 | 35 | export class DataChannelError extends WebRTCTransportError { 36 | constructor (streamLabel: string, msg: string) { 37 | super(`[stream: ${streamLabel}] data channel error: ${msg}`, codes.ERR_DATA_CHANNEL) 38 | this.name = 'WebRTC/DataChannelError' 39 | } 40 | } 41 | 42 | export function dataChannelError (streamLabel: string, msg: string): DataChannelError { 43 | return new DataChannelError(streamLabel, msg) 44 | } 45 | 46 | export class InappropriateMultiaddrError extends WebRTCTransportError { 47 | constructor (msg: string) { 48 | super(`There was a problem with the Multiaddr which was passed in: ${msg}`, codes.ERR_INVALID_MULTIADDR) 49 | this.name = 'WebRTC/InappropriateMultiaddrError' 50 | } 51 | } 52 | 53 | export function inappropriateMultiaddr (msg: string): InappropriateMultiaddrError { 54 | return new InappropriateMultiaddrError(msg) 55 | } 56 | 57 | export class InvalidArgumentError extends WebRTCTransportError { 58 | constructor (msg: string) { 59 | super(`There was a problem with a provided argument: ${msg}`, codes.ERR_INVALID_PARAMETERS) 60 | this.name = 'WebRTC/InvalidArgumentError' 61 | } 62 | } 63 | 64 | export function invalidArgument (msg: string): InvalidArgumentError { 65 | return new InvalidArgumentError(msg) 66 | } 67 | 68 | export class InvalidFingerprintError extends WebRTCTransportError { 69 | constructor (fingerprint: string, source: string) { 70 | super(`Invalid fingerprint "${fingerprint}" within ${source}`, codes.ERR_INVALID_FINGERPRINT) 71 | this.name = 'WebRTC/InvalidFingerprintError' 72 | } 73 | } 74 | 75 | export function invalidFingerprint (fingerprint: string, source: string): InvalidFingerprintError { 76 | return new InvalidFingerprintError(fingerprint, source) 77 | } 78 | 79 | export class OperationAbortedError extends WebRTCTransportError { 80 | constructor (context: string, abortReason: string) { 81 | super(`Signalled to abort because (${abortReason}}) ${context}`, codes.ERR_ALREADY_ABORTED) 82 | this.name = 'WebRTC/OperationAbortedError' 83 | } 84 | } 85 | 86 | export function operationAborted (context: string, reason: string): OperationAbortedError { 87 | return new OperationAbortedError(context, reason) 88 | } 89 | 90 | export class OverStreamLimitError extends WebRTCTransportError { 91 | constructor (msg: string) { 92 | const code = msg.startsWith('inbound') ? codes.ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS : codes.ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS 93 | super(msg, code) 94 | this.name = 'WebRTC/OverStreamLimitError' 95 | } 96 | } 97 | 98 | export function overStreamLimit (dir: Direction, proto: string): OverStreamLimitError { 99 | return new OverStreamLimitError(`${dir} stream limit reached for protocol - ${proto}`) 100 | } 101 | 102 | export class UnimplementedError extends WebRTCTransportError { 103 | constructor (methodName: string) { 104 | super(`A method (${methodName}) was called though it has been intentionally left unimplemented.`, codes.ERR_NOT_IMPLEMENTED) 105 | this.name = 'WebRTC/UnimplementedError' 106 | } 107 | } 108 | 109 | export function unimplemented (methodName: string): UnimplementedError { 110 | return new UnimplementedError(methodName) 111 | } 112 | 113 | export class UnsupportedHashAlgorithmError extends WebRTCTransportError { 114 | constructor (algo: string) { 115 | super(`unsupported hash algorithm: ${algo}`, codes.ERR_HASH_NOT_SUPPORTED) 116 | this.name = 'WebRTC/UnsupportedHashAlgorithmError' 117 | } 118 | } 119 | 120 | export function unsupportedHashAlgorithm (algorithm: string): UnsupportedHashAlgorithmError { 121 | return new UnsupportedHashAlgorithmError(algorithm) 122 | } 123 | -------------------------------------------------------------------------------- /src/private-to-public/sdp.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@libp2p/logger' 2 | import { bases } from 'multiformats/basics' 3 | import * as multihashes from 'multihashes' 4 | import { inappropriateMultiaddr, invalidArgument, invalidFingerprint, unsupportedHashAlgorithm } from '../error.js' 5 | import { CERTHASH_CODE } from './transport.js' 6 | import type { Multiaddr } from '@multiformats/multiaddr' 7 | import type { HashCode, HashName } from 'multihashes' 8 | 9 | const log = logger('libp2p:webrtc:sdp') 10 | 11 | /** 12 | * Get base2 | identity decoders 13 | */ 14 | // @ts-expect-error - Not easy to combine these types. 15 | export const mbdecoder: any = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b)) 16 | 17 | export function getLocalFingerprint (pc: RTCPeerConnection): string | undefined { 18 | // try to fetch fingerprint from local certificate 19 | const localCert = pc.getConfiguration().certificates?.at(0) 20 | if (localCert == null || localCert.getFingerprints == null) { 21 | log.trace('fetching fingerprint from local SDP') 22 | const localDescription = pc.localDescription 23 | if (localDescription == null) { 24 | return undefined 25 | } 26 | return getFingerprintFromSdp(localDescription.sdp) 27 | } 28 | 29 | log.trace('fetching fingerprint from local certificate') 30 | 31 | if (localCert.getFingerprints().length === 0) { 32 | return undefined 33 | } 34 | 35 | const fingerprint = localCert.getFingerprints()[0].value 36 | if (fingerprint == null) { 37 | throw invalidFingerprint('', 'no fingerprint on local certificate') 38 | } 39 | 40 | return fingerprint 41 | } 42 | 43 | const fingerprintRegex = /^a=fingerprint:(?:\w+-[0-9]+)\s(?(:?[0-9a-fA-F]{2})+)$/m 44 | export function getFingerprintFromSdp (sdp: string): string | undefined { 45 | const searchResult = sdp.match(fingerprintRegex) 46 | return searchResult?.groups?.fingerprint 47 | } 48 | /** 49 | * Get base2 | identity decoders 50 | */ 51 | function ipv (ma: Multiaddr): string { 52 | for (const proto of ma.protoNames()) { 53 | if (proto.startsWith('ip')) { 54 | return proto.toUpperCase() 55 | } 56 | } 57 | 58 | log('Warning: multiaddr does not appear to contain IP4 or IP6, defaulting to IP6', ma) 59 | 60 | return 'IP6' 61 | } 62 | 63 | // Extract the certhash from a multiaddr 64 | export function certhash (ma: Multiaddr): string { 65 | const tups = ma.stringTuples() 66 | const certhash = tups.filter((tup) => tup[0] === CERTHASH_CODE).map((tup) => tup[1])[0] 67 | 68 | if (certhash === undefined || certhash === '') { 69 | throw inappropriateMultiaddr(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`) 70 | } 71 | 72 | return certhash 73 | } 74 | 75 | /** 76 | * Convert a certhash into a multihash 77 | */ 78 | export function decodeCerthash (certhash: string): { code: HashCode, name: HashName, length: number, digest: Uint8Array } { 79 | const mbdecoded = mbdecoder.decode(certhash) 80 | return multihashes.decode(mbdecoded) 81 | } 82 | 83 | /** 84 | * Extract the fingerprint from a multiaddr 85 | */ 86 | export function ma2Fingerprint (ma: Multiaddr): string[] { 87 | const mhdecoded = decodeCerthash(certhash(ma)) 88 | const prefix = toSupportedHashFunction(mhdecoded.name) 89 | const fingerprint = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '') 90 | const sdp = fingerprint.match(/.{1,2}/g) 91 | 92 | if (sdp == null) { 93 | throw invalidFingerprint(fingerprint, ma.toString()) 94 | } 95 | 96 | return [`${prefix.toUpperCase()} ${sdp.join(':').toUpperCase()}`, fingerprint] 97 | } 98 | 99 | /** 100 | * Normalize the hash name from a given multihash has name 101 | */ 102 | export function toSupportedHashFunction (name: multihashes.HashName): string { 103 | switch (name) { 104 | case 'sha1': 105 | return 'sha-1' 106 | case 'sha2-256': 107 | return 'sha-256' 108 | case 'sha2-512': 109 | return 'sha-512' 110 | default: 111 | throw unsupportedHashAlgorithm(name) 112 | } 113 | } 114 | 115 | /** 116 | * Convert a multiaddr into a SDP 117 | */ 118 | function ma2sdp (ma: Multiaddr, ufrag: string): string { 119 | const { host, port } = ma.toOptions() 120 | const ipVersion = ipv(ma) 121 | const [CERTFP] = ma2Fingerprint(ma) 122 | 123 | return `v=0 124 | o=- 0 0 IN ${ipVersion} ${host} 125 | s=- 126 | c=IN ${ipVersion} ${host} 127 | t=0 0 128 | a=ice-lite 129 | m=application ${port} UDP/DTLS/SCTP webrtc-datachannel 130 | a=mid:0 131 | a=setup:passive 132 | a=ice-ufrag:${ufrag} 133 | a=ice-pwd:${ufrag} 134 | a=fingerprint:${CERTFP} 135 | a=sctp-port:5000 136 | a=max-message-size:100000 137 | a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host\r\n` 138 | } 139 | 140 | /** 141 | * Create an answer SDP from a multiaddr 142 | */ 143 | export function fromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit { 144 | return { 145 | type: 'answer', 146 | sdp: ma2sdp(ma, ufrag) 147 | } 148 | } 149 | 150 | /** 151 | * Replace (munge) the ufrag and password values in a SDP 152 | */ 153 | export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit { 154 | if (desc.sdp === undefined) { 155 | throw invalidArgument("Can't munge a missing SDP") 156 | } 157 | 158 | desc.sdp = desc.sdp 159 | .replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n') 160 | .replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n') 161 | return desc 162 | } 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@libp2p/webrtc", 3 | "version": "2.0.11", 4 | "description": "A libp2p transport using WebRTC connections", 5 | "author": "", 6 | "license": "Apache-2.0 OR MIT", 7 | "homepage": "https://github.com/libp2p/js-libp2p-webrtc#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/libp2p/js-libp2p-webrtc.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/libp2p/js-libp2p-webrtc/issues" 14 | }, 15 | "engines": { 16 | "node": ">=18.0.0", 17 | "npm": ">=8.6.0" 18 | }, 19 | "type": "module", 20 | "types": "./dist/src/index.d.ts", 21 | "files": [ 22 | "src", 23 | "dist", 24 | "!dist/test", 25 | "!**/*.tsbuildinfo", 26 | "proto_ts" 27 | ], 28 | "exports": { 29 | ".": { 30 | "types": "./dist/src/index.d.ts", 31 | "import": "./dist/src/index.js" 32 | } 33 | }, 34 | "eslintConfig": { 35 | "extends": "ipfs", 36 | "parserOptions": { 37 | "sourceType": "module" 38 | } 39 | }, 40 | "release": { 41 | "branches": [ 42 | "main" 43 | ], 44 | "plugins": [ 45 | [ 46 | "@semantic-release/commit-analyzer", 47 | { 48 | "preset": "conventionalcommits", 49 | "releaseRules": [ 50 | { 51 | "breaking": true, 52 | "release": "major" 53 | }, 54 | { 55 | "revert": true, 56 | "release": "patch" 57 | }, 58 | { 59 | "type": "feat", 60 | "release": "minor" 61 | }, 62 | { 63 | "type": "fix", 64 | "release": "patch" 65 | }, 66 | { 67 | "type": "docs", 68 | "release": "patch" 69 | }, 70 | { 71 | "type": "test", 72 | "release": "patch" 73 | }, 74 | { 75 | "type": "deps", 76 | "release": "patch" 77 | }, 78 | { 79 | "scope": "no-release", 80 | "release": false 81 | } 82 | ] 83 | } 84 | ], 85 | [ 86 | "@semantic-release/release-notes-generator", 87 | { 88 | "preset": "conventionalcommits", 89 | "presetConfig": { 90 | "types": [ 91 | { 92 | "type": "feat", 93 | "section": "Features" 94 | }, 95 | { 96 | "type": "fix", 97 | "section": "Bug Fixes" 98 | }, 99 | { 100 | "type": "chore", 101 | "section": "Trivial Changes" 102 | }, 103 | { 104 | "type": "docs", 105 | "section": "Documentation" 106 | }, 107 | { 108 | "type": "deps", 109 | "section": "Dependencies" 110 | }, 111 | { 112 | "type": "test", 113 | "section": "Tests" 114 | } 115 | ] 116 | } 117 | } 118 | ], 119 | "@semantic-release/changelog", 120 | "@semantic-release/npm", 121 | "@semantic-release/github", 122 | "@semantic-release/git" 123 | ] 124 | }, 125 | "scripts": { 126 | "generate": "protons src/private-to-private/pb/message.proto src/pb/message.proto", 127 | "build": "aegir build", 128 | "test": "aegir test -t browser", 129 | "test:chrome": "aegir test -t browser --cov", 130 | "test:firefox": "aegir test -t browser -- --browser firefox", 131 | "lint": "aegir lint", 132 | "lint:fix": "aegir lint --fix", 133 | "clean": "aegir clean", 134 | "dep-check": "aegir dep-check -i protons", 135 | "release": "aegir release" 136 | }, 137 | "dependencies": { 138 | "@chainsafe/libp2p-noise": "^12.0.0", 139 | "@libp2p/interface-connection": "^5.0.2", 140 | "@libp2p/interface-metrics": "^4.0.8", 141 | "@libp2p/interface-peer-id": "^2.0.2", 142 | "@libp2p/interface-registrar": "^2.0.12", 143 | "@libp2p/interface-stream-muxer": "^4.1.2", 144 | "@libp2p/interface-transport": "^4.0.3", 145 | "@libp2p/interfaces": "^3.3.2", 146 | "@libp2p/logger": "^2.0.7", 147 | "@libp2p/peer-id": "^2.0.3", 148 | "@multiformats/mafmt": "^12.1.2", 149 | "@multiformats/multiaddr": "^12.1.2", 150 | "abortable-iterator": "^5.0.1", 151 | "detect-browser": "^5.3.0", 152 | "it-length-prefixed": "^9.0.1", 153 | "it-pb-stream": "^4.0.1", 154 | "it-pipe": "^3.0.1", 155 | "it-pushable": "^3.1.3", 156 | "it-stream-types": "^2.0.1", 157 | "it-to-buffer": "^4.0.2", 158 | "multiformats": "^11.0.2", 159 | "multihashes": "^4.0.3", 160 | "p-defer": "^4.0.0", 161 | "p-event": "^6.0.0", 162 | "protons-runtime": "^5.0.0", 163 | "uint8arraylist": "^2.4.3", 164 | "uint8arrays": "^4.0.3" 165 | }, 166 | "devDependencies": { 167 | "@chainsafe/libp2p-yamux": "^4.0.1", 168 | "@libp2p/interface-libp2p": "^3.1.0", 169 | "@libp2p/interface-mocks": "^12.0.1", 170 | "@libp2p/peer-id-factory": "^2.0.3", 171 | "@libp2p/websockets": "^6.0.1", 172 | "@types/sinon": "^10.0.14", 173 | "aegir": "^39.0.7", 174 | "delay": "^6.0.0", 175 | "it-length": "^3.0.2", 176 | "it-map": "^3.0.3", 177 | "it-pair": "^2.0.6", 178 | "libp2p": "^0.45.0", 179 | "protons": "^7.0.2", 180 | "sinon": "^15.0.4", 181 | "sinon-ts": "^1.0.0" 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/muxer.ts: -------------------------------------------------------------------------------- 1 | import { createStream } from './stream.js' 2 | import { nopSink, nopSource } from './util.js' 3 | import type { DataChannelOpts } from './stream.js' 4 | import type { Stream } from '@libp2p/interface-connection' 5 | import type { CounterGroup } from '@libp2p/interface-metrics' 6 | import type { StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface-stream-muxer' 7 | import type { Source, Sink } from 'it-stream-types' 8 | import type { Uint8ArrayList } from 'uint8arraylist' 9 | 10 | const PROTOCOL = '/webrtc' 11 | 12 | export interface DataChannelMuxerFactoryInit { 13 | /** 14 | * WebRTC Peer Connection 15 | */ 16 | peerConnection: RTCPeerConnection 17 | 18 | /** 19 | * Optional metrics for this data channel muxer 20 | */ 21 | metrics?: CounterGroup 22 | 23 | /** 24 | * Data channel options 25 | */ 26 | dataChannelOptions?: Partial 27 | 28 | /** 29 | * The protocol to use 30 | */ 31 | protocol?: string 32 | } 33 | 34 | export class DataChannelMuxerFactory implements StreamMuxerFactory { 35 | public readonly protocol: string 36 | 37 | /** 38 | * WebRTC Peer Connection 39 | */ 40 | private readonly peerConnection: RTCPeerConnection 41 | private streamBuffer: Stream[] = [] 42 | private readonly metrics?: CounterGroup 43 | private readonly dataChannelOptions?: Partial 44 | 45 | constructor (init: DataChannelMuxerFactoryInit) { 46 | this.peerConnection = init.peerConnection 47 | this.metrics = init.metrics 48 | this.protocol = init.protocol ?? PROTOCOL 49 | this.dataChannelOptions = init.dataChannelOptions 50 | 51 | // store any datachannels opened before upgrade has been completed 52 | this.peerConnection.ondatachannel = ({ channel }) => { 53 | const stream = createStream({ 54 | channel, 55 | direction: 'inbound', 56 | dataChannelOptions: init.dataChannelOptions, 57 | onEnd: () => { 58 | this.streamBuffer = this.streamBuffer.filter(s => s.id !== stream.id) 59 | } 60 | }) 61 | this.streamBuffer.push(stream) 62 | } 63 | } 64 | 65 | createStreamMuxer (init?: StreamMuxerInit): StreamMuxer { 66 | return new DataChannelMuxer({ 67 | ...init, 68 | peerConnection: this.peerConnection, 69 | dataChannelOptions: this.dataChannelOptions, 70 | metrics: this.metrics, 71 | streams: this.streamBuffer, 72 | protocol: this.protocol 73 | }) 74 | } 75 | } 76 | 77 | export interface DataChannelMuxerInit extends DataChannelMuxerFactoryInit, StreamMuxerInit { 78 | streams: Stream[] 79 | } 80 | 81 | /** 82 | * A libp2p data channel stream muxer 83 | */ 84 | export class DataChannelMuxer implements StreamMuxer { 85 | /** 86 | * Array of streams in the data channel 87 | */ 88 | public streams: Stream[] 89 | public protocol: string 90 | 91 | private readonly peerConnection: RTCPeerConnection 92 | private readonly dataChannelOptions?: DataChannelOpts 93 | private readonly metrics?: CounterGroup 94 | 95 | /** 96 | * Close or abort all tracked streams and stop the muxer 97 | */ 98 | close: (err?: Error | undefined) => void = () => { } 99 | 100 | /** 101 | * The stream source, a no-op as the transport natively supports multiplexing 102 | */ 103 | source: AsyncGenerator = nopSource() 104 | 105 | /** 106 | * The stream destination, a no-op as the transport natively supports multiplexing 107 | */ 108 | sink: Sink, Promise> = nopSink 109 | 110 | constructor (readonly init: DataChannelMuxerInit) { 111 | this.streams = init.streams 112 | this.peerConnection = init.peerConnection 113 | this.protocol = init.protocol ?? PROTOCOL 114 | this.metrics = init.metrics 115 | 116 | /** 117 | * Fired when a data channel has been added to the connection has been 118 | * added by the remote peer. 119 | * 120 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/datachannel_event} 121 | */ 122 | this.peerConnection.ondatachannel = ({ channel }) => { 123 | const stream = createStream({ 124 | channel, 125 | direction: 'inbound', 126 | dataChannelOptions: this.dataChannelOptions, 127 | onEnd: () => { 128 | this.streams = this.streams.filter(s => s.id !== stream.id) 129 | this.metrics?.increment({ stream_end: true }) 130 | init?.onStreamEnd?.(stream) 131 | } 132 | }) 133 | 134 | this.streams.push(stream) 135 | if ((init?.onIncomingStream) != null) { 136 | this.metrics?.increment({ incoming_stream: true }) 137 | init.onIncomingStream(stream) 138 | } 139 | } 140 | 141 | const onIncomingStream = init?.onIncomingStream 142 | if (onIncomingStream != null) { 143 | this.streams.forEach(s => { onIncomingStream(s) }) 144 | } 145 | } 146 | 147 | newStream (): Stream { 148 | // The spec says the label SHOULD be an empty string: https://github.com/libp2p/specs/blob/master/webrtc/README.md#rtcdatachannel-label 149 | const channel = this.peerConnection.createDataChannel('') 150 | const stream = createStream({ 151 | channel, 152 | direction: 'outbound', 153 | dataChannelOptions: this.dataChannelOptions, 154 | onEnd: () => { 155 | this.streams = this.streams.filter(s => s.id !== stream.id) 156 | this.metrics?.increment({ stream_end: true }) 157 | this.init?.onStreamEnd?.(stream) 158 | } 159 | }) 160 | this.streams.push(stream) 161 | this.metrics?.increment({ outgoing_stream: true }) 162 | 163 | return stream 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /test/stream.browser.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'aegir/chai' 2 | import delay from 'delay' 3 | import * as lengthPrefixed from 'it-length-prefixed' 4 | import { bytes } from 'multiformats' 5 | import { Message } from '../src/pb/message.js' 6 | import { createStream } from '../src/stream' 7 | import type { Stream } from '@libp2p/interface-connection' 8 | const TEST_MESSAGE = 'test_message' 9 | 10 | function setup (): { peerConnection: RTCPeerConnection, dataChannel: RTCDataChannel, stream: Stream } { 11 | const peerConnection = new RTCPeerConnection() 12 | const dataChannel = peerConnection.createDataChannel('whatever', { negotiated: true, id: 91 }) 13 | const stream = createStream({ channel: dataChannel, direction: 'outbound' }) 14 | 15 | return { peerConnection, dataChannel, stream } 16 | } 17 | 18 | function generatePbByFlag (flag?: Message.Flag): Uint8Array { 19 | const buf = Message.encode({ 20 | flag, 21 | message: bytes.fromString(TEST_MESSAGE) 22 | }) 23 | 24 | return lengthPrefixed.encode.single(buf).subarray() 25 | } 26 | 27 | describe('Stream Stats', () => { 28 | let stream: Stream 29 | 30 | beforeEach(async () => { 31 | ({ stream } = setup()) 32 | }) 33 | 34 | it('can construct', () => { 35 | expect(stream.stat.timeline.close).to.not.exist() 36 | }) 37 | 38 | it('close marks it closed', () => { 39 | expect(stream.stat.timeline.close).to.not.exist() 40 | stream.close() 41 | expect(stream.stat.timeline.close).to.be.a('number') 42 | }) 43 | 44 | it('closeRead marks it read-closed only', () => { 45 | expect(stream.stat.timeline.close).to.not.exist() 46 | stream.closeRead() 47 | expect(stream.stat.timeline.close).to.not.exist() 48 | expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) 49 | }) 50 | 51 | it('closeWrite marks it write-closed only', () => { 52 | expect(stream.stat.timeline.close).to.not.exist() 53 | stream.closeWrite() 54 | expect(stream.stat.timeline.close).to.not.exist() 55 | expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) 56 | }) 57 | 58 | it('closeWrite AND closeRead = close', async () => { 59 | expect(stream.stat.timeline.close).to.not.exist() 60 | stream.closeWrite() 61 | stream.closeRead() 62 | expect(stream.stat.timeline.close).to.be.a('number') 63 | expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) 64 | expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) 65 | }) 66 | 67 | it('abort = close', () => { 68 | expect(stream.stat.timeline.close).to.not.exist() 69 | stream.abort(new Error('Oh no!')) 70 | expect(stream.stat.timeline.close).to.be.a('number') 71 | expect(stream.stat.timeline.close).to.be.greaterThanOrEqual(stream.stat.timeline.open) 72 | expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) 73 | expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) 74 | }) 75 | 76 | it('reset = close', () => { 77 | expect(stream.stat.timeline.close).to.not.exist() 78 | stream.reset() // only resets the write side 79 | expect(stream.stat.timeline.close).to.be.a('number') 80 | expect(stream.stat.timeline.close).to.be.greaterThanOrEqual(stream.stat.timeline.open) 81 | expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) 82 | expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) 83 | }) 84 | }) 85 | 86 | describe('Stream Read Stats Transition By Incoming Flag', () => { 87 | let dataChannel: RTCDataChannel 88 | let stream: Stream 89 | 90 | beforeEach(async () => { 91 | ({ dataChannel, stream } = setup()) 92 | }) 93 | 94 | it('no flag, no transition', () => { 95 | expect(stream.stat.timeline.close).to.not.exist() 96 | const data = generatePbByFlag() 97 | dataChannel.onmessage?.(new MessageEvent('message', { data })) 98 | 99 | expect(stream.stat.timeline.close).to.not.exist() 100 | }) 101 | 102 | it('open to read-close by flag:FIN', async () => { 103 | const data = generatePbByFlag(Message.Flag.FIN) 104 | dataChannel.dispatchEvent(new MessageEvent('message', { data })) 105 | 106 | await delay(100) 107 | 108 | expect(stream.stat.timeline.closeWrite).to.not.exist() 109 | expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) 110 | }) 111 | 112 | it('read-close to close by flag:STOP_SENDING', async () => { 113 | const data = generatePbByFlag(Message.Flag.STOP_SENDING) 114 | dataChannel.dispatchEvent(new MessageEvent('message', { data })) 115 | 116 | await delay(100) 117 | 118 | expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) 119 | expect(stream.stat.timeline.closeRead).to.not.exist() 120 | }) 121 | }) 122 | 123 | describe('Stream Write Stats Transition By Incoming Flag', () => { 124 | let dataChannel: RTCDataChannel 125 | let stream: Stream 126 | 127 | beforeEach(async () => { 128 | ({ dataChannel, stream } = setup()) 129 | }) 130 | 131 | it('open to write-close by flag:STOP_SENDING', async () => { 132 | const data = generatePbByFlag(Message.Flag.STOP_SENDING) 133 | dataChannel.dispatchEvent(new MessageEvent('message', { data })) 134 | 135 | await delay(100) 136 | 137 | expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) 138 | expect(stream.stat.timeline.closeRead).to.not.exist() 139 | }) 140 | 141 | it('write-close to close by flag:FIN', async () => { 142 | const data = generatePbByFlag(Message.Flag.FIN) 143 | dataChannel.dispatchEvent(new MessageEvent('message', { data })) 144 | 145 | await delay(100) 146 | 147 | expect(stream.stat.timeline.closeWrite).to.not.exist() 148 | expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) 149 | }) 150 | }) 151 | -------------------------------------------------------------------------------- /examples/go-libp2p-server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/libp2p/js-libp2p-webrtc/examples/go-libp2p-server 2 | 3 | go 1.18 4 | 5 | // TODO: Remove this once webrtc is merged into Go libp2p 6 | replace github.com/libp2p/go-libp2p => github.com/libp2p/go-libp2p v0.26.1-0.20230404184453-257fbfba50c3 7 | 8 | require github.com/libp2p/go-libp2p v0.26.3 9 | 10 | require ( 11 | github.com/benbjohnson/clock v1.3.0 // indirect 12 | github.com/beorn7/perks v1.0.1 // indirect 13 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 14 | github.com/containerd/cgroups v1.1.0 // indirect 15 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 16 | github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect 17 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect 18 | github.com/docker/go-units v0.5.0 // indirect 19 | github.com/elastic/gosigar v0.14.2 // indirect 20 | github.com/flynn/noise v1.0.0 // indirect 21 | github.com/francoispqt/gojay v1.2.13 // indirect 22 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 23 | github.com/godbus/dbus/v5 v5.1.0 // indirect 24 | github.com/gogo/protobuf v1.3.2 // indirect 25 | github.com/golang/mock v1.6.0 // indirect 26 | github.com/golang/protobuf v1.5.3 // indirect 27 | github.com/google/gopacket v1.1.19 // indirect 28 | github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect 29 | github.com/google/uuid v1.3.0 // indirect 30 | github.com/gorilla/websocket v1.5.0 // indirect 31 | github.com/huin/goupnp v1.1.0 // indirect 32 | github.com/ipfs/go-cid v0.4.1 // indirect 33 | github.com/ipfs/go-log/v2 v2.5.1 // indirect 34 | github.com/jackpal/go-nat-pmp v1.0.2 // indirect 35 | github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect 36 | github.com/klauspost/compress v1.16.3 // indirect 37 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 38 | github.com/koron/go-ssdp v0.0.4 // indirect 39 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 40 | github.com/libp2p/go-cidranger v1.1.0 // indirect 41 | github.com/libp2p/go-flow-metrics v0.1.0 // indirect 42 | github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect 43 | github.com/libp2p/go-msgio v0.3.0 // indirect 44 | github.com/libp2p/go-nat v0.1.0 // indirect 45 | github.com/libp2p/go-netroute v0.2.1 // indirect 46 | github.com/libp2p/go-reuseport v0.2.0 // indirect 47 | github.com/libp2p/go-yamux/v4 v4.0.0 // indirect 48 | github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect 49 | github.com/mattn/go-isatty v0.0.17 // indirect 50 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 51 | github.com/miekg/dns v1.1.52 // indirect 52 | github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect 53 | github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect 54 | github.com/minio/sha256-simd v1.0.0 // indirect 55 | github.com/mr-tron/base58 v1.2.0 // indirect 56 | github.com/multiformats/go-base32 v0.1.0 // indirect 57 | github.com/multiformats/go-base36 v0.2.0 // indirect 58 | github.com/multiformats/go-multiaddr v0.9.0 // indirect 59 | github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect 60 | github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect 61 | github.com/multiformats/go-multibase v0.2.0 // indirect 62 | github.com/multiformats/go-multicodec v0.8.1 // indirect 63 | github.com/multiformats/go-multihash v0.2.1 // indirect 64 | github.com/multiformats/go-multistream v0.4.1 // indirect 65 | github.com/multiformats/go-varint v0.0.7 // indirect 66 | github.com/onsi/ginkgo/v2 v2.9.1 // indirect 67 | github.com/opencontainers/runtime-spec v1.0.2 // indirect 68 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 69 | github.com/pion/datachannel v1.5.5 // indirect 70 | github.com/pion/dtls/v2 v2.1.5 // indirect 71 | github.com/pion/ice/v2 v2.2.13 // indirect 72 | github.com/pion/interceptor v0.1.12 // indirect 73 | github.com/pion/logging v0.2.2 // indirect 74 | github.com/pion/mdns v0.0.5 // indirect 75 | github.com/pion/randutil v0.1.0 // indirect 76 | github.com/pion/rtcp v1.2.10 // indirect 77 | github.com/pion/rtp v1.7.13 // indirect 78 | github.com/pion/sctp v1.8.6 // indirect 79 | github.com/pion/sdp/v3 v3.0.6 // indirect 80 | github.com/pion/srtp/v2 v2.0.11 // indirect 81 | github.com/pion/stun v0.4.0 // indirect 82 | github.com/pion/transport v0.14.1 // indirect 83 | github.com/pion/transport/v2 v2.0.0 // indirect 84 | github.com/pion/turn/v2 v2.0.9 // indirect 85 | github.com/pion/udp v0.1.1 // indirect 86 | github.com/pion/webrtc/v3 v3.1.51 // indirect 87 | github.com/pkg/errors v0.9.1 // indirect 88 | github.com/prometheus/client_golang v1.14.0 // indirect 89 | github.com/prometheus/client_model v0.3.0 // indirect 90 | github.com/prometheus/common v0.42.0 // indirect 91 | github.com/prometheus/procfs v0.9.0 // indirect 92 | github.com/quic-go/qpack v0.4.0 // indirect 93 | github.com/quic-go/qtls-go1-19 v0.2.1 // indirect 94 | github.com/quic-go/qtls-go1-20 v0.1.1 // indirect 95 | github.com/quic-go/quic-go v0.33.0 // indirect 96 | github.com/quic-go/webtransport-go v0.5.2 // indirect 97 | github.com/raulk/go-watchdog v1.3.0 // indirect 98 | github.com/rogpeppe/go-internal v1.9.0 // indirect 99 | github.com/spaolacci/murmur3 v1.1.0 // indirect 100 | github.com/stretchr/testify v1.8.2 // indirect 101 | go.uber.org/atomic v1.10.0 // indirect 102 | go.uber.org/dig v1.16.1 // indirect 103 | go.uber.org/fx v1.19.2 // indirect 104 | go.uber.org/multierr v1.10.0 // indirect 105 | go.uber.org/zap v1.24.0 // indirect 106 | golang.org/x/crypto v0.7.0 // indirect 107 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect 108 | golang.org/x/mod v0.9.0 // indirect 109 | golang.org/x/net v0.8.0 // indirect 110 | golang.org/x/sync v0.1.0 // indirect 111 | golang.org/x/sys v0.7.0 // indirect 112 | golang.org/x/text v0.8.0 // indirect 113 | golang.org/x/tools v0.7.0 // indirect 114 | google.golang.org/protobuf v1.30.0 // indirect 115 | lukechampine.com/blake3 v1.1.7 // indirect 116 | nhooyr.io/websocket v1.8.7 // indirect 117 | ) 118 | -------------------------------------------------------------------------------- /test/peer.browser.spec.ts: -------------------------------------------------------------------------------- 1 | import { mockConnection, mockMultiaddrConnection, mockRegistrar, mockStream, mockUpgrader } from '@libp2p/interface-mocks' 2 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 3 | import { multiaddr } from '@multiformats/multiaddr' 4 | import { expect } from 'aegir/chai' 5 | import { detect } from 'detect-browser' 6 | import { pair } from 'it-pair' 7 | import { duplexPair } from 'it-pair/duplex' 8 | import { pbStream } from 'it-pb-stream' 9 | import Sinon from 'sinon' 10 | import { initiateConnection, handleIncomingStream } from '../src/private-to-private/handler' 11 | import { Message } from '../src/private-to-private/pb/message.js' 12 | import { WebRTCTransport, splitAddr } from '../src/private-to-private/transport' 13 | 14 | const browser = detect() 15 | 16 | describe('webrtc basic', () => { 17 | const isFirefox = ((browser != null) && browser.name === 'firefox') 18 | it('should connect', async () => { 19 | const [receiver, initiator] = duplexPair() 20 | const dstPeerId = await createEd25519PeerId() 21 | const connection = mockConnection( 22 | mockMultiaddrConnection(pair(), dstPeerId) 23 | ) 24 | const controller = new AbortController() 25 | const initiatorPeerConnectionPromise = initiateConnection({ stream: mockStream(initiator), signal: controller.signal }) 26 | const receiverPeerConnectionPromise = handleIncomingStream({ stream: mockStream(receiver), connection }) 27 | await expect(initiatorPeerConnectionPromise).to.be.fulfilled() 28 | await expect(receiverPeerConnectionPromise).to.be.fulfilled() 29 | const [{ pc: pc0 }, { pc: pc1 }] = await Promise.all([initiatorPeerConnectionPromise, receiverPeerConnectionPromise]) 30 | if (isFirefox) { 31 | expect(pc0.iceConnectionState).eq('connected') 32 | expect(pc1.iceConnectionState).eq('connected') 33 | return 34 | } 35 | expect(pc0.connectionState).eq('connected') 36 | expect(pc1.connectionState).eq('connected') 37 | }) 38 | }) 39 | 40 | describe('webrtc receiver', () => { 41 | it('should fail receiving on invalid sdp offer', async () => { 42 | const [receiver, initiator] = duplexPair() 43 | const dstPeerId = await createEd25519PeerId() 44 | const connection = mockConnection( 45 | mockMultiaddrConnection(pair(), dstPeerId) 46 | ) 47 | const receiverPeerConnectionPromise = handleIncomingStream({ stream: mockStream(receiver), connection }) 48 | const stream = pbStream(initiator).pb(Message) 49 | 50 | stream.write({ type: Message.Type.SDP_OFFER, data: 'bad' }) 51 | await expect(receiverPeerConnectionPromise).to.be.rejectedWith(/Failed to set remoteDescription/) 52 | }) 53 | }) 54 | 55 | describe('webrtc dialer', () => { 56 | it('should fail receiving on invalid sdp answer', async () => { 57 | const [receiver, initiator] = duplexPair() 58 | const controller = new AbortController() 59 | const initiatorPeerConnectionPromise = initiateConnection({ signal: controller.signal, stream: mockStream(initiator) }) 60 | const stream = pbStream(receiver).pb(Message) 61 | 62 | { 63 | const offerMessage = await stream.read() 64 | expect(offerMessage.type).to.eq(Message.Type.SDP_OFFER) 65 | } 66 | 67 | stream.write({ type: Message.Type.SDP_ANSWER, data: 'bad' }) 68 | await expect(initiatorPeerConnectionPromise).to.be.rejectedWith(/Failed to set remoteDescription/) 69 | }) 70 | 71 | it('should fail on receiving a candidate before an answer', async () => { 72 | const [receiver, initiator] = duplexPair() 73 | const controller = new AbortController() 74 | const initiatorPeerConnectionPromise = initiateConnection({ signal: controller.signal, stream: mockStream(initiator) }) 75 | const stream = pbStream(receiver).pb(Message) 76 | 77 | const pc = new RTCPeerConnection() 78 | pc.onicecandidate = ({ candidate }) => { 79 | stream.write({ type: Message.Type.ICE_CANDIDATE, data: JSON.stringify(candidate?.toJSON()) }) 80 | } 81 | { 82 | const offerMessage = await stream.read() 83 | expect(offerMessage.type).to.eq(Message.Type.SDP_OFFER) 84 | const offer = new RTCSessionDescription({ type: 'offer', sdp: offerMessage.data }) 85 | await pc.setRemoteDescription(offer) 86 | 87 | const answer = await pc.createAnswer() 88 | await pc.setLocalDescription(answer) 89 | } 90 | 91 | await expect(initiatorPeerConnectionPromise).to.be.rejectedWith(/remote should send an SDP answer/) 92 | }) 93 | }) 94 | 95 | describe('webrtc filter', () => { 96 | it('can filter multiaddrs to dial', async () => { 97 | const transport = new WebRTCTransport({ 98 | transportManager: Sinon.stub() as any, 99 | peerId: Sinon.stub() as any, 100 | registrar: mockRegistrar(), 101 | upgrader: mockUpgrader({}) 102 | }, {}) 103 | 104 | const valid = [ 105 | multiaddr('/ip4/127.0.0.1/tcp/1234/ws/p2p-circuit/webrtc') 106 | ] 107 | 108 | expect(transport.filter(valid)).length(1) 109 | }) 110 | }) 111 | 112 | describe('webrtc splitAddr', () => { 113 | it('can split a ws relay addr', async () => { 114 | const ma = multiaddr('/ip4/127.0.0.1/tcp/49173/ws/p2p/12D3KooWFqpHsdZaL4NW6eVE3yjhoSDNv7HJehPZqj17kjKntAh2/p2p-circuit/webrtc/p2p/12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') 115 | 116 | const { baseAddr, peerId } = splitAddr(ma) 117 | 118 | expect(baseAddr.toString()).to.eq('/ip4/127.0.0.1/tcp/49173/ws/p2p/12D3KooWFqpHsdZaL4NW6eVE3yjhoSDNv7HJehPZqj17kjKntAh2/p2p-circuit/p2p/12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') 119 | expect(peerId.toString()).to.eq('12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') 120 | }) 121 | 122 | it('can split a webrtc-direct relay addr', async () => { 123 | const ma = multiaddr('/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiBUr89tH2P9paTCPn-AcfVZcgvIvkwns96t4h55IpxFtA/p2p/12D3KooWB64sJqc3T3VCaubQCrfCvvfummrAA9z1vEXHJT77ZNJh/p2p-circuit/webrtc/p2p/12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') 124 | 125 | const { baseAddr, peerId } = splitAddr(ma) 126 | 127 | expect(baseAddr.toString()).to.eq('/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiBUr89tH2P9paTCPn-AcfVZcgvIvkwns96t4h55IpxFtA/p2p/12D3KooWB64sJqc3T3VCaubQCrfCvvfummrAA9z1vEXHJT77ZNJh/p2p-circuit/p2p/12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') 128 | expect(peerId.toString()).to.eq('12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') 129 | }) 130 | }) 131 | 132 | export { } 133 | -------------------------------------------------------------------------------- /.github/workflows/js-test-and-release.yml: -------------------------------------------------------------------------------- 1 | # File managed by web3-bot. DO NOT EDIT. 2 | # See https://github.com/protocol/.github/ for details. 3 | 4 | name: test & maybe release 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | jobs: 12 | 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: lts/* 20 | - uses: ipfs/aegir/actions/cache-node-modules@master 21 | - run: npm run --if-present lint 22 | - run: npm run --if-present dep-check 23 | 24 | test-node: 25 | needs: check 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | os: [windows-latest, ubuntu-latest, macos-latest] 30 | node: [lts/*] 31 | fail-fast: true 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node }} 37 | - uses: ipfs/aegir/actions/cache-node-modules@master 38 | - run: npm run --if-present test:node 39 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 40 | with: 41 | flags: node 42 | 43 | test-chrome: 44 | needs: check 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: actions/setup-node@v3 49 | with: 50 | node-version: lts/* 51 | - uses: ipfs/aegir/actions/cache-node-modules@master 52 | - run: npm run --if-present test:chrome 53 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 54 | with: 55 | flags: chrome 56 | 57 | test-chrome-webworker: 58 | needs: check 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v3 62 | - uses: actions/setup-node@v3 63 | with: 64 | node-version: lts/* 65 | - uses: ipfs/aegir/actions/cache-node-modules@master 66 | - run: npm run --if-present test:chrome-webworker 67 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 68 | with: 69 | flags: chrome-webworker 70 | 71 | test-firefox: 72 | needs: check 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: actions/checkout@v3 76 | - uses: actions/setup-node@v3 77 | with: 78 | node-version: lts/* 79 | - uses: ipfs/aegir/actions/cache-node-modules@master 80 | - run: npm run --if-present test:firefox 81 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 82 | with: 83 | flags: firefox 84 | 85 | test-firefox-webworker: 86 | needs: check 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v3 90 | - uses: actions/setup-node@v3 91 | with: 92 | node-version: lts/* 93 | - uses: ipfs/aegir/actions/cache-node-modules@master 94 | - run: npm run --if-present test:firefox-webworker 95 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 96 | with: 97 | flags: firefox-webworker 98 | 99 | test-webkit: 100 | needs: check 101 | runs-on: ${{ matrix.os }} 102 | strategy: 103 | matrix: 104 | os: [ubuntu-latest, macos-latest] 105 | node: [lts/*] 106 | fail-fast: true 107 | steps: 108 | - uses: actions/checkout@v3 109 | - uses: actions/setup-node@v3 110 | with: 111 | node-version: lts/* 112 | - uses: ipfs/aegir/actions/cache-node-modules@master 113 | - run: npm run --if-present test:webkit 114 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 115 | with: 116 | flags: webkit 117 | 118 | test-webkit-webworker: 119 | needs: check 120 | runs-on: ${{ matrix.os }} 121 | strategy: 122 | matrix: 123 | os: [ubuntu-latest, macos-latest] 124 | node: [lts/*] 125 | fail-fast: true 126 | steps: 127 | - uses: actions/checkout@v3 128 | - uses: actions/setup-node@v3 129 | with: 130 | node-version: lts/* 131 | - uses: ipfs/aegir/actions/cache-node-modules@master 132 | - run: npm run --if-present test:webkit-webworker 133 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 134 | with: 135 | flags: webkit-webworker 136 | 137 | test-electron-main: 138 | needs: check 139 | runs-on: ubuntu-latest 140 | steps: 141 | - uses: actions/checkout@v3 142 | - uses: actions/setup-node@v3 143 | with: 144 | node-version: lts/* 145 | - uses: ipfs/aegir/actions/cache-node-modules@master 146 | - run: npx xvfb-maybe npm run --if-present test:electron-main 147 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 148 | with: 149 | flags: electron-main 150 | 151 | test-electron-renderer: 152 | needs: check 153 | runs-on: ubuntu-latest 154 | steps: 155 | - uses: actions/checkout@v3 156 | - uses: actions/setup-node@v3 157 | with: 158 | node-version: lts/* 159 | - uses: ipfs/aegir/actions/cache-node-modules@master 160 | - run: npx xvfb-maybe npm run --if-present test:electron-renderer 161 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 162 | with: 163 | flags: electron-renderer 164 | 165 | release: 166 | needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-webkit, test-webkit-webworker, test-electron-main, test-electron-renderer] 167 | runs-on: ubuntu-latest 168 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 169 | steps: 170 | - uses: actions/checkout@v3 171 | with: 172 | fetch-depth: 0 173 | - uses: actions/setup-node@v3 174 | with: 175 | node-version: lts/* 176 | - uses: ipfs/aegir/actions/cache-node-modules@master 177 | - uses: ipfs/aegir/actions/docker-login@master 178 | with: 179 | docker-token: ${{ secrets.DOCKER_TOKEN }} 180 | docker-username: ${{ secrets.DOCKER_USERNAME }} 181 | - run: npm run --if-present release 182 | env: 183 | GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN || github.token }} 184 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 185 | -------------------------------------------------------------------------------- /src/private-to-private/handler.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@libp2p/logger' 2 | import { abortableDuplex } from 'abortable-iterator' 3 | import { pbStream } from 'it-pb-stream' 4 | import pDefer, { type DeferredPromise } from 'p-defer' 5 | import { DataChannelMuxerFactory } from '../muxer.js' 6 | import { Message } from './pb/message.js' 7 | import { readCandidatesUntilConnected, resolveOnConnected } from './util.js' 8 | import type { DataChannelOpts } from '../stream.js' 9 | import type { Stream } from '@libp2p/interface-connection' 10 | import type { IncomingStreamData } from '@libp2p/interface-registrar' 11 | import type { StreamMuxerFactory } from '@libp2p/interface-stream-muxer' 12 | 13 | const DEFAULT_TIMEOUT = 30 * 1000 14 | 15 | const log = logger('libp2p:webrtc:peer') 16 | 17 | export type IncomingStreamOpts = { rtcConfiguration?: RTCConfiguration, dataChannelOptions?: Partial } & IncomingStreamData 18 | 19 | export async function handleIncomingStream ({ rtcConfiguration, dataChannelOptions, stream: rawStream }: IncomingStreamOpts): Promise<{ pc: RTCPeerConnection, muxerFactory: StreamMuxerFactory, remoteAddress: string }> { 20 | const signal = AbortSignal.timeout(DEFAULT_TIMEOUT) 21 | const stream = pbStream(abortableDuplex(rawStream, signal)).pb(Message) 22 | const pc = new RTCPeerConnection(rtcConfiguration) 23 | const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions }) 24 | const connectedPromise: DeferredPromise = pDefer() 25 | const answerSentPromise: DeferredPromise = pDefer() 26 | 27 | signal.onabort = () => { connectedPromise.reject() } 28 | // candidate callbacks 29 | pc.onicecandidate = ({ candidate }) => { 30 | answerSentPromise.promise.then( 31 | () => { 32 | stream.write({ 33 | type: Message.Type.ICE_CANDIDATE, 34 | data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : '' 35 | }) 36 | }, 37 | (err) => { 38 | log.error('cannot set candidate since sending answer failed', err) 39 | } 40 | ) 41 | } 42 | 43 | resolveOnConnected(pc, connectedPromise) 44 | 45 | // read an SDP offer 46 | const pbOffer = await stream.read() 47 | if (pbOffer.type !== Message.Type.SDP_OFFER) { 48 | throw new Error(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `) 49 | } 50 | const offer = new RTCSessionDescription({ 51 | type: 'offer', 52 | sdp: pbOffer.data 53 | }) 54 | 55 | await pc.setRemoteDescription(offer).catch(err => { 56 | log.error('could not execute setRemoteDescription', err) 57 | throw new Error('Failed to set remoteDescription') 58 | }) 59 | 60 | // create and write an SDP answer 61 | const answer = await pc.createAnswer().catch(err => { 62 | log.error('could not execute createAnswer', err) 63 | answerSentPromise.reject(err) 64 | throw new Error('Failed to create answer') 65 | }) 66 | // write the answer to the remote 67 | stream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp }) 68 | 69 | await pc.setLocalDescription(answer).catch(err => { 70 | log.error('could not execute setLocalDescription', err) 71 | answerSentPromise.reject(err) 72 | throw new Error('Failed to set localDescription') 73 | }) 74 | 75 | answerSentPromise.resolve() 76 | 77 | // wait until candidates are connected 78 | await readCandidatesUntilConnected(connectedPromise, pc, stream) 79 | 80 | const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '') 81 | 82 | return { pc, muxerFactory, remoteAddress } 83 | } 84 | 85 | export interface ConnectOptions { 86 | stream: Stream 87 | signal: AbortSignal 88 | rtcConfiguration?: RTCConfiguration 89 | dataChannelOptions?: Partial 90 | } 91 | 92 | export async function initiateConnection ({ rtcConfiguration, dataChannelOptions, signal, stream: rawStream }: ConnectOptions): Promise<{ pc: RTCPeerConnection, muxerFactory: StreamMuxerFactory, remoteAddress: string }> { 93 | const stream = pbStream(abortableDuplex(rawStream, signal)).pb(Message) 94 | // setup peer connection 95 | const pc = new RTCPeerConnection(rtcConfiguration) 96 | const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions }) 97 | 98 | const connectedPromise: DeferredPromise = pDefer() 99 | resolveOnConnected(pc, connectedPromise) 100 | 101 | // reject the connectedPromise if the signal aborts 102 | signal.onabort = connectedPromise.reject 103 | // we create the channel so that the peerconnection has a component for which 104 | // to collect candidates. The label is not relevant to connection initiation 105 | // but can be useful for debugging 106 | const channel = pc.createDataChannel('init') 107 | // setup callback to write ICE candidates to the remote 108 | // peer 109 | pc.onicecandidate = ({ candidate }) => { 110 | stream.write({ 111 | type: Message.Type.ICE_CANDIDATE, 112 | data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : '' 113 | }) 114 | } 115 | // create an offer 116 | const offerSdp = await pc.createOffer() 117 | // write the offer to the stream 118 | stream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp }) 119 | // set offer as local description 120 | await pc.setLocalDescription(offerSdp).catch(err => { 121 | log.error('could not execute setLocalDescription', err) 122 | throw new Error('Failed to set localDescription') 123 | }) 124 | 125 | // read answer 126 | const answerMessage = await stream.read() 127 | if (answerMessage.type !== Message.Type.SDP_ANSWER) { 128 | throw new Error('remote should send an SDP answer') 129 | } 130 | 131 | const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data }) 132 | await pc.setRemoteDescription(answerSdp).catch(err => { 133 | log.error('could not execute setRemoteDescription', err) 134 | throw new Error('Failed to set remoteDescription') 135 | }) 136 | 137 | await readCandidatesUntilConnected(connectedPromise, pc, stream) 138 | channel.close() 139 | 140 | const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '') 141 | 142 | return { pc, muxerFactory, remoteAddress } 143 | } 144 | 145 | function parseRemoteAddress (sdp: string): string { 146 | // 'a=candidate:1746876089 1 udp 2113937151 0614fbad-b...ocal 54882 typ host generation 0 network-cost 999' 147 | const candidateLine = sdp.split('\r\n').filter(line => line.startsWith('a=candidate')).pop() 148 | const candidateParts = candidateLine?.split(' ') 149 | 150 | if (candidateLine == null || candidateParts == null || candidateParts.length < 5) { 151 | log('could not parse remote address from', candidateLine) 152 | return '/webrtc' 153 | } 154 | 155 | return `/dnsaddr/${candidateParts[4]}/${candidateParts[2].toLowerCase()}/${candidateParts[3]}/webrtc` 156 | } 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📁 Archived - this module has been merged into [js-libp2p](https://github.com/libp2p/js-libp2p/tree/master/packages/transport-webrtc) 2 | 3 | # @libp2p/webrtc 4 | 5 | [![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) 6 | [![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) 7 | [![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-webrtc.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc) 8 | [![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p-webrtc/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/libp2p/js-libp2p-webrtc/actions/workflows/js-test-and-release.yml?query=branch%3Amain) 9 | 10 | > A libp2p transport using WebRTC connections 11 | 12 | ## Table of contents 13 | 14 | - [Install](#install) 15 | - [Browser ` 43 | ``` 44 | 45 | ## Usage 46 | 47 | ```js 48 | import { createLibp2p } from 'libp2p' 49 | import { noise } from '@chainsafe/libp2p-noise' 50 | import { multiaddr } from '@multiformats/multiaddr' 51 | import first from 'it-first' 52 | import { pipe } from 'it-pipe' 53 | import { fromString, toString } from 'uint8arrays' 54 | import { webRTC } from '@libp2p/webrtc' 55 | 56 | const node = await createLibp2p({ 57 | transports: [webRTC()], 58 | connectionEncryption: [noise()], 59 | }); 60 | 61 | await node.start() 62 | 63 | const ma = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ') 64 | const stream = await node.dialProtocol(ma, ['/my-protocol/1.0.0']) 65 | const message = `Hello js-libp2p-webrtc\n` 66 | const response = await pipe([fromString(message)], stream, async (source) => await first(source)) 67 | const responseDecoded = toString(response.slice(0, response.length)) 68 | ``` 69 | 70 | ## Examples 71 | 72 | Examples can be found in the [examples folder](examples/README.md). 73 | 74 | ## Interfaces 75 | 76 | ### Transport 77 | 78 | ![https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/interface-transport](https://raw.githubusercontent.com/libp2p/js-libp2p-interfaces/master/packages/interface-transport/img/badge.png) 79 | 80 | Browsers can usually only `dial`, but `listen` is supported in the WebRTC 81 | transport when paired with another listener like CircuitV2, where you listen on 82 | a relayed connection. Take a look at [index.js](examples/browser-to-browser/index.js) for 83 | an example. 84 | 85 | ### Connection 86 | 87 | ![https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/interface-connection](https://raw.githubusercontent.com/libp2p/js-libp2p-interfaces/master/packages/interface-connection/img/badge.png) 88 | 89 | ```js 90 | interface MultiaddrConnection extends Duplex { 91 | close: (err?: Error) => Promise 92 | remoteAddr: Multiaddr 93 | timeline: MultiaddrConnectionTimeline 94 | } 95 | 96 | class WebRTCMultiaddrConnection implements MultiaddrConnection { } 97 | ``` 98 | 99 | ## Development 100 | 101 | Contributions are welcome! The libp2p implementation in JavaScript is a work in progress. As such, there's a few things you can do right now to help out: 102 | 103 | - [Check out the existing issues](//github.com/little-bear-labs/js-libp2p-webrtc/issues). 104 | - **Perform code reviews**. 105 | - **Add tests**. There can never be enough tests. 106 | - Go through the modules and **check out existing issues**. This is especially useful for modules in active development. Some knowledge of IPFS/libp2p may be required, as well as the infrastructure behind it - for instance, you may need to read up on p2p and more complex operations like muxing to be able to help technically. 107 | 108 | Please be aware that all interactions related to libp2p are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). 109 | 110 | Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 111 | 112 | This module leans heavily on (Aegir)\[] for most of the `package.json` scripts. 113 | 114 | ### Build 115 | 116 | The build script is a wrapper to `aegir build`. To build this package: 117 | 118 | ```shell 119 | npm run build 120 | ``` 121 | 122 | The build will be located in the `/dist` folder. 123 | 124 | ### Protocol Buffers 125 | 126 | There is also `npm run generate:proto` script that uses protoc to populate the generated code directory `proto_ts` based on `*.proto` files in src. Don't forget to run this step before `build` any time you make a change to any of the `*.proto` files. 127 | 128 | ### Test 129 | 130 | To run all tests: 131 | 132 | ```shell 133 | npm test 134 | ``` 135 | 136 | To run tests for Chrome only: 137 | 138 | ```shell 139 | npm run test:chrome 140 | ``` 141 | 142 | To run tests for Firefox only: 143 | 144 | ```shell 145 | npm run test:firefox 146 | ``` 147 | 148 | ### Lint 149 | 150 | Aegir is also used to lint the code, which follows the [Standard](https://github.com/standard/standard) JS linter. 151 | The VS Code plugin for this standard is located at . 152 | To lint this repo: 153 | 154 | ```shell 155 | npm run lint 156 | ``` 157 | 158 | You can also auto-fix when applicable: 159 | 160 | ```shell 161 | npm run lint:fix 162 | ``` 163 | 164 | ### Clean 165 | 166 | ```shell 167 | npm run clean 168 | ``` 169 | 170 | ### Check Dependencies 171 | 172 | ```shell 173 | npm run deps-check 174 | ``` 175 | 176 | ## License 177 | 178 | Licensed under either of 179 | 180 | - Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) 181 | - MIT ([LICENSE-MIT](LICENSE-MIT) / ) 182 | 183 | ## Contribution 184 | 185 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 186 | -------------------------------------------------------------------------------- /src/private-to-private/transport.ts: -------------------------------------------------------------------------------- 1 | import { type CreateListenerOptions, type DialOptions, type Listener, symbol, type Transport, type Upgrader, type TransportManager } from '@libp2p/interface-transport' 2 | import { CodeError } from '@libp2p/interfaces/errors' 3 | import { logger } from '@libp2p/logger' 4 | import { peerIdFromString } from '@libp2p/peer-id' 5 | import { multiaddr, type Multiaddr, protocols } from '@multiformats/multiaddr' 6 | import { codes } from '../error.js' 7 | import { WebRTCMultiaddrConnection } from '../maconn.js' 8 | import { initiateConnection, handleIncomingStream } from './handler.js' 9 | import { WebRTCPeerListener } from './listener.js' 10 | import type { DataChannelOpts } from '../stream.js' 11 | import type { Connection } from '@libp2p/interface-connection' 12 | import type { PeerId } from '@libp2p/interface-peer-id' 13 | import type { IncomingStreamData, Registrar } from '@libp2p/interface-registrar' 14 | import type { Startable } from '@libp2p/interfaces/startable' 15 | 16 | const log = logger('libp2p:webrtc:peer') 17 | 18 | const WEBRTC_TRANSPORT = '/webrtc' 19 | const CIRCUIT_RELAY_TRANSPORT = '/p2p-circuit' 20 | const SIGNALING_PROTO_ID = '/webrtc-signaling/0.0.1' 21 | const WEBRTC_CODE = protocols('webrtc').code 22 | 23 | export interface WebRTCTransportInit { 24 | rtcConfiguration?: RTCConfiguration 25 | dataChannel?: Partial 26 | } 27 | 28 | export interface WebRTCTransportComponents { 29 | peerId: PeerId 30 | registrar: Registrar 31 | upgrader: Upgrader 32 | transportManager: TransportManager 33 | } 34 | 35 | export class WebRTCTransport implements Transport, Startable { 36 | private _started = false 37 | 38 | constructor ( 39 | private readonly components: WebRTCTransportComponents, 40 | private readonly init: WebRTCTransportInit = {} 41 | ) { 42 | } 43 | 44 | isStarted (): boolean { 45 | return this._started 46 | } 47 | 48 | async start (): Promise { 49 | await this.components.registrar.handle(SIGNALING_PROTO_ID, (data: IncomingStreamData) => { 50 | this._onProtocol(data).catch(err => { log.error('failed to handle incoming connect from %p', data.connection.remotePeer, err) }) 51 | }) 52 | this._started = true 53 | } 54 | 55 | async stop (): Promise { 56 | await this.components.registrar.unhandle(SIGNALING_PROTO_ID) 57 | this._started = false 58 | } 59 | 60 | createListener (options: CreateListenerOptions): Listener { 61 | return new WebRTCPeerListener(this.components) 62 | } 63 | 64 | readonly [Symbol.toStringTag] = '@libp2p/webrtc' 65 | 66 | readonly [symbol] = true 67 | 68 | filter (multiaddrs: Multiaddr[]): Multiaddr[] { 69 | return multiaddrs.filter((ma) => { 70 | const codes = ma.protoCodes() 71 | return codes.includes(WEBRTC_CODE) 72 | }) 73 | } 74 | 75 | /* 76 | * dial connects to a remote via the circuit relay or any other protocol 77 | * and proceeds to upgrade to a webrtc connection. 78 | * multiaddr of the form: /webrtc/p2p/ 79 | * For a circuit relay, this will be of the form 80 | * /p2p//p2p-circuit/webrtc/p2p/ 81 | */ 82 | async dial (ma: Multiaddr, options: DialOptions): Promise { 83 | log.trace('dialing address: ', ma) 84 | const { baseAddr, peerId } = splitAddr(ma) 85 | 86 | if (options.signal == null) { 87 | const controller = new AbortController() 88 | options.signal = controller.signal 89 | } 90 | 91 | const connection = await this.components.transportManager.dial(baseAddr, options) 92 | const signalingStream = await connection.newStream([SIGNALING_PROTO_ID], options) 93 | 94 | try { 95 | const { pc, muxerFactory, remoteAddress } = await initiateConnection({ 96 | stream: signalingStream, 97 | rtcConfiguration: this.init.rtcConfiguration, 98 | dataChannelOptions: this.init.dataChannel, 99 | signal: options.signal 100 | }) 101 | 102 | const result = await options.upgrader.upgradeOutbound( 103 | new WebRTCMultiaddrConnection({ 104 | peerConnection: pc, 105 | timeline: { open: Date.now() }, 106 | remoteAddr: multiaddr(remoteAddress).encapsulate(`/p2p/${peerId.toString()}`) 107 | }), 108 | { 109 | skipProtection: true, 110 | skipEncryption: true, 111 | muxerFactory 112 | } 113 | ) 114 | 115 | // close the stream if SDP has been exchanged successfully 116 | signalingStream.close() 117 | return result 118 | } catch (err) { 119 | // reset the stream in case of any error 120 | signalingStream.reset() 121 | throw err 122 | } finally { 123 | // Close the signaling connection 124 | await connection.close() 125 | } 126 | } 127 | 128 | async _onProtocol ({ connection, stream }: IncomingStreamData): Promise { 129 | try { 130 | const { pc, muxerFactory, remoteAddress } = await handleIncomingStream({ 131 | rtcConfiguration: this.init.rtcConfiguration, 132 | connection, 133 | stream, 134 | dataChannelOptions: this.init.dataChannel 135 | }) 136 | 137 | await this.components.upgrader.upgradeInbound(new WebRTCMultiaddrConnection({ 138 | peerConnection: pc, 139 | timeline: { open: (new Date()).getTime() }, 140 | remoteAddr: multiaddr(remoteAddress).encapsulate(`/p2p/${connection.remotePeer.toString()}`) 141 | }), { 142 | skipEncryption: true, 143 | skipProtection: true, 144 | muxerFactory 145 | }) 146 | } catch (err) { 147 | stream.reset() 148 | throw err 149 | } finally { 150 | // Close the signaling connection 151 | await connection.close() 152 | } 153 | } 154 | } 155 | 156 | export function splitAddr (ma: Multiaddr): { baseAddr: Multiaddr, peerId: PeerId } { 157 | const addrs = ma.toString().split(WEBRTC_TRANSPORT + '/') 158 | if (addrs.length !== 2) { 159 | throw new CodeError('webrtc protocol was not present in multiaddr', codes.ERR_INVALID_MULTIADDR) 160 | } 161 | 162 | if (!addrs[0].includes(CIRCUIT_RELAY_TRANSPORT)) { 163 | throw new CodeError('p2p-circuit protocol was not present in multiaddr', codes.ERR_INVALID_MULTIADDR) 164 | } 165 | 166 | // look for remote peerId 167 | let remoteAddr = multiaddr(addrs[0]) 168 | const destination = multiaddr('/' + addrs[1]) 169 | 170 | const destinationIdString = destination.getPeerId() 171 | if (destinationIdString == null) { 172 | throw new CodeError('destination peer id was missing', codes.ERR_INVALID_MULTIADDR) 173 | } 174 | 175 | const lastProtoInRemote = remoteAddr.protos().pop() 176 | if (lastProtoInRemote === undefined) { 177 | throw new CodeError('invalid multiaddr', codes.ERR_INVALID_MULTIADDR) 178 | } 179 | if (lastProtoInRemote.name !== 'p2p') { 180 | remoteAddr = remoteAddr.encapsulate(`/p2p/${destinationIdString}`) 181 | } 182 | 183 | return { baseAddr: remoteAddr, peerId: peerIdFromString(destinationIdString) } 184 | } 185 | -------------------------------------------------------------------------------- /src/stream.ts: -------------------------------------------------------------------------------- 1 | import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface-stream-muxer/stream' 2 | import { CodeError } from '@libp2p/interfaces/errors' 3 | import { logger } from '@libp2p/logger' 4 | import * as lengthPrefixed from 'it-length-prefixed' 5 | import { type Pushable, pushable } from 'it-pushable' 6 | import { pEvent, TimeoutError } from 'p-event' 7 | import { Uint8ArrayList } from 'uint8arraylist' 8 | import { Message } from './pb/message.js' 9 | import type { Direction, Stream } from '@libp2p/interface-connection' 10 | 11 | const log = logger('libp2p:webrtc:stream') 12 | 13 | export interface DataChannelOpts { 14 | maxMessageSize: number 15 | maxBufferedAmount: number 16 | bufferedAmountLowEventTimeout: number 17 | } 18 | 19 | export interface WebRTCStreamInit extends AbstractStreamInit { 20 | /** 21 | * The network channel used for bidirectional peer-to-peer transfers of 22 | * arbitrary data 23 | * 24 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel} 25 | */ 26 | channel: RTCDataChannel 27 | 28 | dataChannelOptions?: Partial 29 | } 30 | 31 | // Max message size that can be sent to the DataChannel 32 | const MAX_MESSAGE_SIZE = 16 * 1024 33 | 34 | // How much can be buffered to the DataChannel at once 35 | const MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024 36 | 37 | // How long time we wait for the 'bufferedamountlow' event to be emitted 38 | const BUFFERED_AMOUNT_LOW_TIMEOUT = 30 * 1000 39 | 40 | // protobuf field definition overhead 41 | const PROTOBUF_OVERHEAD = 3 42 | 43 | class WebRTCStream extends AbstractStream { 44 | /** 45 | * The data channel used to send and receive data 46 | */ 47 | private readonly channel: RTCDataChannel 48 | 49 | /** 50 | * Data channel options 51 | */ 52 | private readonly dataChannelOptions: DataChannelOpts 53 | 54 | /** 55 | * push data from the underlying datachannel to the length prefix decoder 56 | * and then the protobuf decoder. 57 | */ 58 | private readonly incomingData: Pushable 59 | 60 | private messageQueue?: Uint8ArrayList 61 | 62 | constructor (init: WebRTCStreamInit) { 63 | super(init) 64 | 65 | this.channel = init.channel 66 | this.channel.binaryType = 'arraybuffer' 67 | this.incomingData = pushable() 68 | this.messageQueue = new Uint8ArrayList() 69 | this.dataChannelOptions = { 70 | bufferedAmountLowEventTimeout: init.dataChannelOptions?.bufferedAmountLowEventTimeout ?? BUFFERED_AMOUNT_LOW_TIMEOUT, 71 | maxBufferedAmount: init.dataChannelOptions?.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT, 72 | maxMessageSize: init.dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE 73 | } 74 | 75 | // set up initial state 76 | switch (this.channel.readyState) { 77 | case 'open': 78 | break 79 | 80 | case 'closed': 81 | case 'closing': 82 | if (this.stat.timeline.close === undefined || this.stat.timeline.close === 0) { 83 | this.stat.timeline.close = Date.now() 84 | } 85 | break 86 | case 'connecting': 87 | // noop 88 | break 89 | 90 | default: 91 | log.error('unknown datachannel state %s', this.channel.readyState) 92 | throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE') 93 | } 94 | 95 | // handle RTCDataChannel events 96 | this.channel.onopen = (_evt) => { 97 | this.stat.timeline.open = new Date().getTime() 98 | 99 | if (this.messageQueue != null) { 100 | // send any queued messages 101 | this._sendMessage(this.messageQueue) 102 | .catch(err => { 103 | this.abort(err) 104 | }) 105 | this.messageQueue = undefined 106 | } 107 | } 108 | 109 | this.channel.onclose = (_evt) => { 110 | this.close() 111 | } 112 | 113 | this.channel.onerror = (evt) => { 114 | const err = (evt as RTCErrorEvent).error 115 | this.abort(err) 116 | } 117 | 118 | const self = this 119 | 120 | this.channel.onmessage = async (event: MessageEvent) => { 121 | const { data } = event 122 | 123 | if (data === null || data.byteLength === 0) { 124 | return 125 | } 126 | 127 | this.incomingData.push(new Uint8Array(data, 0, data.byteLength)) 128 | } 129 | 130 | // pipe framed protobuf messages through a length prefixed decoder, and 131 | // surface data from the `Message.message` field through a source. 132 | Promise.resolve().then(async () => { 133 | for await (const buf of lengthPrefixed.decode(this.incomingData)) { 134 | const message = self.processIncomingProtobuf(buf.subarray()) 135 | 136 | if (message != null) { 137 | self.sourcePush(new Uint8ArrayList(message)) 138 | } 139 | } 140 | }) 141 | .catch(err => { 142 | log.error('error processing incoming data channel messages', err) 143 | }) 144 | } 145 | 146 | sendNewStream (): void { 147 | // opening new streams is handled by WebRTC so this is a noop 148 | } 149 | 150 | async _sendMessage (data: Uint8ArrayList, checkBuffer: boolean = true): Promise { 151 | if (checkBuffer && this.channel.bufferedAmount > this.dataChannelOptions.maxBufferedAmount) { 152 | try { 153 | await pEvent(this.channel, 'bufferedamountlow', { timeout: this.dataChannelOptions.bufferedAmountLowEventTimeout }) 154 | } catch (err: any) { 155 | if (err instanceof TimeoutError) { 156 | this.abort(err) 157 | throw new Error('Timed out waiting for DataChannel buffer to clear') 158 | } 159 | 160 | throw err 161 | } 162 | } 163 | 164 | if (this.channel.readyState === 'closed' || this.channel.readyState === 'closing') { 165 | throw new CodeError('Invalid datachannel state - closed or closing', 'ERR_INVALID_STATE') 166 | } 167 | 168 | if (this.channel.readyState === 'open') { 169 | // send message without copying data 170 | for (const buf of data) { 171 | this.channel.send(buf) 172 | } 173 | } else if (this.channel.readyState === 'connecting') { 174 | // queue message for when we are open 175 | if (this.messageQueue == null) { 176 | this.messageQueue = new Uint8ArrayList() 177 | } 178 | 179 | this.messageQueue.append(data) 180 | } else { 181 | log.error('unknown datachannel state %s', this.channel.readyState) 182 | throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE') 183 | } 184 | } 185 | 186 | async sendData (data: Uint8ArrayList): Promise { 187 | const msgbuf = Message.encode({ message: data.subarray() }) 188 | const sendbuf = lengthPrefixed.encode.single(msgbuf) 189 | 190 | await this._sendMessage(sendbuf) 191 | } 192 | 193 | async sendReset (): Promise { 194 | await this._sendFlag(Message.Flag.RESET) 195 | } 196 | 197 | async sendCloseWrite (): Promise { 198 | await this._sendFlag(Message.Flag.FIN) 199 | } 200 | 201 | async sendCloseRead (): Promise { 202 | await this._sendFlag(Message.Flag.STOP_SENDING) 203 | } 204 | 205 | /** 206 | * Handle incoming 207 | */ 208 | private processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined { 209 | const message = Message.decode(buffer) 210 | 211 | if (message.flag !== undefined) { 212 | if (message.flag === Message.Flag.FIN) { 213 | // We should expect no more data from the remote, stop reading 214 | this.incomingData.end() 215 | this.closeRead() 216 | } 217 | 218 | if (message.flag === Message.Flag.RESET) { 219 | // Stop reading and writing to the stream immediately 220 | this.reset() 221 | } 222 | 223 | if (message.flag === Message.Flag.STOP_SENDING) { 224 | // The remote has stopped reading 225 | this.closeWrite() 226 | } 227 | } 228 | 229 | return message.message 230 | } 231 | 232 | private async _sendFlag (flag: Message.Flag): Promise { 233 | log.trace('Sending flag: %s', flag.toString()) 234 | const msgbuf = Message.encode({ flag }) 235 | const prefixedBuf = lengthPrefixed.encode.single(msgbuf) 236 | 237 | await this._sendMessage(prefixedBuf, false) 238 | } 239 | } 240 | 241 | export interface WebRTCStreamOptions { 242 | /** 243 | * The network channel used for bidirectional peer-to-peer transfers of 244 | * arbitrary data 245 | * 246 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel} 247 | */ 248 | channel: RTCDataChannel 249 | 250 | /** 251 | * The stream direction 252 | */ 253 | direction: Direction 254 | 255 | dataChannelOptions?: Partial 256 | 257 | maxMsgSize?: number 258 | 259 | onEnd?: (err?: Error | undefined) => void 260 | } 261 | 262 | export function createStream (options: WebRTCStreamOptions): Stream { 263 | const { channel, direction, onEnd, dataChannelOptions } = options 264 | 265 | return new WebRTCStream({ 266 | id: direction === 'inbound' ? (`i${channel.id}`) : `r${channel.id}`, 267 | direction, 268 | maxDataSize: (dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD, 269 | dataChannelOptions, 270 | onEnd, 271 | channel 272 | }) 273 | } 274 | -------------------------------------------------------------------------------- /src/private-to-public/transport.ts: -------------------------------------------------------------------------------- 1 | import { noise as Noise } from '@chainsafe/libp2p-noise' 2 | import { type CreateListenerOptions, type Listener, symbol, type Transport } from '@libp2p/interface-transport' 3 | import { logger } from '@libp2p/logger' 4 | import * as p from '@libp2p/peer-id' 5 | import { protocols } from '@multiformats/multiaddr' 6 | import * as multihashes from 'multihashes' 7 | import { concat } from 'uint8arrays/concat' 8 | import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' 9 | import { dataChannelError, inappropriateMultiaddr, unimplemented, invalidArgument } from '../error.js' 10 | import { WebRTCMultiaddrConnection } from '../maconn.js' 11 | import { DataChannelMuxerFactory } from '../muxer.js' 12 | import { createStream } from '../stream.js' 13 | import { isFirefox } from '../util.js' 14 | import * as sdp from './sdp.js' 15 | import { genUfrag } from './util.js' 16 | import type { WebRTCDialOptions } from './options.js' 17 | import type { DataChannelOpts } from '../stream.js' 18 | import type { Connection } from '@libp2p/interface-connection' 19 | import type { CounterGroup, Metrics } from '@libp2p/interface-metrics' 20 | import type { PeerId } from '@libp2p/interface-peer-id' 21 | import type { Multiaddr } from '@multiformats/multiaddr' 22 | 23 | const log = logger('libp2p:webrtc:transport') 24 | 25 | /** 26 | * The time to wait, in milliseconds, for the data channel handshake to complete 27 | */ 28 | const HANDSHAKE_TIMEOUT_MS = 10_000 29 | 30 | /** 31 | * Created by converting the hexadecimal protocol code to an integer. 32 | * 33 | * {@link https://github.com/multiformats/multiaddr/blob/master/protocols.csv} 34 | */ 35 | export const WEBRTC_CODE: number = protocols('webrtc-direct').code 36 | 37 | /** 38 | * Created by converting the hexadecimal protocol code to an integer. 39 | * 40 | * {@link https://github.com/multiformats/multiaddr/blob/master/protocols.csv} 41 | */ 42 | export const CERTHASH_CODE: number = protocols('certhash').code 43 | 44 | /** 45 | * The peer for this transport 46 | */ 47 | export interface WebRTCDirectTransportComponents { 48 | peerId: PeerId 49 | metrics?: Metrics 50 | } 51 | 52 | export interface WebRTCMetrics { 53 | dialerEvents: CounterGroup 54 | } 55 | 56 | export interface WebRTCTransportDirectInit { 57 | dataChannel?: Partial 58 | } 59 | 60 | export class WebRTCDirectTransport implements Transport { 61 | private readonly metrics?: WebRTCMetrics 62 | private readonly components: WebRTCDirectTransportComponents 63 | private readonly init: WebRTCTransportDirectInit 64 | constructor (components: WebRTCDirectTransportComponents, init: WebRTCTransportDirectInit = {}) { 65 | this.components = components 66 | this.init = init 67 | if (components.metrics != null) { 68 | this.metrics = { 69 | dialerEvents: components.metrics.registerCounterGroup('libp2p_webrtc_dialer_events_total', { 70 | label: 'event', 71 | help: 'Total count of WebRTC dial events by type' 72 | }) 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Dial a given multiaddr 79 | */ 80 | async dial (ma: Multiaddr, options: WebRTCDialOptions): Promise { 81 | const rawConn = await this._connect(ma, options) 82 | log(`dialing address - ${ma.toString()}`) 83 | return rawConn 84 | } 85 | 86 | /** 87 | * Create transport listeners no supported by browsers 88 | */ 89 | createListener (options: CreateListenerOptions): Listener { 90 | throw unimplemented('WebRTCTransport.createListener') 91 | } 92 | 93 | /** 94 | * Takes a list of `Multiaddr`s and returns only valid addresses for the transport 95 | */ 96 | filter (multiaddrs: Multiaddr[]): Multiaddr[] { 97 | return multiaddrs.filter(validMa) 98 | } 99 | 100 | /** 101 | * Implement toString() for WebRTCTransport 102 | */ 103 | readonly [Symbol.toStringTag] = '@libp2p/webrtc-direct' 104 | 105 | /** 106 | * Symbol.for('@libp2p/transport') 107 | */ 108 | readonly [symbol] = true 109 | 110 | /** 111 | * Connect to a peer using a multiaddr 112 | */ 113 | async _connect (ma: Multiaddr, options: WebRTCDialOptions): Promise { 114 | const controller = new AbortController() 115 | const signal = controller.signal 116 | 117 | const remotePeerString = ma.getPeerId() 118 | if (remotePeerString === null) { 119 | throw inappropriateMultiaddr("we need to have the remote's PeerId") 120 | } 121 | const theirPeerId = p.peerIdFromString(remotePeerString) 122 | 123 | const remoteCerthash = sdp.decodeCerthash(sdp.certhash(ma)) 124 | 125 | // ECDSA is preferred over RSA here. From our testing we find that P-256 elliptic 126 | // curve is supported by Pion, webrtc-rs, as well as Chromium (P-228 and P-384 127 | // was not supported in Chromium). We use the same hash function as found in the 128 | // multiaddr if it is supported. 129 | const certificate = await RTCPeerConnection.generateCertificate({ 130 | name: 'ECDSA', 131 | namedCurve: 'P-256', 132 | hash: sdp.toSupportedHashFunction(remoteCerthash.name) 133 | } as any) 134 | 135 | const peerConnection = new RTCPeerConnection({ certificates: [certificate] }) 136 | 137 | // create data channel for running the noise handshake. Once the data channel is opened, 138 | // the remote will initiate the noise handshake. This is used to confirm the identity of 139 | // the peer. 140 | const dataChannelOpenPromise = new Promise((resolve, reject) => { 141 | const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 }) 142 | const handshakeTimeout = setTimeout(() => { 143 | const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}` 144 | log.error(error) 145 | this.metrics?.dialerEvents.increment({ open_error: true }) 146 | reject(dataChannelError('data', error)) 147 | }, HANDSHAKE_TIMEOUT_MS) 148 | 149 | handshakeDataChannel.onopen = (_) => { 150 | clearTimeout(handshakeTimeout) 151 | resolve(handshakeDataChannel) 152 | } 153 | 154 | // ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event 155 | handshakeDataChannel.onerror = (event: Event) => { 156 | clearTimeout(handshakeTimeout) 157 | const errorTarget = event.target?.toString() ?? 'not specified' 158 | const error = `Error opening a data channel for handshaking: ${errorTarget}` 159 | log.error(error) 160 | // NOTE: We use unknown error here but this could potentially be considered a reset by some standards. 161 | this.metrics?.dialerEvents.increment({ unknown_error: true }) 162 | reject(dataChannelError('data', error)) 163 | } 164 | }) 165 | 166 | const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32) 167 | 168 | // Create offer and munge sdp with ufrag == pwd. This allows the remote to 169 | // respond to STUN messages without performing an actual SDP exchange. 170 | // This is because it can infer the passwd field by reading the USERNAME 171 | // attribute of the STUN message. 172 | const offerSdp = await peerConnection.createOffer() 173 | const mungedOfferSdp = sdp.munge(offerSdp, ufrag) 174 | await peerConnection.setLocalDescription(mungedOfferSdp) 175 | 176 | // construct answer sdp from multiaddr and ufrag 177 | const answerSdp = sdp.fromMultiAddr(ma, ufrag) 178 | await peerConnection.setRemoteDescription(answerSdp) 179 | 180 | // wait for peerconnection.onopen to fire, or for the datachannel to open 181 | const handshakeDataChannel = await dataChannelOpenPromise 182 | 183 | const myPeerId = this.components.peerId 184 | 185 | // Do noise handshake. 186 | // Set the Noise Prologue to libp2p-webrtc-noise: before starting the actual Noise handshake. 187 | // is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order. 188 | const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma) 189 | 190 | // Since we use the default crypto interface and do not use a static key or early data, 191 | // we pass in undefined for these parameters. 192 | const noise = Noise({ prologueBytes: fingerprintsPrologue })() 193 | 194 | const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel }) 195 | const wrappedDuplex = { 196 | ...wrappedChannel, 197 | sink: wrappedChannel.sink.bind(wrappedChannel), 198 | source: (async function * () { 199 | for await (const list of wrappedChannel.source) { 200 | for (const buf of list) { 201 | yield buf 202 | } 203 | } 204 | }()) 205 | } 206 | 207 | // Creating the connection before completion of the noise 208 | // handshake ensures that the stream opening callback is set up 209 | const maConn = new WebRTCMultiaddrConnection({ 210 | peerConnection, 211 | remoteAddr: ma, 212 | timeline: { 213 | open: Date.now() 214 | }, 215 | metrics: this.metrics?.dialerEvents 216 | }) 217 | 218 | const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange' 219 | 220 | peerConnection.addEventListener(eventListeningName, () => { 221 | switch (peerConnection.connectionState) { 222 | case 'failed': 223 | case 'disconnected': 224 | case 'closed': 225 | maConn.close().catch((err) => { 226 | log.error('error closing connection', err) 227 | }).finally(() => { 228 | // Remove the event listener once the connection is closed 229 | controller.abort() 230 | }) 231 | break 232 | default: 233 | break 234 | } 235 | }, { signal }) 236 | 237 | // Track opened peer connection 238 | this.metrics?.dialerEvents.increment({ peer_connection: true }) 239 | 240 | const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel }) 241 | 242 | // For outbound connections, the remote is expected to start the noise handshake. 243 | // Therefore, we need to secure an inbound noise connection from the remote. 244 | await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId) 245 | 246 | return options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory }) 247 | } 248 | 249 | /** 250 | * Generate a noise prologue from the peer connection's certificate. 251 | * noise prologue = bytes('libp2p-webrtc-noise:') + noise-responder fingerprint + noise-initiator fingerprint 252 | */ 253 | private generateNoisePrologue (pc: RTCPeerConnection, hashCode: multihashes.HashCode, ma: Multiaddr): Uint8Array { 254 | if (pc.getConfiguration().certificates?.length === 0) { 255 | throw invalidArgument('no local certificate') 256 | } 257 | 258 | const localFingerprint = sdp.getLocalFingerprint(pc) 259 | if (localFingerprint == null) { 260 | throw invalidArgument('no local fingerprint found') 261 | } 262 | 263 | const localFpString = localFingerprint.trim().toLowerCase().replaceAll(':', '') 264 | const localFpArray = uint8arrayFromString(localFpString, 'hex') 265 | const local = multihashes.encode(localFpArray, hashCode) 266 | const remote: Uint8Array = sdp.mbdecoder.decode(sdp.certhash(ma)) 267 | const prefix = uint8arrayFromString('libp2p-webrtc-noise:') 268 | 269 | return concat([prefix, local, remote]) 270 | } 271 | } 272 | 273 | /** 274 | * Determine if a given multiaddr contains a WebRTC Code (280), 275 | * a Certhash Code (466) and a PeerId 276 | */ 277 | function validMa (ma: Multiaddr): boolean { 278 | const codes = ma.protoCodes() 279 | return codes.includes(WEBRTC_CODE) && codes.includes(CERTHASH_CODE) && ma.getPeerId() != null && !codes.includes(protocols('p2p-circuit').code) 280 | } 281 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.11](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.10...v2.0.11) (2023-06-15) 2 | 3 | 4 | ### Trivial Changes 5 | 6 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([2cb3586](https://github.com/libp2p/js-libp2p-webrtc/commit/2cb3586fb0818235083554d8581694a65b00b31f)) 7 | * Update .github/workflows/stale.yml [skip ci] ([c80d4e9](https://github.com/libp2p/js-libp2p-webrtc/commit/c80d4e9cb2eae1ce721e0bcf8c78f79a0f49aea5)) 8 | 9 | 10 | ### Dependencies 11 | 12 | * bump p-event from 5.0.1 to 6.0.0 ([#182](https://github.com/libp2p/js-libp2p-webrtc/issues/182)) ([4df61fb](https://github.com/libp2p/js-libp2p-webrtc/commit/4df61fbdebb7ccd9c75408dc1d7fcc076a430433)) 13 | 14 | ## [2.0.10](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.9...v2.0.10) (2023-06-12) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * add browser-to-browser test for bi-directional communication ([#172](https://github.com/libp2p/js-libp2p-webrtc/issues/172)) ([1ec3d8a](https://github.com/libp2p/js-libp2p-webrtc/commit/1ec3d8a8b611d5227f430037e2547fd86d115eaa)) 20 | 21 | ## [2.0.9](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.8...v2.0.9) (2023-06-12) 22 | 23 | 24 | ### Dependencies 25 | 26 | * **dev:** bump delay from 5.0.0 to 6.0.0 ([#169](https://github.com/libp2p/js-libp2p-webrtc/issues/169)) ([104cbf0](https://github.com/libp2p/js-libp2p-webrtc/commit/104cbf0e2009961656cda530925089dc126b19a8)) 27 | 28 | ## [2.0.8](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.7...v2.0.8) (2023-06-12) 29 | 30 | 31 | ### Tests 32 | 33 | * add a test for large transfers ([#175](https://github.com/libp2p/js-libp2p-webrtc/issues/175)) ([0f60060](https://github.com/libp2p/js-libp2p-webrtc/commit/0f60060c9ceaf2bf2142df25f32174112edf6ec9)) 34 | 35 | ## [2.0.7](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.6...v2.0.7) (2023-06-07) 36 | 37 | 38 | ### Tests 39 | 40 | * actually run firefox tests on firefox ([#176](https://github.com/libp2p/js-libp2p-webrtc/issues/176)) ([386a607](https://github.com/libp2p/js-libp2p-webrtc/commit/386a6071923e6cb1d89c51b73dada306b7cc243f)) 41 | 42 | ## [2.0.6](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.5...v2.0.6) (2023-06-04) 43 | 44 | 45 | ### Documentation 46 | 47 | * update README.md example ([#178](https://github.com/libp2p/js-libp2p-webrtc/issues/178)) ([1264875](https://github.com/libp2p/js-libp2p-webrtc/commit/1264875ebd40b057e70aa47bebde45bfbe80facb)) 48 | 49 | ## [2.0.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.4...v2.0.5) (2023-06-01) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * Update splitAddr function to correctly parse multiaddrs ([#174](https://github.com/libp2p/js-libp2p-webrtc/issues/174)) ([22a7029](https://github.com/libp2p/js-libp2p-webrtc/commit/22a7029caab7601cfc1f1d1051bc218ebe4dfce0)) 55 | 56 | ## [2.0.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.3...v2.0.4) (2023-05-17) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * use abstract stream class from muxer interface module ([#165](https://github.com/libp2p/js-libp2p-webrtc/issues/165)) ([32f68de](https://github.com/libp2p/js-libp2p-webrtc/commit/32f68de455d2f0b136553aa41caf06adaf1f09d1)), closes [#164](https://github.com/libp2p/js-libp2p-webrtc/issues/164) 62 | 63 | ## [2.0.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.2...v2.0.3) (2023-05-17) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * restrict message sizes to 16kb ([#147](https://github.com/libp2p/js-libp2p-webrtc/issues/147)) ([aca4422](https://github.com/libp2p/js-libp2p-webrtc/commit/aca4422f5d4b81576d8c3cc5531cef7b7491abd2)), closes [#144](https://github.com/libp2p/js-libp2p-webrtc/issues/144) [#158](https://github.com/libp2p/js-libp2p-webrtc/issues/158) 69 | 70 | ## [2.0.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.1...v2.0.2) (2023-05-15) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * use transport manager getListeners to get listen addresses ([#166](https://github.com/libp2p/js-libp2p-webrtc/issues/166)) ([2e144f9](https://github.com/libp2p/js-libp2p-webrtc/commit/2e144f977a2025aa3adce1816d5f7d0dc3aaa477)) 76 | 77 | ## [2.0.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.0...v2.0.1) (2023-05-12) 78 | 79 | 80 | ### Bug Fixes 81 | 82 | * remove protobuf-ts and split code into two folders ([#162](https://github.com/libp2p/js-libp2p-webrtc/issues/162)) ([64723a7](https://github.com/libp2p/js-libp2p-webrtc/commit/64723a726302edcdc7ec958a759c3c587a184d69)) 83 | 84 | ## [2.0.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.2.0...v2.0.0) (2023-05-11) 85 | 86 | 87 | ### ⚠ BREAKING CHANGES 88 | 89 | * must be used with libp2p@0.45.x 90 | 91 | ### Dependencies 92 | 93 | * update all libp2p deps for compat with libp2p@0.45.x ([#160](https://github.com/libp2p/js-libp2p-webrtc/issues/160)) ([b20875d](https://github.com/libp2p/js-libp2p-webrtc/commit/b20875d9f73e5cad05376db2d1228363dd1bce7d)) 94 | 95 | ## [1.2.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.11...v1.2.0) (2023-05-09) 96 | 97 | 98 | ### Features 99 | 100 | * export metrics ([#71](https://github.com/libp2p/js-libp2p-webrtc/issues/71)) ([b3cb445](https://github.com/libp2p/js-libp2p-webrtc/commit/b3cb445e226d6d4ddba092cf961d6178d9a19ac1)) 101 | 102 | ## [1.1.11](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.10...v1.1.11) (2023-05-06) 103 | 104 | 105 | ### Dependencies 106 | 107 | * upgrade transport interface to 4.0.1 ([#150](https://github.com/libp2p/js-libp2p-webrtc/issues/150)) ([dc61fa2](https://github.com/libp2p/js-libp2p-webrtc/commit/dc61fa27a2f53568b1f3b320971de166b5b243f9)) 108 | 109 | ## [1.1.10](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.9...v1.1.10) (2023-05-03) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * Fetch local fingerprint from SDP ([#109](https://github.com/libp2p/js-libp2p-webrtc/issues/109)) ([3673d6c](https://github.com/libp2p/js-libp2p-webrtc/commit/3673d6c2637c21e488e684cdff4eedbb7f5b3692)) 115 | 116 | ## [1.1.9](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.8...v1.1.9) (2023-04-26) 117 | 118 | 119 | ### Documentation 120 | 121 | * update import in README example ([#141](https://github.com/libp2p/js-libp2p-webrtc/issues/141)) ([42275df](https://github.com/libp2p/js-libp2p-webrtc/commit/42275df0727cd729006cbf3fae300fc428c9ca51)) 122 | 123 | ## [1.1.8](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.7...v1.1.8) (2023-04-25) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * added peer connection state listener to emit closed events ([#134](https://github.com/libp2p/js-libp2p-webrtc/issues/134)) ([16e8503](https://github.com/libp2p/js-libp2p-webrtc/commit/16e85030e78ed9edb2ebecf81bac3ad33d622111)), closes [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) 129 | 130 | ## [1.1.7](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.6...v1.1.7) (2023-04-24) 131 | 132 | 133 | ### Dependencies 134 | 135 | * bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#135](https://github.com/libp2p/js-libp2p-webrtc/issues/135)) ([2fc8399](https://github.com/libp2p/js-libp2p-webrtc/commit/2fc839912a65c310ca7c8935d1901cc56849a21d)) 136 | 137 | ## [1.1.6](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.5...v1.1.6) (2023-04-21) 138 | 139 | 140 | ### Bug Fixes 141 | 142 | * readme: Remove confusing section ([#122](https://github.com/libp2p/js-libp2p-webrtc/issues/122)) ([dc78154](https://github.com/libp2p/js-libp2p-webrtc/commit/dc781543b8175c6c40c6745029a4ba53587aef29)) 143 | 144 | ## [1.1.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.4...v1.1.5) (2023-04-13) 145 | 146 | 147 | ### Dependencies 148 | 149 | * bump it-pipe from 2.0.5 to 3.0.1 ([#111](https://github.com/libp2p/js-libp2p-webrtc/issues/111)) ([7e593a3](https://github.com/libp2p/js-libp2p-webrtc/commit/7e593a34b44b7a2cf4758df2218b3ba9ebacfce9)) 150 | * bump protons-runtime from 4.0.2 to 5.0.0 ([#117](https://github.com/libp2p/js-libp2p-webrtc/issues/117)) ([87cbb19](https://github.com/libp2p/js-libp2p-webrtc/commit/87cbb193e2a45642333498d9317ab17eb527d34d)) 151 | * **dev:** bump protons from 6.1.3 to 7.0.2 ([#119](https://github.com/libp2p/js-libp2p-webrtc/issues/119)) ([fd20f4f](https://github.com/libp2p/js-libp2p-webrtc/commit/fd20f4f7a182a8edca5a511fe747885d24a60652)) 152 | 153 | ## [1.1.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.3...v1.1.4) (2023-04-13) 154 | 155 | 156 | ### Dependencies 157 | 158 | * Update multiaddr to 12.1.1 and multiformats 11.0.2 ([#123](https://github.com/libp2p/js-libp2p-webrtc/issues/123)) ([e069784](https://github.com/libp2p/js-libp2p-webrtc/commit/e069784229f2495b3cebc2c2a85969f23f0e7acf)) 159 | 160 | ## [1.1.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.2...v1.1.3) (2023-04-12) 161 | 162 | 163 | ### Dependencies 164 | 165 | * bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#124](https://github.com/libp2p/js-libp2p-webrtc/issues/124)) ([4146761](https://github.com/libp2p/js-libp2p-webrtc/commit/4146761226118268d510c8834f894083ba5408d3)) 166 | 167 | ## [1.1.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.1...v1.1.2) (2023-04-11) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * update multiaddr in webrtc connection to include webRTC ([#121](https://github.com/libp2p/js-libp2p-webrtc/issues/121)) ([6ea04db](https://github.com/libp2p/js-libp2p-webrtc/commit/6ea04db9800259963affcb3101ea542de79271c0)) 173 | 174 | ## [1.1.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.0...v1.1.1) (2023-04-10) 175 | 176 | 177 | ### Dependencies 178 | 179 | * bump it-pb-stream from 2.0.4 to 3.2.1 ([#118](https://github.com/libp2p/js-libp2p-webrtc/issues/118)) ([7e2ac67](https://github.com/libp2p/js-libp2p-webrtc/commit/7e2ac6795ea096b3cf5dc2c4077f6f39821e0502)) 180 | 181 | ## [1.1.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.5...v1.1.0) (2023-04-07) 182 | 183 | 184 | ### Features 185 | 186 | * Browser to Browser ([#90](https://github.com/libp2p/js-libp2p-webrtc/issues/90)) ([add5c46](https://github.com/libp2p/js-libp2p-webrtc/commit/add5c467a2d02058933e6e11751af0c850568eaf)) 187 | 188 | ## [1.0.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.4...v1.0.5) (2023-03-30) 189 | 190 | 191 | ### Bug Fixes 192 | 193 | * correction package.json exports types path ([#103](https://github.com/libp2p/js-libp2p-webrtc/issues/103)) ([c78851f](https://github.com/libp2p/js-libp2p-webrtc/commit/c78851fe71f6a6ca79a146a7022e818378ea6721)) 194 | 195 | 196 | ### Trivial Changes 197 | 198 | * replace err-code with CodeError ([#82](https://github.com/libp2p/js-libp2p-webrtc/issues/82)) ([cfa6494](https://github.com/libp2p/js-libp2p-webrtc/commit/cfa6494c43c4edb977e70abe81a260bf0e03de73)) 199 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([f0ae5e7](https://github.com/libp2p/js-libp2p-webrtc/commit/f0ae5e78a0469bd1129d7b242e4fb41f0b2ed49e)) 200 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([4c8806c](https://github.com/libp2p/js-libp2p-webrtc/commit/4c8806c6d2a1a8eff48f0e2248203d48bd84c065)) 201 | 202 | ## [1.0.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.3...v1.0.4) (2023-02-22) 203 | 204 | 205 | ### Dependencies 206 | 207 | * **dev:** bump aegir from 37.12.1 to 38.1.6 ([#94](https://github.com/libp2p/js-libp2p-webrtc/issues/94)) ([2ee8a5e](https://github.com/libp2p/js-libp2p-webrtc/commit/2ee8a5e4bb03377214ff3c12744c2e153a3f69b4)) 208 | 209 | 210 | ### Trivial Changes 211 | 212 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([7e0b1c0](https://github.com/libp2p/js-libp2p-webrtc/commit/7e0b1c00b28cae7249a506f06f18bf3537bf3476)) 213 | 214 | ## [1.0.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.2...v1.0.3) (2023-01-30) 215 | 216 | 217 | ### Tests 218 | 219 | * add stream transition test ([#72](https://github.com/libp2p/js-libp2p-webrtc/issues/72)) ([27ec3da](https://github.com/libp2p/js-libp2p-webrtc/commit/27ec3da4ef66cf07c1452c6f987cb55d313c1a03)) 220 | 221 | ## [1.0.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.1...v1.0.2) (2023-01-04) 222 | 223 | 224 | ### Dependencies 225 | 226 | * bump multiformats from 10.0.3 to 11.0.0 ([#70](https://github.com/libp2p/js-libp2p-webrtc/issues/70)) ([7dafe5a](https://github.com/libp2p/js-libp2p-webrtc/commit/7dafe5a126ca0ce2b6d887f6a84fabe55e36229d)) 227 | 228 | ## [1.0.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.0...v1.0.1) (2023-01-03) 229 | 230 | 231 | ### Bug Fixes 232 | 233 | * remove uuid dependency ([#68](https://github.com/libp2p/js-libp2p-webrtc/issues/68)) ([fb14b88](https://github.com/libp2p/js-libp2p-webrtc/commit/fb14b880d1b1b278e1e826bb0d9939db358e6ccc)) 234 | 235 | ## 1.0.0 (2022-12-13) 236 | 237 | 238 | ### Bug Fixes 239 | 240 | * update project config ([#65](https://github.com/libp2p/js-libp2p-webrtc/issues/65)) ([09c33cc](https://github.com/libp2p/js-libp2p-webrtc/commit/09c33ccfff97059eab001e46a662467dea670ce1)) 241 | 242 | 243 | ### Dependencies 244 | 245 | * update libp2p to release version ([dbd0237](https://github.com/libp2p/js-libp2p-webrtc/commit/dbd0237e9f8500ac13948e3a35d912df257968a4)) 246 | 247 | 248 | ### Trivial Changes 249 | 250 | * Update .github/workflows/stale.yml [skip ci] ([43c70bc](https://github.com/libp2p/js-libp2p-webrtc/commit/43c70bcd3c63388ed44d76703ce9a32e51d9ef30)) 251 | 252 | 253 | ### Documentation 254 | 255 | * fix 'browser to server' build config ([#66](https://github.com/libp2p/js-libp2p-webrtc/issues/66)) ([b54132c](https://github.com/libp2p/js-libp2p-webrtc/commit/b54132cecac180f0577a1b7905f79b20207c3647)) 256 | --------------------------------------------------------------------------------