@libp2p/webrtc 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +210 -0
  3. package/dist/index.min.js +54 -0
  4. package/dist/index.min.js.map +7 -0
  5. package/dist/proto_ts/message.d.ts +56 -0
  6. package/dist/proto_ts/message.d.ts.map +1 -0
  7. package/dist/proto_ts/message.js +86 -0
  8. package/dist/proto_ts/message.js.map +1 -0
  9. package/dist/src/error.d.ts +54 -0
  10. package/dist/src/error.d.ts.map +1 -0
  11. package/dist/src/error.js +105 -0
  12. package/dist/src/error.js.map +1 -0
  13. package/dist/src/index.d.ts +4 -0
  14. package/dist/src/index.d.ts.map +1 -0
  15. package/dist/src/index.js +6 -0
  16. package/dist/src/index.js.map +1 -0
  17. package/dist/src/maconn.d.ts +43 -0
  18. package/dist/src/maconn.d.ts.map +1 -0
  19. package/dist/src/maconn.js +29 -0
  20. package/dist/src/maconn.js.map +1 -0
  21. package/dist/src/muxer.d.ts +55 -0
  22. package/dist/src/muxer.d.ts.map +1 -0
  23. package/dist/src/muxer.js +92 -0
  24. package/dist/src/muxer.js.map +1 -0
  25. package/dist/src/options.d.ts +6 -0
  26. package/dist/src/options.d.ts.map +1 -0
  27. package/dist/src/options.js +2 -0
  28. package/dist/src/options.js.map +1 -0
  29. package/dist/src/sdp.d.ts +33 -0
  30. package/dist/src/sdp.d.ts.map +1 -0
  31. package/dist/src/sdp.js +118 -0
  32. package/dist/src/sdp.js.map +1 -0
  33. package/dist/src/stream.d.ts +141 -0
  34. package/dist/src/stream.d.ts.map +1 -0
  35. package/dist/src/stream.js +290 -0
  36. package/dist/src/stream.js.map +1 -0
  37. package/dist/src/transport.d.ts +60 -0
  38. package/dist/src/transport.d.ts.map +1 -0
  39. package/dist/src/transport.js +193 -0
  40. package/dist/src/transport.js.map +1 -0
  41. package/dist/src/util.d.ts +5 -0
  42. package/dist/src/util.d.ts.map +1 -0
  43. package/dist/src/util.js +5 -0
  44. package/dist/src/util.js.map +1 -0
  45. package/dist/stats.json +1 -0
  46. package/package.json +170 -0
  47. package/proto_ts/message.ts +105 -0
  48. package/src/error.ts +123 -0
  49. package/src/index.ts +6 -0
  50. package/src/maconn.ts +67 -0
  51. package/src/message.proto +22 -0
  52. package/src/muxer.ts +120 -0
  53. package/src/options.ts +4 -0
  54. package/src/sdp.ts +136 -0
  55. package/src/stream.ts +422 -0
  56. package/src/transport.ts +243 -0
  57. package/src/util.ts +5 -0
package/src/sdp.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { logger } from '@libp2p/logger'
2
+ import { Multiaddr } from '@multiformats/multiaddr'
3
+ import { bases } from 'multiformats/basics'
4
+ import * as multihashes from 'multihashes'
5
+
6
+ import { inappropriateMultiaddr, invalidArgument, invalidFingerprint, unsupportedHashAlgorithm } from './error.js'
7
+ import { CERTHASH_CODE } from './transport.js'
8
+
9
+ const log = logger('libp2p:webrtc:sdp')
10
+
11
+ /**
12
+ * Get base2 | identity decoders
13
+ */
14
+ export const mbdecoder: any = (function () {
15
+ const decoders = Object.values(bases).map((b) => b.decoder)
16
+ let acc = decoders[0].or(decoders[1])
17
+ decoders.slice(2).forEach((d) => (acc = acc.or(d)))
18
+ return acc
19
+ })()
20
+
21
+ /**
22
+ * Get base2 | identity decoders
23
+ */
24
+ function ipv (ma: Multiaddr): string {
25
+ for (const proto of ma.protoNames()) {
26
+ if (proto.startsWith('ip')) {
27
+ return proto.toUpperCase()
28
+ }
29
+ }
30
+
31
+ log('Warning: multiaddr does not appear to contain IP4 or IP6, defaulting to IP6', ma)
32
+
33
+ return 'IP6'
34
+ }
35
+
36
+ // Extract the certhash from a multiaddr
37
+ export function certhash (ma: Multiaddr): string {
38
+ const tups = ma.stringTuples()
39
+ const certhash = tups.filter((tup) => tup[0] === CERTHASH_CODE).map((tup) => tup[1])[0]
40
+
41
+ if (certhash === undefined || certhash === '') {
42
+ throw inappropriateMultiaddr(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`)
43
+ }
44
+
45
+ return certhash
46
+ }
47
+
48
+ /**
49
+ * Convert a certhash into a multihash
50
+ */
51
+ export function decodeCerthash (certhash: string) {
52
+ const mbdecoded = mbdecoder.decode(certhash)
53
+ return multihashes.decode(mbdecoded)
54
+ }
55
+
56
+ /**
57
+ * Extract the fingerprint from a multiaddr
58
+ */
59
+ export function ma2Fingerprint (ma: Multiaddr): string[] {
60
+ // certhash_value is a multibase encoded multihash encoded string
61
+ const mhdecoded = decodeCerthash(certhash(ma))
62
+ const prefix = toSupportedHashFunction(mhdecoded.name)
63
+ const fingerprint = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')
64
+ const sdp = fingerprint.match(/.{1,2}/g)
65
+
66
+ if (sdp == null) {
67
+ throw invalidFingerprint(fingerprint, ma.toString())
68
+ }
69
+
70
+ return [`${prefix.toUpperCase()} ${sdp.join(':').toUpperCase()}`, fingerprint]
71
+ }
72
+
73
+ /**
74
+ * Normalize the hash name from a given multihash has name
75
+ */
76
+ export function toSupportedHashFunction (name: multihashes.HashName): string {
77
+ switch (name) {
78
+ case 'sha1':
79
+ return 'sha-1'
80
+ case 'sha2-256':
81
+ return 'sha-256'
82
+ case 'sha2-512':
83
+ return 'sha-512'
84
+ default:
85
+ throw unsupportedHashAlgorithm(name)
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Convert a multiaddr into a SDP
91
+ */
92
+ function ma2sdp (ma: Multiaddr, ufrag: string): string {
93
+ const { host, port } = ma.toOptions()
94
+ const ipVersion = ipv(ma)
95
+ const [CERTFP] = ma2Fingerprint(ma)
96
+
97
+ return `v=0
98
+ o=- 0 0 IN ${ipVersion} ${host}
99
+ s=-
100
+ c=IN ${ipVersion} ${host}
101
+ t=0 0
102
+ a=ice-lite
103
+ m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
104
+ a=mid:0
105
+ a=setup:passive
106
+ a=ice-ufrag:${ufrag}
107
+ a=ice-pwd:${ufrag}
108
+ a=fingerprint:${CERTFP}
109
+ a=sctp-port:5000
110
+ a=max-message-size:100000
111
+ a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host\r\n`
112
+ }
113
+
114
+ /**
115
+ * Create an answer SDP from a multiaddr
116
+ */
117
+ export function fromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
118
+ return {
119
+ type: 'answer',
120
+ sdp: ma2sdp(ma, ufrag)
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Replace (munge) the ufrag and password values in a SDP
126
+ */
127
+ export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit {
128
+ if (desc.sdp === undefined) {
129
+ throw invalidArgument("Can't munge a missing SDP")
130
+ }
131
+
132
+ desc.sdp = desc.sdp
133
+ .replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n')
134
+ .replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n')
135
+ return desc
136
+ }
package/src/stream.ts ADDED
@@ -0,0 +1,422 @@
1
+ import { Stream, StreamStat, Direction } from '@libp2p/interface-connection'
2
+ import { logger } from '@libp2p/logger'
3
+ import * as lengthPrefixed from 'it-length-prefixed'
4
+ import merge from 'it-merge'
5
+ import { pipe } from 'it-pipe'
6
+ import { pushable } from 'it-pushable'
7
+ import defer, { DeferredPromise } from 'p-defer'
8
+ import { Source, Sink } from 'it-stream-types'
9
+ import { Uint8ArrayList } from 'uint8arraylist'
10
+
11
+ import * as pb from '../proto_ts/message.js'
12
+
13
+ const log = logger('libp2p:webrtc:stream')
14
+
15
+ /**
16
+ * Constructs a default StreamStat
17
+ */
18
+ export function defaultStat (dir: Direction): StreamStat {
19
+ return {
20
+ direction: dir,
21
+ timeline: {
22
+ open: 0,
23
+ close: undefined
24
+ }
25
+ }
26
+ }
27
+
28
+ interface StreamInitOpts {
29
+ /**
30
+ * The network channel used for bidirectional peer-to-peer transfers of
31
+ * arbitrary data
32
+ *
33
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel}
34
+ */
35
+ channel: RTCDataChannel
36
+
37
+ /**
38
+ * User defined stream metadata
39
+ */
40
+ metadata?: Record<string, any>
41
+
42
+ /**
43
+ * Stats about this stream
44
+ */
45
+ stat: StreamStat
46
+
47
+ /**
48
+ * Callback to invoke when the stream is closed.
49
+ */
50
+ closeCb?: (stream: WebRTCStream) => void
51
+ }
52
+
53
+ /*
54
+ * State transitions for a stream
55
+ */
56
+ interface StreamStateInput {
57
+ /**
58
+ * Outbound conections are opened by the local node, inbound streams are
59
+ * opened by the remote
60
+ */
61
+ direction: 'inbound' | 'outbound'
62
+
63
+ /**
64
+ * Message flag from the protobuffs
65
+ *
66
+ * 0 = FIN
67
+ * 1 = STOP_SENDING
68
+ * 2 = RESET
69
+ */
70
+ flag: pb.Message_Flag
71
+ }
72
+
73
+ export enum StreamStates {
74
+ OPEN,
75
+ READ_CLOSED,
76
+ WRITE_CLOSED,
77
+ CLOSED,
78
+ }
79
+
80
+ class StreamState {
81
+ state: StreamStates = StreamStates.OPEN
82
+
83
+ transition ({ direction, flag }: StreamStateInput): [StreamStates, StreamStates] {
84
+ const prev = this.state
85
+
86
+ // return early if the stream is closed
87
+ if (this.state === StreamStates.CLOSED) {
88
+ return [prev, StreamStates.CLOSED]
89
+ }
90
+
91
+ if (direction === 'inbound') {
92
+ switch (flag) {
93
+ case pb.Message_Flag.FIN:
94
+ if (this.state === StreamStates.OPEN) {
95
+ this.state = StreamStates.READ_CLOSED
96
+ } else if (this.state === StreamStates.WRITE_CLOSED) {
97
+ this.state = StreamStates.CLOSED
98
+ }
99
+ break
100
+
101
+ case pb.Message_Flag.STOP_SENDING:
102
+ if (this.state === StreamStates.OPEN) {
103
+ this.state = StreamStates.WRITE_CLOSED
104
+ } else if (this.state === StreamStates.READ_CLOSED) {
105
+ this.state = StreamStates.CLOSED
106
+ }
107
+ break
108
+
109
+ case pb.Message_Flag.RESET:
110
+ this.state = StreamStates.CLOSED
111
+ break
112
+
113
+ // no default
114
+ }
115
+ } else {
116
+ switch (flag) {
117
+ case pb.Message_Flag.FIN:
118
+ if (this.state === StreamStates.OPEN) {
119
+ this.state = StreamStates.WRITE_CLOSED
120
+ } else if (this.state === StreamStates.READ_CLOSED) {
121
+ this.state = StreamStates.CLOSED
122
+ }
123
+ break
124
+
125
+ case pb.Message_Flag.STOP_SENDING:
126
+ if (this.state === StreamStates.OPEN) {
127
+ this.state = StreamStates.READ_CLOSED
128
+ } else if (this.state === StreamStates.WRITE_CLOSED) {
129
+ this.state = StreamStates.CLOSED
130
+ }
131
+ break
132
+
133
+ case pb.Message_Flag.RESET:
134
+ this.state = StreamStates.CLOSED
135
+ break
136
+
137
+ // no default
138
+ }
139
+ }
140
+ return [prev, this.state]
141
+ }
142
+ }
143
+
144
+ export class WebRTCStream implements Stream {
145
+ /**
146
+ * Unique identifier for a stream
147
+ */
148
+ id: string;
149
+
150
+ /**
151
+ * Stats about this stream
152
+ */
153
+ stat: StreamStat;
154
+
155
+ /**
156
+ * User defined stream metadata
157
+ */
158
+ metadata: Record<string, any>;
159
+
160
+ /**
161
+ * The data channel used to send and receive data
162
+ */
163
+ private readonly channel: RTCDataChannel;
164
+
165
+ /**
166
+ * The current state of the stream
167
+ */
168
+ streamState = new StreamState();
169
+
170
+ /**
171
+ * Read unwrapped protobuf data from the underlying datachannel.
172
+ * _src is exposed to the user via the `source` getter to .
173
+ */
174
+ private readonly _src: Source<Uint8ArrayList>;
175
+
176
+ /**
177
+ * push data from the underlying datachannel to the length prefix decoder
178
+ * and then the protobuf decoder.
179
+ */
180
+ private readonly _innersrc = pushable();
181
+
182
+ /**
183
+ * Write data to the remote peer.
184
+ * It takes care of wrapping data in a protobuf and adding the length prefix.
185
+ */
186
+ sink: Sink<Uint8ArrayList | Uint8Array, Promise<void>>;
187
+
188
+ /**
189
+ * Deferred promise that resolves when the underlying datachannel is in the
190
+ * open state.
191
+ */
192
+ opened: DeferredPromise<void> = defer();
193
+
194
+ /**
195
+ * Triggers a generator which can be used to close the sink.
196
+ */
197
+ closeWritePromise: DeferredPromise<void> = defer();
198
+
199
+ /**
200
+ * Callback to invoke when the stream is closed.
201
+ */
202
+ closeCb?: (stream: WebRTCStream) => void
203
+
204
+ constructor (opts: StreamInitOpts) {
205
+ this.channel = opts.channel
206
+ this.id = this.channel.label
207
+
208
+ this.stat = opts.stat
209
+ switch (this.channel.readyState) {
210
+ case 'open':
211
+ this.opened.resolve()
212
+ break
213
+
214
+ case 'closed':
215
+ case 'closing':
216
+ this.streamState.state = StreamStates.CLOSED
217
+ if (this.stat.timeline.close === undefined || this.stat.timeline.close === 0) {
218
+ this.stat.timeline.close = new Date().getTime()
219
+ }
220
+ this.opened.resolve()
221
+ break
222
+
223
+ // no default
224
+ }
225
+
226
+ this.metadata = opts.metadata ?? {}
227
+
228
+ // closable sink
229
+ this.sink = this._sinkFn
230
+
231
+ // handle RTCDataChannel events
232
+ this.channel.onopen = (_evt) => {
233
+ this.stat.timeline.open = new Date().getTime()
234
+ this.opened.resolve()
235
+ }
236
+
237
+ this.channel.onclose = (_evt) => {
238
+ this.close()
239
+ }
240
+
241
+ this.channel.onerror = (evt) => {
242
+ const err = (evt as RTCErrorEvent).error
243
+ this.abort(err)
244
+ }
245
+
246
+ const self = this
247
+
248
+ // reader pipe
249
+ this.channel.onmessage = async ({ data }) => {
250
+ if (data === null || data.length === 0) {
251
+ return
252
+ }
253
+ this._innersrc.push(new Uint8Array(data as ArrayBufferLike))
254
+ }
255
+
256
+ // pipe framed protobuf messages through a length prefixed decoder, and
257
+ // surface data from the `Message.message` field through a source.
258
+ this._src = pipe(
259
+ this._innersrc,
260
+ lengthPrefixed.decode(),
261
+ (source) => (async function * () {
262
+ for await (const buf of source) {
263
+ const message = self.processIncomingProtobuf(buf.subarray())
264
+ if (message != null) {
265
+ yield new Uint8ArrayList(message)
266
+ }
267
+ }
268
+ })()
269
+ )
270
+ }
271
+
272
+ // If user attempts to set a new source this should be a noop
273
+ set source (_src: Source<Uint8ArrayList>) { }
274
+
275
+ get source (): Source<Uint8ArrayList> {
276
+ return this._src
277
+ }
278
+
279
+ /**
280
+ * Closable sink
281
+ */
282
+ private async _sinkFn (src: Source<Uint8ArrayList | Uint8Array>): Promise<void> {
283
+ await this.opened.promise
284
+
285
+ const isClosed = (state: StreamStates) => state === StreamStates.CLOSED || state === StreamStates.WRITE_CLOSED
286
+
287
+ if (isClosed(this.streamState.state)) {
288
+ return
289
+ }
290
+
291
+ const self = this
292
+ const closeWriteIterable = {
293
+ async * [Symbol.asyncIterator] () {
294
+ await self.closeWritePromise.promise
295
+ yield new Uint8Array(0)
296
+ }
297
+ }
298
+
299
+ for await (const buf of merge(closeWriteIterable, src)) {
300
+ if (isClosed(self.streamState.state)) {
301
+ return
302
+ }
303
+
304
+ const msgbuf = pb.Message.toBinary({ message: buf.subarray() })
305
+ const sendbuf = lengthPrefixed.encode.single(msgbuf)
306
+
307
+ this.channel.send(sendbuf.subarray())
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Handle incoming
313
+ */
314
+ processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined {
315
+ const message = pb.Message.fromBinary(buffer)
316
+
317
+ if (message.flag !== undefined) {
318
+ const [currentState, nextState] = this.streamState.transition({ direction: 'inbound', flag: message.flag })
319
+
320
+ if (currentState !== nextState) {
321
+ // @TODO(ddimaria): determine if we need to check for StreamStates.OPEN
322
+ switch (nextState) {
323
+ case StreamStates.READ_CLOSED:
324
+ this._innersrc.end()
325
+ break
326
+ case StreamStates.WRITE_CLOSED:
327
+ this.closeWritePromise.resolve()
328
+ break
329
+ case StreamStates.CLOSED:
330
+ this.close()
331
+ break
332
+
333
+ // no default
334
+ }
335
+ }
336
+ }
337
+
338
+ return message.message
339
+ }
340
+
341
+ /**
342
+ * Close a stream for reading and writing
343
+ */
344
+ close (): void {
345
+ this.stat.timeline.close = new Date().getTime()
346
+ this.streamState.state = StreamStates.CLOSED
347
+ this._innersrc.end()
348
+ this.closeWritePromise.resolve()
349
+ this.channel.close()
350
+
351
+ if (this.closeCb !== undefined) {
352
+ this.closeCb(this)
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Close a stream for reading only
358
+ */
359
+ closeRead (): void {
360
+ const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.STOP_SENDING })
361
+
362
+ if (currentState === StreamStates.OPEN || currentState === StreamStates.WRITE_CLOSED) {
363
+ this._sendFlag(pb.Message_Flag.STOP_SENDING);
364
+ (this._innersrc).end()
365
+ }
366
+
367
+ if (currentState !== nextState && nextState === StreamStates.CLOSED) {
368
+ this.close()
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Close a stream for writing only
374
+ */
375
+ closeWrite (): void {
376
+ const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.FIN })
377
+
378
+ if (currentState === StreamStates.OPEN || currentState === StreamStates.READ_CLOSED) {
379
+ this._sendFlag(pb.Message_Flag.FIN)
380
+ this.closeWritePromise.resolve()
381
+ }
382
+
383
+ if (currentState !== nextState && nextState === StreamStates.CLOSED) {
384
+ this.close()
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Call when a local error occurs, should close the stream for reading and writing
390
+ */
391
+ abort (err: Error): void {
392
+ log.error(`An error occurred, clost the stream for reading and writing: ${err.message}`)
393
+ this.close()
394
+ }
395
+
396
+ /**
397
+ * Close the stream for writing, and indicate to the remote side this is being done 'abruptly'
398
+ *
399
+ * @see closeWrite
400
+ */
401
+ reset (): void {
402
+ this.stat = defaultStat(this.stat.direction)
403
+ const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.RESET })
404
+
405
+ if (currentState !== nextState) {
406
+ this._sendFlag(pb.Message_Flag.RESET)
407
+ this.close()
408
+ }
409
+ }
410
+
411
+ private _sendFlag (flag: pb.Message_Flag): void {
412
+ try {
413
+ log.trace('Sending flag: %s', flag.toString())
414
+ const msgbuf = pb.Message.toBinary({ flag: flag })
415
+ this.channel.send(lengthPrefixed.encode.single(msgbuf).subarray())
416
+ } catch (err) {
417
+ if (err instanceof Error) {
418
+ log.error(`Exception while sending flag ${flag}: ${err.message}`)
419
+ }
420
+ }
421
+ }
422
+ }