diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index e6e2407..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,44 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - plugins: [ - '@typescript-eslint', - 'eslint-plugin-prettier', - 'autofix', - 'import', - 'compat', - 'prettier', - 'unused-imports', - 'react-perf', - ], - ignorePatterns: ['**/proto_ts/**/*'], - rules: { - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-var-requires': 0, - '@typescript-eslint/ban-ts-comment': 0, - '@typescript-eslint/no-empty-interface': 0, - '@typescript-eslint/ban-types': 0, - '@typescript-eslint/no-explicit-any': 0, - '@typescript-eslint/no-non-null-assertion': 0, - 'prettier/prettier': 'error', - 'import/order': 'error', - 'function-paren-newline': ['error', 'consistent'], - 'array-callback-return': 0, - '@typescript-eslint/no-unused-vars': 1, - 'function-paren-newline': 0, - 'unused-imports/no-unused-imports-ts': 2, - camelcase: 0, - 'react-hooks/exhaustive-deps': 1, - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': ['error'], - }, - extends: [ - 'react-app', - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier', - 'plugin:import/errors', - 'plugin:import/warnings', - 'plugin:import/typescript', - 'plugin:markdown/recommended', - ], -}; diff --git a/.github/workflows/js-test-and-release.yml b/.github/workflows/js-test-and-release.yml index 08c8996..77edead 100644 --- a/.github/workflows/js-test-and-release.yml +++ b/.github/workflows/js-test-and-release.yml @@ -95,7 +95,7 @@ jobs: flags: electron-main release: - needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-electron-main, test-electron-renderer] + needs: [test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-electron-main] runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/master' # with #262 - 'refs/heads/${{{ github.default_branch }}}' steps: diff --git a/package.json b/package.json index 77a799e..33c5963 100644 --- a/package.json +++ b/package.json @@ -78,10 +78,14 @@ }, "dependencies": { "@chainsafe/libp2p-noise": "^10.0.0", + "@libp2p/components": "^3.0.2", + "@libp2p/interfaces": "^3.0.2", "@libp2p/interface-connection": "^3.0.2", + "@libp2p/interface-peer-id": "^1.0.5", "@libp2p/interface-stream-muxer": "^3.0.0", "@libp2p/interface-transport": "^2.0.0", "@libp2p/logger": "^2.0.0", + "@libp2p/multistream-select": "^3.0.2", "@libp2p/peer-id": "^1.1.15", "@multiformats/multiaddr": "^11.0.3", "@protobuf-ts/runtime": "^2.8.0", @@ -94,6 +98,7 @@ "multiformats": "^10.0.0", "multihashes": "^4.0.3", "p-defer": "^4.0.0", + "timeout-abort-controller": "^3.0.0", "uint8arraylist": "^2.3.3", "uint8arrays": "^4.0.2", "uuid": "^9.0.0" diff --git a/src/error.ts b/src/error.ts index 1d7f8d1..98ff1e8 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,111 +1,123 @@ -import { default as createError } from 'err-code'; -import { Direction } from '@libp2p/interface-connection'; +import errCode from 'err-code' +import { Direction } from '@libp2p/interface-connection' export class WebRTCTransportError extends Error { - constructor(msg: string) { - super('WebRTC transport error: ' + msg); - this.name = 'WebRTCTransportError'; + constructor (msg: string) { + super('WebRTC transport error: ' + msg) + this.name = 'WebRTCTransportError' } } export enum codes { ERR_ALREADY_ABORTED = 'ERR_ALREADY_ABORTED', ERR_DATA_CHANNEL = 'ERR_DATA_CHANNEL', + ERR_CONNECTION_CLOSED = 'ERR_CONNECTION_CLOSED', + ERR_HASH_NOT_SUPPORTED = 'ERR_HASH_NOT_SUPPORTED', ERR_INVALID_MULTIADDR = 'ERR_INVALID_MULTIADDR', + ERR_INVALID_FINGERPRINT = 'ERR_INVALID_FINGERPRINT', ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS', - ERR_HASH_NOT_SUPPORTED = 'ERR_HASH_NOT_SUPPORTED', ERR_NOT_IMPLEMENTED = 'ERR_NOT_IMPLEMENTED', ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS', ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS', - ERR_CONNECTION_CLOSED = 'ERR_CONNECTION_CLOSED', } export class ConnectionClosedError extends WebRTCTransportError { - constructor(state: RTCPeerConnectionState, msg: string) { - super(`peerconnection moved to state: ${state}:` + msg); - this.name = 'WebRTC/ConnectionClosed'; + constructor (state: RTCPeerConnectionState, msg: string) { + super(`peerconnection moved to state: ${state}:` + msg) + this.name = 'WebRTC/ConnectionClosed' } } -export function connectionClosedError(state: RTCPeerConnectionState, msg: string) { - return createError(new ConnectionClosedError(state, msg), codes.ERR_CONNECTION_CLOSED) +export function connectionClosedError (state: RTCPeerConnectionState, msg: string) { + return errCode(new ConnectionClosedError(state, msg), codes.ERR_CONNECTION_CLOSED) } export class InvalidArgumentError extends WebRTCTransportError { - constructor(msg: string) { - super('There was a problem with a provided argument: ' + msg); - this.name = 'WebRTC/InvalidArgumentError'; + constructor (msg: string) { + super('There was a problem with a provided argument: ' + msg) + this.name = 'WebRTC/InvalidArgumentError' } } -export function unsupportedHashAlgorithm(algorithm: string) { - return createError(new UnsupportedHashAlgorithmError(algorithm), codes.ERR_HASH_NOT_SUPPORTED); +export function unsupportedHashAlgorithm (algorithm: string) { + return errCode(new UnsupportedHashAlgorithmError(algorithm), codes.ERR_HASH_NOT_SUPPORTED) } export class UnsupportedHashAlgorithmError extends WebRTCTransportError { - constructor(algo: string) { - let msg = `unsupported hash algorithm: ${algo}`; - super(msg); - this.name = 'WebRTC/UnsupportedHashAlgorithmError'; + constructor (algo: string) { + const msg = `unsupported hash algorithm: ${algo}` + super(msg) + this.name = 'WebRTC/UnsupportedHashAlgorithmError' } } -export function invalidArgument(msg: string) { - return createError(new InvalidArgumentError(msg), codes.ERR_INVALID_PARAMETERS); +export function invalidArgument (msg: string) { + return errCode(new InvalidArgumentError(msg), codes.ERR_INVALID_PARAMETERS) } export class UnimplementedError extends WebRTCTransportError { - constructor(methodName: string) { - super('A method (' + methodName + ') was called though it has been intentionally left unimplemented.'); - this.name = 'WebRTC/UnimplementedError'; + constructor (methodName: string) { + super('A method (' + methodName + ') was called though it has been intentionally left unimplemented.') + this.name = 'WebRTC/UnimplementedError' + } +} + +export function unimplemented (methodName: string) { + return errCode(new UnimplementedError(methodName), codes.ERR_NOT_IMPLEMENTED) +} + +export class InvalidFingerprintError extends WebRTCTransportError { + constructor (fingerprint: string, source: string) { + super(`Invalid fingerprint "${fingerprint}" within ${source}`) + this.name = 'WebRTC/InvalidFingerprintError' } } -export function unimplemented(methodName: string) { - return createError(new UnimplementedError(methodName), codes.ERR_NOT_IMPLEMENTED); +export function invalidFingerprint (fingerprint: string, source: string) { + return errCode(new InvalidFingerprintError(fingerprint, source), codes.ERR_INVALID_FINGERPRINT) } export class InappropriateMultiaddrError extends WebRTCTransportError { - constructor(msg: string) { - super('There was a problem with the Multiaddr which was passed in: ' + msg); - this.name = 'WebRTC/InappropriateMultiaddrError'; + constructor (msg: string) { + super('There was a problem with the Multiaddr which was passed in: ' + msg) + this.name = 'WebRTC/InappropriateMultiaddrError' } } -export function inappropriateMultiaddr(msg: string) { - return createError(new InappropriateMultiaddrError(msg), codes.ERR_INVALID_MULTIADDR); +export function inappropriateMultiaddr (msg: string) { + return errCode(new InappropriateMultiaddrError(msg), codes.ERR_INVALID_MULTIADDR) } export class OperationAbortedError extends WebRTCTransportError { - constructor(context: string, abortReason: string) { - super(`Signalled to abort because (${abortReason}})${context}`); - this.name = 'WebRTC/OperationAbortedError'; + constructor (context: string, abortReason: string) { + super(`Signalled to abort because (${abortReason}})${context}`) + this.name = 'WebRTC/OperationAbortedError' } } -export function operationAborted(context: string, reason: string) { - return createError(new OperationAbortedError(context, reason), codes.ERR_ALREADY_ABORTED); +export function operationAborted (context: string, reason: string) { + return errCode(new OperationAbortedError(context, reason), codes.ERR_ALREADY_ABORTED) } export class DataChannelError extends WebRTCTransportError { - constructor(streamLabel: string, errorMessage: string) { - super(`[stream: ${streamLabel}] data channel error: ${errorMessage}`); - this.name = 'WebRTC/DataChannelError'; + constructor (streamLabel: string, errorMessage: string) { + super(`[stream: ${streamLabel}] data channel error: ${errorMessage}`) + this.name = 'WebRTC/DataChannelError' } } -export function dataChannelError(streamLabel: string, msg: string) { - return createError(new DataChannelError(streamLabel, msg), codes.ERR_DATA_CHANNEL); +export function dataChannelError (streamLabel: string, msg: string) { + return errCode(new DataChannelError(streamLabel, msg), codes.ERR_DATA_CHANNEL) } export class StreamingLimitationError extends WebRTCTransportError { - constructor(msg: string) { - super(msg); - this.name = 'WebRTC/StreamingLimitationError'; + constructor (msg: string) { + super(msg) + this.name = 'WebRTC/StreamingLimitationError' } } -export function overStreamLimit(dir: Direction, proto: string) { - let code = dir == 'inbound' ? codes.ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS : codes.ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS; - return createError(new StreamingLimitationError(`${dir} stream limit reached for protocol - ${proto}`), code); +export function overStreamLimit (dir: Direction, proto: string) { + const code = dir === 'inbound' ? codes.ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS : codes.ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS + return errCode(new StreamingLimitationError(`${dir} stream limit reached for protocol - ${proto}`), code) } diff --git a/src/maconn.ts b/src/maconn.ts index 71a1f7f..7182372 100644 --- a/src/maconn.ts +++ b/src/maconn.ts @@ -1,33 +1,34 @@ -import {MultiaddrConnection, MultiaddrConnectionTimeline} from "@libp2p/interface-connection"; -import { logger } from '@libp2p/logger'; -import {Multiaddr} from "@multiformats/multiaddr"; -import {Source, Sink} from "it-stream-types"; -import {nopSink, nopSource} from "./util.js"; +import { MultiaddrConnection, MultiaddrConnectionTimeline } from '@libp2p/interface-connection' +import { logger } from '@libp2p/logger' +import { Multiaddr } from '@multiformats/multiaddr' +import { Source, Sink } from 'it-stream-types' -const log = logger('libp2p:webrtc:connection'); +import { nopSink, nopSource } from './util.js' -type WebRTCMultiaddrConnectionInit = { - peerConnection: RTCPeerConnection; - remoteAddr: Multiaddr; - timeline: MultiaddrConnectionTimeline; -}; +const log = logger('libp2p:webrtc:connection') + +interface WebRTCMultiaddrConnectionInit { + peerConnection: RTCPeerConnection + remoteAddr: Multiaddr + timeline: MultiaddrConnectionTimeline +} export class WebRTCMultiaddrConnection implements MultiaddrConnection { - private peerConnection: RTCPeerConnection; + private readonly peerConnection: RTCPeerConnection; remoteAddr: Multiaddr; timeline: MultiaddrConnectionTimeline; source: Source = nopSource sink: Sink> = nopSink; - constructor(init: WebRTCMultiaddrConnectionInit) { - this.remoteAddr = init.remoteAddr; - this.timeline = init.timeline; - this.peerConnection = init.peerConnection; + constructor (init: WebRTCMultiaddrConnectionInit) { + this.remoteAddr = init.remoteAddr + this.timeline = init.timeline + this.peerConnection = init.peerConnection } - async close(err?: Error | undefined): Promise { - log.error("error closing connection", err) + async close (err?: Error | undefined): Promise { + log.error('error closing connection', err) this.peerConnection.close() } } diff --git a/src/message.proto b/src/message.proto index 7a40990..cbda09f 100644 --- a/src/message.proto +++ b/src/message.proto @@ -6,9 +6,11 @@ message Message { enum Flag { // The sender will no longer send messages on the stream. FIN = 0; + // The sender will no longer read messages on the stream. Incoming data is // being discarded on receipt. STOP_SENDING = 1; + // The sender abruptly terminates the sending part of the stream. The // receiver can discard any data that it already received on that stream. RESET = 2; diff --git a/src/muxer.ts b/src/muxer.ts index 9b42613..668148b 100644 --- a/src/muxer.ts +++ b/src/muxer.ts @@ -1,26 +1,27 @@ // import {Components} from "@libp2p/components" -import {Stream} from "@libp2p/interface-connection" -import {StreamMuxer, StreamMuxerFactory, StreamMuxerInit} from "@libp2p/interface-stream-muxer" -import {Source, Sink} from "it-stream-types" -import {WebRTCStream} from "./stream.js" -import {nopSink, nopSource} from "./util.js" +import { Stream } from '@libp2p/interface-connection' +import { StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface-stream-muxer' +import { Source, Sink } from 'it-stream-types' + +import { WebRTCStream } from './stream.js' +import { nopSink, nopSource } from './util.js' export class DataChannelMuxerFactory implements StreamMuxerFactory { - private peerConnection: RTCPeerConnection + private readonly peerConnection: RTCPeerConnection protocol: string = '/webrtc' - constructor(peerConnection: RTCPeerConnection) { + constructor (peerConnection: RTCPeerConnection) { this.peerConnection = peerConnection } - createStreamMuxer(init?: StreamMuxerInit | undefined): StreamMuxer { + createStreamMuxer (init?: StreamMuxerInit | undefined): StreamMuxer { return new DataChannelMuxer(this.peerConnection, init) } } export class DataChannelMuxer implements StreamMuxer { private readonly peerConnection: RTCPeerConnection - readonly protocol: string = "/webrtc" + readonly protocol: string = '/webrtc' streams: Stream[] = [] init?: StreamMuxerInit close: (err?: Error | undefined) => void = () => {} @@ -30,37 +31,35 @@ export class DataChannelMuxer implements StreamMuxer { source: Source = nopSource; sink: Sink> = nopSink; - - constructor(peerConnection: RTCPeerConnection, init?: StreamMuxerInit) { + constructor (peerConnection: RTCPeerConnection, init?: StreamMuxerInit) { this.init = init this.peerConnection = peerConnection - this.peerConnection.ondatachannel = ({channel}) => { + this.peerConnection.ondatachannel = ({ channel }) => { const stream = new WebRTCStream({ channel, stat: { direction: 'inbound', timeline: { - open: 0, + open: 0 } }, closeCb: init?.onStreamEnd }) - if (init?.onIncomingStream) { - init.onIncomingStream!(stream) + if ((init?.onIncomingStream) != null) { + init.onIncomingStream(stream) } } } - newStream(name?: string | undefined): Stream { - const streamName = name || ''; - const channel = this.peerConnection.createDataChannel(streamName) + newStream (name: string = ''): Stream { + const channel = this.peerConnection.createDataChannel(name) const stream = new WebRTCStream({ channel, stat: { direction: 'outbound', timeline: { - open: 0, - }, + open: 0 + } }, closeCb: this.init?.onStreamEnd }) diff --git a/src/options.ts b/src/options.ts index 7613aea..b5a7e55 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,5 +1,4 @@ -import { CreateListenerOptions } from '@libp2p/interface-transport'; -import { DialOptions } from '@libp2p/interface-transport'; +import { CreateListenerOptions, DialOptions } from '@libp2p/interface-transport' export interface WebRTCListenerOptions extends CreateListenerOptions { //, WebRTCInitiatorInit { diff --git a/src/sdp.ts b/src/sdp.ts index 49890dc..4c62c95 100644 --- a/src/sdp.ts +++ b/src/sdp.ts @@ -1,87 +1,94 @@ -import {inappropriateMultiaddr, invalidArgument, unsupportedHashAlgorithm} from './error.js'; -import {logger} from '@libp2p/logger'; -import {Multiaddr} from '@multiformats/multiaddr'; -import * as multihashes from 'multihashes'; -import {bases} from 'multiformats/basics'; +import { logger } from '@libp2p/logger' +import { Multiaddr } from '@multiformats/multiaddr' +import { bases } from 'multiformats/basics' +import * as multihashes from 'multihashes' -const log = logger('libp2p:webrtc:sdp'); +import { inappropriateMultiaddr, invalidArgument, invalidFingerprint, unsupportedHashAlgorithm } from './error.js' -export const mbdecoder: any = (function () { - const decoders = Object.values(bases).map((b) => b.decoder); - let acc = decoders[0].or(decoders[1]); - decoders.slice(2).forEach((d) => (acc = acc.or(d))); - return acc; -})(); +const log = logger('libp2p:webrtc:sdp') +const CERTHASH_CODE: number = 466 -const CERTHASH_CODE: number = 466; +// Get base2 | identity decoders +export const mbdecoder: any = (function () { + const decoders = Object.values(bases).map((b) => b.decoder) + let acc = decoders[0].or(decoders[1]) + decoders.slice(2).forEach((d) => (acc = acc.or(d))) + return acc +})() -function ipv(ma: Multiaddr): string { +// Extract the ipv from a multiaddr +function ipv (ma: Multiaddr): string { for (const proto of ma.protoNames()) { if (proto.startsWith('ip')) { - return proto.toUpperCase(); + return proto.toUpperCase() } } - log('Warning: multiaddr does not appear to contain IP4 or IP6.', ma); - return 'IP6'; -} -function ip(ma: Multiaddr): string { - return ma.toOptions().host; -} -function port(ma: Multiaddr): number { - return ma.toOptions().port; + + log('Warning: multiaddr does not appear to contain IP4 or IP6.', ma) + + return 'IP6' } -export function certhash(ma: Multiaddr): string { - const tups = ma.stringTuples(); - const certhash_value = tups.filter((tup) => tup[0] == CERTHASH_CODE).map((tup) => tup[1])[0]; - if (certhash_value) { - return certhash_value; - } else { - throw inappropriateMultiaddr("Couldn't find a certhash component of multiaddr:" + ma.toString()); +// Extract the certhash from a multiaddr +export function certhash (ma: Multiaddr): string { + const tups = ma.stringTuples() + const certhash = tups.filter((tup) => tup[0] === CERTHASH_CODE).map((tup) => tup[1])[0] + + if (certhash === undefined || certhash === '') { + throw inappropriateMultiaddr(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`) } + + return certhash } -export function decodeCerthash(certhash: string) { - const mbdecoded = mbdecoder.decode(certhash); - return multihashes.decode(mbdecoded); +// Convert a certhash into a multihash +export function decodeCerthash (certhash: string) { + const mbdecoded = mbdecoder.decode(certhash) + return multihashes.decode(mbdecoded) } -export function certhashToFingerprint(ma: Multiaddr): string[] { +// Extract the fingerprint from a multiaddr +export function ma2Fingerprint (ma: Multiaddr): string[] { // certhash_value is a multibase encoded multihash encoded string - const mhdecoded = decodeCerthash(certhash(ma)); - let prefix = toSupportedHashFunction(mhdecoded.name); + const mhdecoded = decodeCerthash(certhash(ma)) + const prefix = toSupportedHashFunction(mhdecoded.name) + const fp = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '') + const sdp = fp.match(/.{1,2}/g) - const fp = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); - const fpSdp = fp.match(/.{1,2}/g)!.join(':'); + if (sdp == null) { + throw invalidFingerprint(fp, ma.toString()) + } - return [`${prefix.toUpperCase()} ${fpSdp.toUpperCase()}`, fp]; + return [`${prefix.toUpperCase()} ${sdp.join(':').toUpperCase()}`, fp] } -export function toSupportedHashFunction(name: multihashes.HashName): string { +// Normalize the hash name from a given multihash has name +export function toSupportedHashFunction (name: multihashes.HashName): string { switch (name) { case 'sha1': return 'sha-1' case 'sha2-256': - return 'sha-256'; + return 'sha-256' case 'sha2-512': - return 'sha-512'; + return 'sha-512' default: - throw unsupportedHashAlgorithm(name); + throw unsupportedHashAlgorithm(name) } } -function ma2sdp(ma: Multiaddr, ufrag: string): string { - const IP = ip(ma); - const IPVERSION = ipv(ma); - const PORT = port(ma); - const [CERTFP, _] = certhashToFingerprint(ma); +// Convert a multiaddr into a SDP +function ma2sdp (ma: Multiaddr, ufrag: string): string { + const { host, port } = ma.toOptions() + const ipVersion = ipv(ma) + const [CERTFP] = ma2Fingerprint(ma) + return `v=0 -o=- 0 0 IN ${IPVERSION} ${IP} +o=- 0 0 IN ${ipVersion} ${host} s=- -c=IN ${IPVERSION} ${IP} +c=IN ${ipVersion} ${host} t=0 0 a=ice-lite -m=application ${PORT} UDP/DTLS/SCTP webrtc-datachannel +m=application ${port} UDP/DTLS/SCTP webrtc-datachannel a=mid:0 a=setup:passive a=ice-ufrag:${ufrag} @@ -89,21 +96,25 @@ a=ice-pwd:${ufrag} a=fingerprint:${CERTFP} a=sctp-port:5000 a=max-message-size:100000 -a=candidate:1467250027 1 UDP 1467250027 ${IP} ${PORT} typ host\r\n`; +a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host\r\n` } -export function fromMultiAddr(ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit { +// Create an answer SDP from a multiaddr +export function fromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit { return { type: 'answer', - sdp: ma2sdp(ma, ufrag), - }; + sdp: ma2sdp(ma, ufrag) + } } -export function munge(desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit { - if (desc.sdp) { - desc.sdp = desc.sdp.replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n').replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n'); - return desc; - } else { - throw invalidArgument("Can't munge a missing SDP"); +// Replace the ufrag and password values in a SDP +export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit { + if (desc.sdp === undefined) { + throw invalidArgument("Can't munge a missing SDP") } + + desc.sdp = desc.sdp + .replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n') + .replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n') + return desc } diff --git a/src/stream.ts b/src/stream.ts index 6e10db9..42d4e95 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -1,275 +1,41 @@ -import {Stream, StreamStat, Direction} from '@libp2p/interface-connection'; -import {Source} from 'it-stream-types'; -import {Sink} from 'it-stream-types'; -import {pushable} from 'it-pushable'; -import * as lengthPrefixed from 'it-length-prefixed'; -import {pipe} from 'it-pipe'; -import defer, {DeferredPromise} from 'p-defer'; -import merge from 'it-merge'; -import {Uint8ArrayList} from 'uint8arraylist'; -import {logger} from '@libp2p/logger'; -import * as pb from '../proto_ts/message.js'; - -const log = logger('libp2p:webrtc:stream'); - -export function defaultStat(dir: Direction): StreamStat { +import { Stream, StreamStat, Direction } from '@libp2p/interface-connection' +import { logger } from '@libp2p/logger' +import * as lengthPrefixed from 'it-length-prefixed' +import merge from 'it-merge' +import { pipe } from 'it-pipe' +import { pushable } from 'it-pushable' +import defer, { DeferredPromise } from 'p-defer' +import { Source, Sink } from 'it-stream-types' +import { Uint8ArrayList } from 'uint8arraylist' + +import * as pb from '../proto_ts/message.js' + +const log = logger('libp2p:webrtc:stream') + +export function defaultStat (dir: Direction): StreamStat { return { direction: dir, timeline: { open: 0, - close: undefined, - }, - }; -} - -type StreamInitOpts = { - channel: RTCDataChannel; - metadata?: Record; - stat: StreamStat; - closeCb?: (stream: WebRTCStream) => void; -}; - -export class WebRTCStream implements Stream { - /** - * Unique identifier for a stream - */ - id: string; - - /** - * Stats about this stream - */ - stat: StreamStat; - - /** - * User defined stream metadata - */ - metadata: Record; - - private readonly channel: RTCDataChannel; - streamState = new StreamState(); - - // _src is exposed to the user via the `source` getter to read unwrapped protobuf - // data from the underlying datachannel. - private readonly _src: Source; - - // _innersrc is used to push data from the underlying datachannel to the - // length prefix decoder and then the protobuf decoder. - private readonly _innersrc = pushable(); - - // sink is used to write data to the remote. It takes care of wrapping - // data in a protobuf and adding the length prefix. - sink: Sink>; - - // promises - // opened is resolved when the underlying datachannel is in the open state. - opened: DeferredPromise = defer(); - // closeWritePromise is used to trigger a generator which can be used to close - // the sink. - closeWritePromise: DeferredPromise = defer(); - closeCb?: (stream: WebRTCStream) => void | undefined - - constructor(opts: StreamInitOpts) { - this.channel = opts.channel; - this.id = this.channel.label; - - this.stat = opts.stat; - switch (this.channel.readyState) { - case 'open': - this.opened.resolve(); - break; - case 'closed': - case 'closing': - this.streamState.state = StreamStates.CLOSED; - if (!this.stat.timeline.close) { - this.stat.timeline.close = new Date().getTime(); - } - this.opened.resolve(); - break; - } - - this.metadata = opts.metadata ?? {}; - - // closable sink - this.sink = this._sinkFn; - - // handle RTCDataChannel events - this.channel.onopen = (_evt) => { - this.stat.timeline.open = new Date().getTime(); - this.opened.resolve(); - }; - - this.channel.onclose = (_evt) => { - this.close(); - }; - - this.channel.onerror = (evt) => { - let err = (evt as RTCErrorEvent).error; - this.abort(err); - }; - - const self = this; - - // reader pipe - this.channel.onmessage = async ({data}) => { - if (data.length == 0 || !data) { - return; - } - this._innersrc.push(new Uint8Array(data as ArrayBufferLike)) - }; - - // pipe framed protobuf messages through - // a length prefixed decoder, and surface - // data from the `Message.message` field - // through a source. - this._src = pipe( - this._innersrc, - lengthPrefixed.decode(), - (source) => (async function* () { - for await (const buf of source) { - const message = self.processIncomingProtobuf(buf.subarray()); - if (message) { - yield new Uint8ArrayList(message); - } - } - })(), - ) - - } - - // If user attempts to set a new source - // this should be a nop - set source(_src: Source) { - } - - get source(): Source { - return this._src - } - - private async _sinkFn(src: Source): Promise { - await this.opened.promise; - if (this.streamState.state == StreamStates.CLOSED || this.streamState.state == StreamStates.WRITE_CLOSED) { - return; - } - - const self = this; - const closeWriteIterable = { - async *[Symbol.asyncIterator]() { - await self.closeWritePromise.promise; - yield new Uint8Array(0); - }, - }; - - for await (const buf of merge(closeWriteIterable, src)) { - const state = self.streamState.state; - if (state == StreamStates.CLOSED || state == StreamStates.WRITE_CLOSED) { - return; - } - const msgbuf = pb.Message.toBinary({message: buf.subarray()}); - const sendbuf = lengthPrefixed.encode.single(msgbuf) - this.channel.send(sendbuf.subarray()) - } - } - - processIncomingProtobuf(buffer: Uint8Array): Uint8Array | undefined { - const m = pb.Message.fromBinary(buffer); - if (m.flag) { - const [currentState, nextState] = this.streamState.transition({direction: 'inbound', flag: m.flag!}); - if (currentState != nextState) { - switch (nextState) { - case StreamStates.READ_CLOSED: - this._innersrc.end(); - break; - case StreamStates.WRITE_CLOSED: - this.closeWritePromise.resolve(); - break; - case StreamStates.CLOSED: - this.close(); - } - } - } - return m.message; - } - - /** - * Close a stream for reading and writing - */ - close(): void { - this.stat.timeline.close = new Date().getTime(); - this.streamState.state = StreamStates.CLOSED; - this._innersrc.end(); - this.closeWritePromise.resolve(); - this.channel.close(); - if (this.closeCb) { - this.closeCb(this) - } - } - - /** - * Close a stream for reading only - */ - closeRead(): void { - const [currentState, nextState] = this.streamState.transition({direction: 'outbound', flag: pb.Message_Flag.STOP_SENDING}); - if (currentState == StreamStates.OPEN || currentState == StreamStates.WRITE_CLOSED) { - this._sendFlag(pb.Message_Flag.STOP_SENDING); - (this._innersrc).end(); - } - if (currentState != nextState && nextState == StreamStates.CLOSED) { - this.close(); - } - } - - /** - * Close a stream for writing only - */ - closeWrite(): void { - const [currentState, nextState] = this.streamState.transition({direction: 'outbound', flag: pb.Message_Flag.FIN}); - if (currentState == StreamStates.OPEN || currentState == StreamStates.READ_CLOSED) { - this._sendFlag(pb.Message_Flag.FIN); - this.closeWritePromise.resolve(); - } - if (currentState != nextState && nextState == StreamStates.CLOSED) { - this.close(); - } - } - - /** - * Call when a local error occurs, should close the stream for reading and writing - */ - abort(err: Error): void { - this.close(); - } - - /** - * Close the stream for writing, and indicate to the remote side this is being done 'abruptly' - * @see closeWrite - */ - reset(): void { - this.stat = defaultStat(this.stat.direction); - const [currentState, nextState] = this.streamState.transition({direction: 'outbound', flag: pb.Message_Flag.RESET}); - if (currentState != nextState) { - this._sendFlag(pb.Message_Flag.RESET); - this.close(); + close: undefined } } +} - private _sendFlag(flag: pb.Message_Flag): void { - try { - log.trace('Sending flag: %s', flag.toString()); - const msgbuf = pb.Message.toBinary({flag: flag}); - this.channel.send(lengthPrefixed.encode.single(msgbuf).subarray()); - } catch (e) { - log.error(`Exception while sending flag ${flag}: ${e}`); - } - } +interface StreamInitOpts { + channel: RTCDataChannel + metadata?: Record + stat: StreamStat + closeCb?: (stream: WebRTCStream) => void } /* * State transitions for a stream */ -type StreamStateInput = { - direction: 'inbound' | 'outbound', - flag: pb.Message_Flag, -}; +interface StreamStateInput { + direction: 'inbound' | 'outbound' + flag: pb.Message_Flag +} export enum StreamStates { OPEN, @@ -281,54 +47,334 @@ export enum StreamStates { class StreamState { state: StreamStates = StreamStates.OPEN - transition({direction, flag}: StreamStateInput): [StreamStates, StreamStates] { - let prev = this.state; - if (this.state == StreamStates.CLOSED) { - return [prev, StreamStates.CLOSED]; + transition ({ direction, flag }: StreamStateInput): [StreamStates, StreamStates] { + const prev = this.state + + // return early if the stream is closed + if (this.state === StreamStates.CLOSED) { + return [prev, StreamStates.CLOSED] } - if (direction == 'inbound') { + + if (direction === 'inbound') { switch (flag) { case pb.Message_Flag.FIN: - if (this.state == StreamStates.OPEN) { - this.state = StreamStates.READ_CLOSED; - } else if (this.state == StreamStates.WRITE_CLOSED) { - this.state = StreamStates.CLOSED; + if (this.state === StreamStates.OPEN) { + this.state = StreamStates.READ_CLOSED + } else if (this.state === StreamStates.WRITE_CLOSED) { + this.state = StreamStates.CLOSED } - break; + break case pb.Message_Flag.STOP_SENDING: - if (this.state == StreamStates.OPEN) { - this.state = StreamStates.WRITE_CLOSED; - } else if (this.state == StreamStates.READ_CLOSED) { - this.state = StreamStates.CLOSED; + if (this.state === StreamStates.OPEN) { + this.state = StreamStates.WRITE_CLOSED + } else if (this.state === StreamStates.READ_CLOSED) { + this.state = StreamStates.CLOSED } - break; + break case pb.Message_Flag.RESET: - this.state = StreamStates.CLOSED; + this.state = StreamStates.CLOSED + break + + // no default } } else { switch (flag) { case pb.Message_Flag.FIN: - if (this.state == StreamStates.OPEN) { - this.state = StreamStates.WRITE_CLOSED; - } else if (this.state == StreamStates.READ_CLOSED) { - this.state = StreamStates.CLOSED; + if (this.state === StreamStates.OPEN) { + this.state = StreamStates.WRITE_CLOSED + } else if (this.state === StreamStates.READ_CLOSED) { + this.state = StreamStates.CLOSED } - break; + break case pb.Message_Flag.STOP_SENDING: - if (this.state == StreamStates.OPEN) { - this.state = StreamStates.READ_CLOSED; - } else if (this.state == StreamStates.WRITE_CLOSED) { - this.state = StreamStates.CLOSED; + if (this.state === StreamStates.OPEN) { + this.state = StreamStates.READ_CLOSED + } else if (this.state === StreamStates.WRITE_CLOSED) { + this.state = StreamStates.CLOSED } - break; + break case pb.Message_Flag.RESET: - this.state = StreamStates.CLOSED; + this.state = StreamStates.CLOSED + break + + // no default } } - return [prev, this.state]; + return [prev, this.state] } } + +export class WebRTCStream implements Stream { + /** + * Unique identifier for a stream + */ + id: string; + + /** + * Stats about this stream + */ + stat: StreamStat; + + /** + * User defined stream metadata + */ + metadata: Record; + + /** + * The data channel used to send and receive data + */ + private readonly channel: RTCDataChannel; + + /** + * The current state of the stream + */ + streamState = new StreamState(); + + /** + * Read unwrapped protobuf data from the underlying datachannel. + * _src is exposed to the user via the `source` getter to . + */ + private readonly _src: Source; + + /** + * push data from the underlying datachannel to the length prefix decoder + * and then the protobuf decoder. + */ + private readonly _innersrc = pushable(); + + /** + * Write data to the remote peer. + * It takes care of wrapping data in a protobuf and adding the length prefix. + */ + sink: Sink>; + + /** + * Deferred promise that resolves when the underlying datachannel is in the + * open state. + */ + opened: DeferredPromise = defer(); + + /** + * Triggers a generator which can be used to close the sink. + */ + closeWritePromise: DeferredPromise = defer(); + + /** + * Callback to invoke when the stream is closed. + */ + closeCb?: (stream: WebRTCStream) => void + + constructor (opts: StreamInitOpts) { + this.channel = opts.channel + this.id = this.channel.label + + this.stat = opts.stat + switch (this.channel.readyState) { + case 'open': + this.opened.resolve() + break + + case 'closed': + case 'closing': + this.streamState.state = StreamStates.CLOSED + if (this.stat.timeline.close === undefined || this.stat.timeline.close === 0) { + this.stat.timeline.close = new Date().getTime() + } + this.opened.resolve() + break + + // no default + } + + this.metadata = opts.metadata ?? {} + + // closable sink + this.sink = this._sinkFn + + // handle RTCDataChannel events + this.channel.onopen = (_evt) => { + this.stat.timeline.open = new Date().getTime() + this.opened.resolve() + } + + this.channel.onclose = (_evt) => { + this.close() + } + + this.channel.onerror = (evt) => { + const err = (evt as RTCErrorEvent).error + this.abort(err) + } + + const self = this + + // reader pipe + this.channel.onmessage = async ({ data }) => { + if (data === null || data.length === 0) { + return + } + this._innersrc.push(new Uint8Array(data as ArrayBufferLike)) + } + + // pipe framed protobuf messages through + // a length prefixed decoder, and surface + // data from the `Message.message` field + // through a source. + this._src = pipe( + this._innersrc, + lengthPrefixed.decode(), + (source) => (async function * () { + for await (const buf of source) { + const message = self.processIncomingProtobuf(buf.subarray()) + if (message != null) { + yield new Uint8ArrayList(message) + } + } + })() + ) + } + + // If user attempts to set a new source + // this should be a nop + set source (_src: Source) { + } + + get source (): Source { + return this._src + } + + private async _sinkFn (src: Source): Promise { + await this.opened.promise + if (this.streamState.state === StreamStates.CLOSED || this.streamState.state === StreamStates.WRITE_CLOSED) { + return + } + + const self = this + const closeWriteIterable = { + async * [Symbol.asyncIterator] () { + await self.closeWritePromise.promise + yield new Uint8Array(0) + } + } + + for await (const buf of merge(closeWriteIterable, src)) { + const state = self.streamState.state + if (state === StreamStates.CLOSED || state === StreamStates.WRITE_CLOSED) { + return + } + const msgbuf = pb.Message.toBinary({ message: buf.subarray() }) + const sendbuf = lengthPrefixed.encode.single(msgbuf) + this.channel.send(sendbuf.subarray()) + } + } + + processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined { + const message = pb.Message.fromBinary(buffer) + + if (message.flag !== undefined) { + const [currentState, nextState] = this.streamState.transition({ direction: 'inbound', flag: message.flag }) + if (currentState !== nextState) { + // @TODO(ddimaria): determine if we need to check for StreamStates.OPEN + switch (nextState) { + case StreamStates.READ_CLOSED: + this._innersrc.end() + break + case StreamStates.WRITE_CLOSED: + this.closeWritePromise.resolve() + break + case StreamStates.CLOSED: + this.close() + break + + // no default + } + } + } + return message.message + } + + /** + * Close a stream for reading and writing + */ + close (): void { + this.stat.timeline.close = new Date().getTime() + this.streamState.state = StreamStates.CLOSED + this._innersrc.end() + this.closeWritePromise.resolve() + this.channel.close() + + if (this.closeCb !== undefined) { + this.closeCb(this) + } + } + + /** + * Close a stream for reading only + */ + closeRead (): void { + const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.STOP_SENDING }) + + if (currentState === StreamStates.OPEN || currentState === StreamStates.WRITE_CLOSED) { + this._sendFlag(pb.Message_Flag.STOP_SENDING); + (this._innersrc).end() + } + + if (currentState !== nextState && nextState === StreamStates.CLOSED) { + this.close() + } + } + + /** + * Close a stream for writing only + */ + closeWrite (): void { + const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.FIN }) + + if (currentState === StreamStates.OPEN || currentState === StreamStates.READ_CLOSED) { + this._sendFlag(pb.Message_Flag.FIN) + this.closeWritePromise.resolve() + } + + if (currentState !== nextState && nextState === StreamStates.CLOSED) { + this.close() + } + } + + /** + * Call when a local error occurs, should close the stream for reading and writing + */ + abort (err: Error): void { + log.error(`An error occurred, clost the stream for reading and writing: ${err.message}`) + this.close() + } + + /** + * Close the stream for writing, and indicate to the remote side this is being done 'abruptly' + * + * @see closeWrite + */ + reset (): void { + this.stat = defaultStat(this.stat.direction) + const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.RESET }) + if (currentState !== nextState) { + this._sendFlag(pb.Message_Flag.RESET) + this.close() + } + } + + private _sendFlag (flag: pb.Message_Flag): void { + try { + log.trace('Sending flag: %s', flag.toString()) + const msgbuf = pb.Message.toBinary({ flag: flag }) + this.channel.send(lengthPrefixed.encode.single(msgbuf).subarray()) + } catch (err) { + if (err instanceof Error) { + log.error(`Exception while sending flag ${flag}: ${err.message}`) + } + } + } +} diff --git a/src/transport.ts b/src/transport.ts index c922c2b..be2a6f6 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -1,65 +1,68 @@ -import * as sdp from './sdp.js'; -import * as p from '@libp2p/peer-id'; -import {WebRTCDialOptions} from './options.js'; -import {WebRTCStream} from './stream.js'; -import {Noise} from '@chainsafe/libp2p-noise'; -import {Connection} from '@libp2p/interface-connection'; -import type {PeerId} from '@libp2p/interface-peer-id'; -import {CreateListenerOptions, Listener, symbol, Transport} from '@libp2p/interface-transport'; -import {logger} from '@libp2p/logger'; -import {Multiaddr} from '@multiformats/multiaddr'; -import {v4 as genUuid} from 'uuid'; -import defer from 'p-defer'; -import {fromString as uint8arrayFromString} from 'uint8arrays/from-string'; -import {concat} from 'uint8arrays/concat'; -import * as multihashes from 'multihashes'; -import {dataChannelError, inappropriateMultiaddr, unimplemented, invalidArgument} from './error.js'; -import {WebRTCMultiaddrConnection} from './maconn.js'; -import {DataChannelMuxerFactory} from './muxer.js'; - -const log = logger('libp2p:webrtc:transport'); -const HANDSHAKE_TIMEOUT_MS = 10000; -const WEBRTC_CODE: number = 280; -const CERTHASH_CODE: number = 466; - +import { noise as Noise } from '@chainsafe/libp2p-noise' +import { Connection } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import { CreateListenerOptions, Listener, symbol, Transport } from '@libp2p/interface-transport' +import { logger } from '@libp2p/logger' +import * as p from '@libp2p/peer-id' +import { Multiaddr } from '@multiformats/multiaddr' +import * as multihashes from 'multihashes' +import defer from 'p-defer' +import { v4 as genUuid } from 'uuid' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' +import { concat } from 'uint8arrays/concat' + +import { dataChannelError, inappropriateMultiaddr, unimplemented, invalidArgument } from './error.js' +import { WebRTCMultiaddrConnection } from './maconn.js' +import { DataChannelMuxerFactory } from './muxer.js' +import { WebRTCDialOptions } from './options.js' +import * as sdp from './sdp.js' +import { WebRTCStream } from './stream.js' + +const log = logger('libp2p:webrtc:transport') +const HANDSHAKE_TIMEOUT_MS = 10000 +const WEBRTC_CODE: number = 280 +const CERTHASH_CODE: number = 466 export interface WebRTCTransportComponents { peerId: PeerId } export class WebRTCTransport implements Transport { - private components: WebRTCTransportComponents + private readonly components: WebRTCTransportComponents - constructor(components: WebRTCTransportComponents) { + constructor (components: WebRTCTransportComponents) { this.components = components } - async dial(ma: Multiaddr, options: WebRTCDialOptions): Promise { - const rawConn = await this._connect(ma, options); - log(`dialing address - ${ma}`); - return rawConn; + async dial (ma: Multiaddr, options: WebRTCDialOptions): Promise { + const rawConn = await this._connect(ma, options) + log(`dialing address - ${ma.toString()}`) + return rawConn } - createListener(options: CreateListenerOptions): Listener { - throw unimplemented('WebRTCTransport.createListener'); + createListener (options: CreateListenerOptions): Listener { + throw unimplemented('WebRTCTransport.createListener') } - filter(multiaddrs: Multiaddr[]): Multiaddr[] { - return multiaddrs.filter(validMa); + // Filter out invalid multiaddrs + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter(validMa) } - get [Symbol.toStringTag](): string { - return '@libp2p/webrtc'; + // Implement toString() for WebRTCTransport + get [Symbol.toStringTag] (): string { + return '@libp2p/webrtc' } - get [symbol](): true { - return true; + get [symbol] (): true { + return true } - async _connect(ma: Multiaddr, options: WebRTCDialOptions): Promise { - const rps = ma.getPeerId(); - if (!rps) { - throw inappropriateMultiaddr("we need to have the remote's PeerId"); + // Connect to a peer + async _connect (ma: Multiaddr, options: WebRTCDialOptions): Promise { + const rps = ma.getPeerId() + if (rps === null) { + throw inappropriateMultiaddr("we need to have the remote's PeerId") } const remoteCerthash = sdp.decodeCerthash(sdp.certhash(ma)) @@ -70,67 +73,75 @@ export class WebRTCTransport implements Transport { const certificate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256', - hash: sdp.toSupportedHashFunction(remoteCerthash.name), - } as any); - const peerConnection = new RTCPeerConnection({certificates: [certificate]}); + hash: sdp.toSupportedHashFunction(remoteCerthash.name) + } as any) + const peerConnection = new RTCPeerConnection({ certificates: [certificate] }) // create data channel for running the noise handshake. Once the data channel is opened, // the remote will initiate the noise handshake. This is used to confirm the identity of // the peer. - const dataChannelOpenPromise = defer(); - const handshakeDataChannel = peerConnection.createDataChannel('handshake', {negotiated: true, id: 0}); + const dataChannelOpenPromise = defer() + const handshakeDataChannel = peerConnection.createDataChannel('handshake', { negotiated: true, id: 0 }) const handhsakeTimeout = setTimeout(() => { - log.error('Data channel never opened. State was: %s', handshakeDataChannel.readyState.toString()); - dataChannelOpenPromise.reject(dataChannelError('data', `data channel was never opened: state: ${handshakeDataChannel.readyState}`)); - }, HANDSHAKE_TIMEOUT_MS); + const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}` + log.error(error) + dataChannelOpenPromise.reject(dataChannelError('data', error)) + }, HANDSHAKE_TIMEOUT_MS) handshakeDataChannel.onopen = (_) => { clearTimeout(handhsakeTimeout) - dataChannelOpenPromise.resolve(); + dataChannelOpenPromise.resolve() } - handshakeDataChannel.onerror = (ev: Event) => { + // ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event + handshakeDataChannel.onerror = (event: Event) => { clearTimeout(handhsakeTimeout) - log.error('Error opening a data channel for handshaking: %s', ev.toString()); - dataChannelOpenPromise.reject(dataChannelError('data', `error opening datachannel: ${ev.toString()}`)); - }; - - - let offerSdp = await peerConnection.createOffer(); - const ufrag = "libp2p+webrtc+v1/" + genUuid().replaceAll('-', ''); - // munge sdp with ufrag = pwd. This allows the remote to respond to - // STUN messages without performing an actual SDP exchange. This is because - // it can infer the passwd field by reading the USERNAME attribute - // of the STUN message. - offerSdp = sdp.munge(offerSdp, ufrag); - await peerConnection.setLocalDescription(offerSdp); - // construct answer sdp from multiaddr - const answerSdp = sdp.fromMultiAddr(ma, ufrag); - await peerConnection.setRemoteDescription(answerSdp); + const errorTarget = event.target?.toString() ?? 'not specified' + const error = `Error opening a data channel for handshaking: ${errorTarget}` + log.error(error) + dataChannelOpenPromise.reject(dataChannelError('data', error)) + } + + const ufrag = 'libp2p+webrtc+v1/' + genUuid().replaceAll('-', '') + + // Create offer and munge sdp with ufrag = pwd. This allows the remote to + // respond to STUN messages without performing an actual SDP exchange. + // This is because it can infer the passwd field by reading the USERNAME + // attribute of the STUN message. + const offerSdp = await peerConnection.createOffer() + const mungedOfferSdp = sdp.munge(offerSdp, ufrag) + await peerConnection.setLocalDescription(mungedOfferSdp) + + // construct answer sdp from multiaddr and ufrag + const answerSdp = sdp.fromMultiAddr(ma, ufrag) + await peerConnection.setRemoteDescription(answerSdp) + // wait for peerconnection.onopen to fire, or for the datachannel to open - await dataChannelOpenPromise.promise; + await dataChannelOpenPromise.promise + + const myPeerId = this.components.peerId + const theirPeerId = p.peerIdFromString(rps) - const myPeerId = this.components.peerId; - const theirPeerId = p.peerIdFromString(rps); + // Do noise handshake. + // Set the Noise Prologue to libp2p-webrtc-noise: before starting the actual Noise handshake. + // is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order. + const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.name, ma) - // do noise handshake - //set the Noise Prologue to libp2p-webrtc-noise: before starting the actual Noise handshake. - // is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order. - const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.name, ma); // Since we use the default crypto interface and do not use a static key or early data, // we pass in undefined for these parameters. - const noise = new Noise(undefined, undefined, undefined, fingerprintsPrologue); - const wrappedChannel = new WebRTCStream({channel: handshakeDataChannel, stat: {direction: 'outbound', timeline: {open: 1}}}); + const noiseInit = { staticNoiseKey: undefined, extensions: undefined, crypto: undefined, prologueBytes: fingerprintsPrologue } + const noise = Noise(noiseInit)() + const wrappedChannel = new WebRTCStream({ channel: handshakeDataChannel, stat: { direction: 'outbound', timeline: { open: 1 } } }) const wrappedDuplex = { ...wrappedChannel, source: { - [Symbol.asyncIterator]: async function* () { + [Symbol.asyncIterator]: async function * () { for await (const list of wrappedChannel.source) { - yield list.subarray(); + yield list.subarray() } - }, - }, - }; + } + } + } // Creating the connection before completion of the noise // handshake ensures that the stream opening callback is set up @@ -138,41 +149,49 @@ export class WebRTCTransport implements Transport { peerConnection, remoteAddr: ma, timeline: { - open: (new Date()).getTime(), - }, + open: (new Date()).getTime() + } }) const muxerFactory = new DataChannelMuxerFactory(peerConnection) + // For outbound connections, the remote is expected to start the noise handshake. // Therefore, we need to secure an inbound noise connection from the remote. - await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId); - const upgraded = await options.upgrader.upgradeOutbound(maConn, {skipProtection: true, skipEncryption: true, muxerFactory}) - return upgraded + await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId) + + return await options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory }) } - private generateNoisePrologue(pc: RTCPeerConnection, hashName: multihashes.HashName, ma: Multiaddr): Uint8Array { + // Generate a noise prologue from the peer connection's certificate. + // noise prologue = bytes('libp2p-webrtc-noise:') + noise-responder fingerprint + noise-initiator fingerprint + private generateNoisePrologue (pc: RTCPeerConnection, hashName: multihashes.HashName, ma: Multiaddr): Uint8Array { if (pc.getConfiguration().certificates?.length === 0) { - throw invalidArgument('no local certificate'); + throw invalidArgument('no local certificate') } - const localCert = pc.getConfiguration().certificates?.at(0)!; - if (!localCert || localCert.getFingerprints().length === 0) { - throw invalidArgument('no fingerprint on local certificate'); + + const localCert = pc.getConfiguration().certificates?.at(0) + + if (localCert === undefined || localCert.getFingerprints().length === 0) { + throw invalidArgument('no fingerprint on local certificate') } - const localFingerprint = localCert.getFingerprints()[0]; - const localFpString = localFingerprint.value!.replaceAll(':', ''); - const localFpArray = uint8arrayFromString(localFpString, 'hex'); - const local = multihashes.encode(localFpArray, multihashes.names[hashName]); + const localFingerprint = localCert.getFingerprints()[0] + + if (localFingerprint.value === undefined) { + throw invalidArgument('no fingerprint on local certificate') + } - const remote: Uint8Array = sdp.mbdecoder.decode(sdp.certhash(ma)); - const prefix = uint8arrayFromString('libp2p-webrtc-noise:'); + const localFpString = localFingerprint.value.replace(/:/g, '') + const localFpArray = uint8arrayFromString(localFpString, 'hex') + const local = multihashes.encode(localFpArray, multihashes.names[hashName]) + const remote: Uint8Array = sdp.mbdecoder.decode(sdp.certhash(ma)) + const prefix = uint8arrayFromString('libp2p-webrtc-noise:') - // prologue = bytes("libp2p-webrtc-noise:") + noise-responder fingerprint + noise-initiator fingerprint - return concat([prefix, local, remote]); + return concat([prefix, local, remote]) } } -function validMa(ma: Multiaddr): boolean { - const codes = ma.protoCodes(); - return codes.includes(WEBRTC_CODE) && codes.includes(CERTHASH_CODE) && ma.getPeerId() != null; +function validMa (ma: Multiaddr): boolean { + const codes = ma.protoCodes() + return codes.includes(WEBRTC_CODE) && codes.includes(CERTHASH_CODE) && ma.getPeerId() != null } diff --git a/src/util.ts b/src/util.ts index ea829ae..91fbfcc 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,5 @@ - export const nopSource = { - async *[Symbol.asyncIterator]() {} + async * [Symbol.asyncIterator] () {} } export const nopSink = async (_: any) => {} diff --git a/test/connection.browser.spec.ts b/test/connection.browser.spec.ts index 2b5541e..fa594b1 100644 --- a/test/connection.browser.spec.ts +++ b/test/connection.browser.spec.ts @@ -1,15 +1,15 @@ /* eslint-env mocha */ -//import {createConnectionPair, echoHandler} from "../test/util.js"; -//import { expect } from 'aegir/chai'; -//import { pipe } from 'it-pipe'; -//import first from 'it-first'; -//import {fromString} from 'uint8arrays/from-string'; -//import {v4} from 'uuid'; +// import {createConnectionPair, echoHandler} from "../test/util.js"; +// import { expect } from 'aegir/chai'; +// import { pipe } from 'it-pipe'; +// import first from 'it-first'; +// import {fromString} from 'uint8arrays/from-string'; +// import {v4} from 'uuid'; -//const echoProtocol = '/echo/1.0.0'; +// const echoProtocol = '/echo/1.0.0'; -//describe('connection browser tests', () => { +// describe('connection browser tests', () => { // it('can run the echo protocol (first)', async () => { // let [{ connection: client }, server] = await createConnectionPair(); // let serverRegistrar = server.registrar; @@ -43,6 +43,6 @@ // expect(responsed).to.be.true(); // }); -//}); +// }); -export {}; +export {} diff --git a/test/sdp.spec.ts b/test/sdp.spec.ts index 18c2bf0..8055cc0 100644 --- a/test/sdp.spec.ts +++ b/test/sdp.spec.ts @@ -1,75 +1,76 @@ -import { multiaddr } from '@multiformats/multiaddr'; -import { expect } from 'chai'; -import * as underTest from '../src/sdp.js'; -import { bases } from 'multiformats/basics'; -import * as multihashes from 'multihashes'; +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'chai' +import * as underTest from '../src/sdp' -const an_sdp = `v=0 -o=- 0 0 IN IP4 192.168.0.152 +const sampleMultiAddr = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ') +const sampleCerthash = 'uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ' +const sampleSdp = `v=0 +o=- 0 0 IN IP4 0.0.0.0 s=- -c=IN IP4 192.168.0.152 +c=IN IP4 0.0.0.0 t=0 0 a=ice-lite -m=application 2345 UDP/DTLS/SCTP webrtc-datachannel +m=application 56093 UDP/DTLS/SCTP webrtc-datachannel a=mid:0 -a=setup:active -a=ice-options:ice2 +a=setup:passive a=ice-ufrag:MyUserFragment a=ice-pwd:MyUserFragment -a=fingerprint:sha-256 b9:2e:11:cf:23:ff:da:31:bb:bb:5c:0a:9d:d9:0e:20:07:e2:bb:61:2f:1f:94:cf:e5:2e:0e:05:5c:4e:8a:88 +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 a=sctp-port:5000 a=max-message-size:100000 -a=candidate:1 1 UDP 1 192.168.0.152 2345 typ host`; +a=candidate:1467250027 1 UDP 1467250027 0.0.0.0 56093 typ host` -describe('SDP creation', () => { - it('handles simple blue sky easily enough', async () => { - return; - let ma = multiaddr('/ip4/192.168.0.152/udp/2345/webrtc/certhash/uEiC5LhHPI__aMbu7XAqd2Q4gB-K7YS8flM_lLg4FXE6KiA'); - let ufrag = 'MyUserFragment'; - let sdp = underTest.fromMultiAddr(ma, ufrag); - expect(sdp.sdp).to.equal(an_sdp); - }); +describe('SDP', () => { + it('converts multiaddr with certhash to an answer SDP', async () => { + const ufrag = 'MyUserFragment' + const sdp = underTest.fromMultiAddr(sampleMultiAddr, ufrag) - it('extracts certhash', () => { - let ma = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ'); - let c = underTest.certhash(ma); - expect(c).to.equal('uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ'); - const mbdecoder = (function () { - const decoders = Object.values(bases).map((b) => b.decoder); - let acc = decoders[0].or(decoders[1]); - decoders.slice(2).forEach((d) => (acc = acc.or(d))); - return acc; - })(); + expect(sdp.sdp).to.contain(sampleSdp) + }) - let mbdecoded = mbdecoder.decode(c); - let mhdecoded = multihashes.decode(mbdecoded); - //sha2-256 multihash 0x12 permanent - // https://github.com/multiformats/multicodec/blob/master/table.csv - expect(mhdecoded.name).to.equal('sha2-256'); - expect(mhdecoded.code).to.equal(0x12); - expect(mhdecoded.length).to.equal(32); - expect(mhdecoded.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'); - }); -}); + it('extracts certhash from a multiaddr', () => { + const certhash = underTest.certhash(sampleMultiAddr) -describe('SDP munging', () => { - it('does a simple replacement', () => { - let result = underTest.munge({ type: 'answer', sdp: an_sdp }, 'someotheruserfragmentstring'); - expect(result.sdp).to.equal(`v=0 -o=- 0 0 IN IP4 192.168.0.152 + expect(certhash).to.equal(sampleCerthash) + }) + + it('decodes a certhash', () => { + const decoded = underTest.decodeCerthash(sampleCerthash) + + // sha2-256 multihash 0x12 permanent + // https://github.com/multiformats/multicodec/blob/master/table.csv + expect(decoded.name).to.equal('sha2-256') + expect(decoded.code).to.equal(0x12) + expect(decoded.length).to.equal(32) + 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') + }) + + it('converts a multiaddr into a fingerprint', () => { + const fingerpint = underTest.ma2Fingerprint(sampleMultiAddr) + expect(fingerpint).to.deep.equal([ + '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', + '726847cd48b05ec5604d159cbf401d6f00a123ec90170e2cd1b38fd29d37e5b1' + ]) + }) + + it('munges the ufrag and pwd in a SDP', () => { + const result = underTest.munge({ type: 'answer', sdp: sampleSdp }, 'someotheruserfragmentstring') + const expected = `v=0 +o=- 0 0 IN IP4 0.0.0.0 s=- -c=IN IP4 192.168.0.152 +c=IN IP4 0.0.0.0 t=0 0 a=ice-lite -m=application 2345 UDP/DTLS/SCTP webrtc-datachannel +m=application 56093 UDP/DTLS/SCTP webrtc-datachannel a=mid:0 -a=setup:active -a=ice-options:ice2 +a=setup:passive a=ice-ufrag:someotheruserfragmentstring a=ice-pwd:someotheruserfragmentstring -a=fingerprint:sha-256 b9:2e:11:cf:23:ff:da:31:bb:bb:5c:0a:9d:d9:0e:20:07:e2:bb:61:2f:1f:94:cf:e5:2e:0e:05:5c:4e:8a:88 +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 a=sctp-port:5000 a=max-message-size:100000 -a=candidate:1 1 UDP 1 192.168.0.152 2345 typ host`); - }); -}); +a=candidate:1467250027 1 UDP 1467250027 0.0.0.0 56093 typ host` + + expect(result.sdp).to.equal(expected) + }) +}) diff --git a/test/server-multiaddr.js b/test/server-multiaddr.js deleted file mode 100644 index 978d6d2..0000000 --- a/test/server-multiaddr.js +++ /dev/null @@ -1 +0,0 @@ -export var SERVER_MULTIADDR = ''; diff --git a/test/server-multiaddr.ts b/test/server-multiaddr.ts new file mode 100644 index 0000000..b963fd9 --- /dev/null +++ b/test/server-multiaddr.ts @@ -0,0 +1 @@ +export const SERVER_MULTIADDR = '' diff --git a/test/stream.browser.spec.ts b/test/stream.browser.spec.ts index c4121dc..2a095c1 100644 --- a/test/stream.browser.spec.ts +++ b/test/stream.browser.spec.ts @@ -1,74 +1,74 @@ -import * as underTest from '../src/stream'; -import {expect, assert} from 'chai' +import * as underTest from '../src/stream' +import { expect, assert } from 'chai' -describe('stream stats', () => { +describe('Stream Stats', () => { it('can construct', () => { - let pc = new RTCPeerConnection(); - let dc = pc.createDataChannel('whatever', {negotiated: true, id: 91}); - let s = new underTest.WebRTCStream({channel: dc, stat: underTest.defaultStat('outbound')}); + const pc = new RTCPeerConnection() + const dc = pc.createDataChannel('whatever', { negotiated: true, id: 91 }) + const s = new underTest.WebRTCStream({ channel: dc, stat: underTest.defaultStat('outbound') }) // expect(s.stat.timeline.close).to.not.exist(); - assert.notExists(s.stat.timeline.close); - }); + assert.notExists(s.stat.timeline.close) + }) it('close marks it closed', () => { - let pc = new RTCPeerConnection(); - let dc = pc.createDataChannel('whatever', {negotiated: true, id: 91}); - let s = new underTest.WebRTCStream({channel: dc, stat: underTest.defaultStat('outbound')}); + const pc = new RTCPeerConnection() + const dc = pc.createDataChannel('whatever', { negotiated: true, id: 91 }) + const s = new underTest.WebRTCStream({ channel: dc, stat: underTest.defaultStat('outbound') }) - expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN); - s.close(); - expect(s.streamState.state).to.equal(underTest.StreamStates.CLOSED); - }); + expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN) + s.close() + expect(s.streamState.state).to.equal(underTest.StreamStates.CLOSED) + }) it('closeRead marks it read-closed only', () => { - let pc = new RTCPeerConnection(); - let dc = pc.createDataChannel('whatever', {negotiated: true, id: 91}); - let s = new underTest.WebRTCStream({channel: dc, stat: underTest.defaultStat('outbound')}); - expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN); - s.closeRead(); - expect(s.streamState.state).to.equal(underTest.StreamStates.READ_CLOSED); - }); + const pc = new RTCPeerConnection() + const dc = pc.createDataChannel('whatever', { negotiated: true, id: 91 }) + const s = new underTest.WebRTCStream({ channel: dc, stat: underTest.defaultStat('outbound') }) + expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN) + s.closeRead() + expect(s.streamState.state).to.equal(underTest.StreamStates.READ_CLOSED) + }) it('closeWrite marks it write-closed only', () => { - let pc = new RTCPeerConnection(); - let dc = pc.createDataChannel('whatever', {negotiated: true, id: 91}); - let s = new underTest.WebRTCStream({channel: dc, stat: underTest.defaultStat('outbound')}); - expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN); - s.closeWrite(); - expect(s.streamState.state).to.equal(underTest.StreamStates.WRITE_CLOSED); - }); + const pc = new RTCPeerConnection() + const dc = pc.createDataChannel('whatever', { negotiated: true, id: 91 }) + const s = new underTest.WebRTCStream({ channel: dc, stat: underTest.defaultStat('outbound') }) + expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN) + s.closeWrite() + expect(s.streamState.state).to.equal(underTest.StreamStates.WRITE_CLOSED) + }) it('closeWrite AND closeRead = close', () => { - let pc = new RTCPeerConnection(); - let dc = pc.createDataChannel('whatever', {negotiated: true, id: 91}); - let s = new underTest.WebRTCStream({channel: dc, stat: underTest.defaultStat('outbound')}); - s.closeWrite(); - expect(s.streamState.state).to.equal(underTest.StreamStates.WRITE_CLOSED); - s.closeRead(); - expect(s.streamState.state).to.equal(underTest.StreamStates.CLOSED); - }); + const pc = new RTCPeerConnection() + const dc = pc.createDataChannel('whatever', { negotiated: true, id: 91 }) + const s = new underTest.WebRTCStream({ channel: dc, stat: underTest.defaultStat('outbound') }) + s.closeWrite() + expect(s.streamState.state).to.equal(underTest.StreamStates.WRITE_CLOSED) + s.closeRead() + expect(s.streamState.state).to.equal(underTest.StreamStates.CLOSED) + }) it('abort = close', () => { - let pc = new RTCPeerConnection(); - let dc = pc.createDataChannel('whatever', {negotiated: true, id: 91}); - let s = new underTest.WebRTCStream({channel: dc, stat: underTest.defaultStat('outbound')}); + const pc = new RTCPeerConnection() + const dc = pc.createDataChannel('whatever', { negotiated: true, id: 91 }) + const s = new underTest.WebRTCStream({ channel: dc, stat: underTest.defaultStat('outbound') }) // expect(s.stat.timeline.close).to.not.exist(); - expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN); - s.abort({name: 'irrelevant', message: 'this parameter is actually ignored'}); - expect(s.streamState.state).to.equal(underTest.StreamStates.CLOSED); + expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN) + s.abort({ name: 'irrelevant', message: 'this parameter is actually ignored' }) + expect(s.streamState.state).to.equal(underTest.StreamStates.CLOSED) // expect(s.stat.timeline.close).to.exist(); - expect(s.stat.timeline.close).to.be.greaterThan(s.stat.timeline.open); - }); + expect(s.stat.timeline.close).to.be.greaterThan(s.stat.timeline.open) + }) it('reset = close', () => { - let pc = new RTCPeerConnection(); - let dc = pc.createDataChannel('whatever', {negotiated: true, id: 91}); - let s = new underTest.WebRTCStream({channel: dc, stat: underTest.defaultStat('outbound')}); + const pc = new RTCPeerConnection() + const dc = pc.createDataChannel('whatever', { negotiated: true, id: 91 }) + const s = new underTest.WebRTCStream({ channel: dc, stat: underTest.defaultStat('outbound') }) // expect(s.stat.timeline.close).to.not.exist(); - expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN); - s.reset(); //only resets the write side - expect(s.streamState.state).to.equal(underTest.StreamStates.CLOSED); + expect(s.streamState.state).to.equal(underTest.StreamStates.OPEN) + s.reset() // only resets the write side + expect(s.streamState.state).to.equal(underTest.StreamStates.CLOSED) // expect(s.stat.timeline.close).to.not.exist(); - expect(dc.readyState).to.be.oneOf(['closing', 'closed']); - }); -}); + expect(dc.readyState).to.be.oneOf(['closing', 'closed']) + }) +}) diff --git a/test/transport.browser.spec.ts b/test/transport.browser.spec.ts index 39034f8..c0cf311 100644 --- a/test/transport.browser.spec.ts +++ b/test/transport.browser.spec.ts @@ -1,27 +1,25 @@ -import * as underTest from '../src/transport.js'; -import {UnimplementedError} from '../src/error.js'; -import {webRTC} from '../src/index.js'; -import {mockUpgrader} from '@libp2p/interface-mocks'; -import {CreateListenerOptions, symbol} from '@libp2p/interface-transport'; -import {multiaddr, Multiaddr} from '@multiformats/multiaddr'; -import {SERVER_MULTIADDR} from './server-multiaddr'; -import {Noise} from '@chainsafe/libp2p-noise'; -import {createLibp2p} from 'libp2p'; -import {fromString as uint8arrayFromString} from 'uint8arrays/from-string'; -import {pipe} from 'it-pipe'; -import first from 'it-first'; -import {createEd25519PeerId} from '@libp2p/peer-id-factory'; - -const {expect, assert} = require('chai').use(require('chai-bytes')); - -function ignoredDialOption(): CreateListenerOptions { - let u = mockUpgrader({}); - return { - upgrader: u - }; +import * as underTest from './../src/transport' +import { expectError } from './util' +import { UnimplementedError } from './../src/error' +import { webRTC } from '../src/index' +import { mockUpgrader } from '@libp2p/interface-mocks' +import { CreateListenerOptions, symbol } from '@libp2p/interface-transport' +import { multiaddr, Multiaddr } from '@multiformats/multiaddr' +import { SERVER_MULTIADDR } from './server-multiaddr' +import { noise } from '@chainsafe/libp2p-noise' +import { createLibp2p } from 'libp2p' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' +import { pipe } from 'it-pipe' +import first from 'it-first' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect, assert } from 'chai' + +function ignoredDialOption (): CreateListenerOptions { + const upgrader = mockUpgrader({}) + return { upgrader } } -describe('basic transport tests', () => { +describe('WebRTC Transport', () => { let components: underTest.WebRTCTransportComponents before(async () => { @@ -30,95 +28,90 @@ describe('basic transport tests', () => { } }) - it('Can construct', () => { - let t = new underTest.WebRTCTransport(components); - expect(t.constructor.name).to.equal('WebRTCTransport'); - }); + it('can construct', () => { + const t = new underTest.WebRTCTransport(components) + expect(t.constructor.name).to.equal('WebRTCTransport') + }) + // @TODO(ddimaria): determine if this test has value it('createListner does throw', () => { - let t = new underTest.WebRTCTransport(components); + const t = new underTest.WebRTCTransport(components) try { - t.createListener(ignoredDialOption()); - expect('Should have thrown').to.equal('but did not'); + t.createListener(ignoredDialOption()) + expect('Should have thrown').to.equal('but did not') } catch (e) { - expect(e).to.be.instanceOf(UnimplementedError); + expect(e).to.be.instanceOf(UnimplementedError) } - }); - - it('toString includes the toStringTag', () => { - let t = new underTest.WebRTCTransport(components); - let s = t.toString(); - expect(s).to.contain('@libp2p/webrtc'); - }); + }) + // @TODO(ddimaria): determine if this test has value it('toString property getter', () => { - let t = new underTest.WebRTCTransport(components); - let s = t[Symbol.toStringTag]; - expect(s).to.equal('@libp2p/webrtc'); - }); + const t = new underTest.WebRTCTransport(components) + const s = t[Symbol.toStringTag] + expect(s).to.equal('@libp2p/webrtc') + }) + // @TODO(ddimaria): determine if this test has value it('symbol property getter', () => { - let t = new underTest.WebRTCTransport(components); - let s = t[symbol]; - expect(s).to.equal(true); - }); + const t = new underTest.WebRTCTransport(components) + const s = t[symbol] + expect(s).to.equal(true) + }) - it('filter gets rid of some invalids and returns a valid', async () => { - let mas: Multiaddr[] = [ + it('transport filter filters out invalid multiaddrs', async () => { + const mas: Multiaddr[] = [ '/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ', '/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', '/ip4/1.2.3.4/udp/1234/webrtc/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', - '/ip4/1.2.3.4/udp/1234/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', - ].map((s) => { - return multiaddr(s); - }); - let t = new underTest.WebRTCTransport(components); - let result = t.filter(mas); - let expected: Multiaddr[] = [ - multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd'), - ]; - // expect(result).to.not.be.null(); - assert.isNotNull(result); - expect(result.constructor.name).to.equal('Array'); - expect(expected.constructor.name).to.equal('Array'); - expect(result).to.eql(expected); - }); - - it('throws appropriate error when dialing someone without a peer ID', async () => { - let ma = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ'); - let t = new underTest.WebRTCTransport(components); + '/ip4/1.2.3.4/udp/1234/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd' + ].map((s) => multiaddr(s)) + const t = new underTest.WebRTCTransport(components) + const result = t.filter(mas) + const expected: Multiaddr[] = [ + multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') + ] + + assert.isNotNull(result) + expect(result.constructor.name).to.equal('Array') + expect(expected.constructor.name).to.equal('Array') + expect(result).to.eql(expected) + }) + + it('throws WebRTC transport error when dialing a multiaddr without a PeerId', async () => { + const ma = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') + const transport = new underTest.WebRTCTransport(components) + try { - let conn = await t.dial(ma, ignoredDialOption()); - expect(conn.toString()).to.equal('Should have thrown'); - } catch (e) { - expect(e).to.be.instanceOf(Error); - if (e instanceof Error) { - // let err: Error = e; - expect(e.message).to.contain('PeerId'); - } + await transport.dial(ma, ignoredDialOption()) + } catch (error) { + const expected = 'WebRTC transport error: There was a problem with the Multiaddr which was passed in: we need to have the remote\'s PeerId' + expectError(error, expected) } - }); -}); + }) +}) -describe('Transport interoperability tests', () => { +// @TODO(ddimaria): remove this test and remove related scripts in packageon +describe('WebRTC Transport Interoperability', () => { it('can connect to a server', async () => { - if (SERVER_MULTIADDR) { - console.log('Will test connecting to', SERVER_MULTIADDR); - } else { - console.log('Will not test connecting to an external server, as we do not appear to have one.'); - return; + // we do not test connecting to an external server, as we do not appear to have one + if (SERVER_MULTIADDR === '') { + return } + const node = await createLibp2p({ transports: [webRTC()], - connectionEncryption: [() => new Noise()], - }); + connectionEncryption: [noise({})] + }) + await node.start() + const ma = multiaddr(SERVER_MULTIADDR) const stream = await node.dialProtocol(ma, ['/echo/1.0.0']) - let data = 'dataToBeEchoedBackToMe\n'; - let response = await pipe([uint8arrayFromString(data)], stream, async (source) => await first(source)); - expect(response?.subarray()).to.equalBytes(uint8arrayFromString(data)); - await node.stop(); - }); -}); + const data = 'dataToBeEchoedBackToMe\n' + const response = await pipe([uint8arrayFromString(data)], stream, async (source) => await first(source)) + expect(response?.subarray()).to.equal(uint8arrayFromString(data)) + + await node.stop() + }) +}) diff --git a/test/util.ts b/test/util.ts index 967753a..c272783 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,3 +1,13 @@ +import { expect } from 'chai' + +export const expectError = (error: unknown, message: string) => { + if (error instanceof Error) { + expect(error.message).to.equal(message) + } else { + expect('Did not throw error:').to.equal(message) + } +} + // import * as ic from '@libp2p/interface-connection' // import {createEd25519PeerId} from '@libp2p/peer-id-factory'; // import {mockRegistrar, mockUpgrader} from '@libp2p/interface-mocks'; @@ -102,4 +112,3 @@ // { connection: serverConnection, registrar: serverRegistrar }, // ]; // } -export {} diff --git a/tsconfig.json b/tsconfig.json index f000470..0849b1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "aegir/src/config/tsconfig.aegir.json", "compilerOptions": { "allowJs": true, + "allowSyntheticDefaultImports": true, "emitDeclarationOnly": false, "importsNotUsedAsValues": "preserve", "module": "ES2020",