@libp2p/webrtc 3.2.1 → 3.2.2-62a56b54
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.
- package/dist/index.min.js +13 -13
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/maconn.d.ts.map +1 -1
- package/dist/src/maconn.js +5 -2
- package/dist/src/maconn.js.map +1 -1
- package/dist/src/muxer.d.ts +10 -13
- package/dist/src/muxer.d.ts.map +1 -1
- package/dist/src/muxer.js +44 -29
- package/dist/src/muxer.js.map +1 -1
- package/dist/src/pb/message.d.ts +2 -1
- package/dist/src/pb/message.d.ts.map +1 -1
- package/dist/src/pb/message.js +2 -0
- package/dist/src/pb/message.js.map +1 -1
- package/dist/src/private-to-private/initiate-connection.d.ts +25 -0
- package/dist/src/private-to-private/initiate-connection.d.ts.map +1 -0
- package/dist/src/private-to-private/initiate-connection.js +145 -0
- package/dist/src/private-to-private/initiate-connection.js.map +1 -0
- package/dist/src/private-to-private/listener.d.ts +6 -2
- package/dist/src/private-to-private/listener.d.ts.map +1 -1
- package/dist/src/private-to-private/listener.js +6 -3
- package/dist/src/private-to-private/listener.js.map +1 -1
- package/dist/src/private-to-private/signaling-stream-handler.d.ts +10 -0
- package/dist/src/private-to-private/signaling-stream-handler.d.ts.map +1 -0
- package/dist/src/private-to-private/signaling-stream-handler.js +97 -0
- package/dist/src/private-to-private/signaling-stream-handler.js.map +1 -0
- package/dist/src/private-to-private/transport.d.ts +12 -2
- package/dist/src/private-to-private/transport.d.ts.map +1 -1
- package/dist/src/private-to-private/transport.js +67 -56
- package/dist/src/private-to-private/transport.js.map +1 -1
- package/dist/src/private-to-private/util.d.ts +6 -5
- package/dist/src/private-to-private/util.d.ts.map +1 -1
- package/dist/src/private-to-private/util.js +72 -21
- package/dist/src/private-to-private/util.js.map +1 -1
- package/dist/src/private-to-public/transport.d.ts +2 -2
- package/dist/src/private-to-public/transport.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.js +2 -2
- package/dist/src/private-to-public/transport.js.map +1 -1
- package/dist/src/stream.d.ts +39 -19
- package/dist/src/stream.d.ts.map +1 -1
- package/dist/src/stream.js +135 -39
- package/dist/src/stream.js.map +1 -1
- package/dist/src/util.d.ts +6 -0
- package/dist/src/util.d.ts.map +1 -1
- package/dist/src/util.js +46 -0
- package/dist/src/util.js.map +1 -1
- package/package.json +17 -11
- package/src/index.ts +34 -0
- package/src/maconn.ts +7 -2
- package/src/muxer.ts +58 -44
- package/src/pb/message.proto +6 -1
- package/src/pb/message.ts +4 -2
- package/src/private-to-private/initiate-connection.ts +191 -0
- package/src/private-to-private/listener.ts +12 -4
- package/src/private-to-private/signaling-stream-handler.ts +129 -0
- package/src/private-to-private/transport.ts +87 -59
- package/src/private-to-private/util.ts +89 -24
- package/src/private-to-public/transport.ts +4 -4
- package/src/stream.ts +163 -61
- package/src/util.ts +60 -0
- package/dist/src/private-to-private/handler.d.ts +0 -26
- package/dist/src/private-to-private/handler.d.ts.map +0 -1
- package/dist/src/private-to-private/handler.js +0 -137
- package/dist/src/private-to-private/handler.js.map +0 -1
- package/dist/typedoc-urls.json +0 -6
- package/src/private-to-private/handler.ts +0 -177
package/src/index.ts
CHANGED
|
@@ -3,6 +3,40 @@ import { WebRTCDirectTransport, type WebRTCTransportDirectInit, type WebRTCDirec
|
|
|
3
3
|
import type { WebRTCTransportComponents, WebRTCTransportInit } from './private-to-private/transport.js'
|
|
4
4
|
import type { Transport } from '@libp2p/interface/transport'
|
|
5
5
|
|
|
6
|
+
export interface DataChannelOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The maximum message size sendable over the channel in bytes (default 16KB)
|
|
9
|
+
*/
|
|
10
|
+
maxMessageSize?: number
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* If the channel's `bufferedAmount` grows over this amount in bytes, wait
|
|
14
|
+
* for it to drain before sending more data (default: 16MB)
|
|
15
|
+
*/
|
|
16
|
+
maxBufferedAmount?: number
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* When `bufferedAmount` is above `maxBufferedAmount`, we pause sending until
|
|
20
|
+
* the `bufferedAmountLow` event fires - this controls how long we wait for
|
|
21
|
+
* that event in ms (default: 30s)
|
|
22
|
+
*/
|
|
23
|
+
bufferedAmountLowEventTimeout?: number
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* When closing a stream, we wait for `bufferedAmount` to become 0 before
|
|
27
|
+
* closing the underlying RTCDataChannel - this controls how long we wait
|
|
28
|
+
* in ms (default: 30s)
|
|
29
|
+
*/
|
|
30
|
+
drainTimeout?: number
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* When closing a stream we first send a FIN flag to the remote and wait
|
|
34
|
+
* for a FIN_ACK reply before closing the underlying RTCDataChannel - this
|
|
35
|
+
* controls how long we wait for the acknowledgement in ms (default: 5s)
|
|
36
|
+
*/
|
|
37
|
+
closeTimeout?: number
|
|
38
|
+
}
|
|
39
|
+
|
|
6
40
|
/**
|
|
7
41
|
* @param {WebRTCTransportDirectInit} init - WebRTC direct transport configuration
|
|
8
42
|
* @param init.dataChannel - DataChannel configurations
|
package/src/maconn.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { CounterGroup } from '@libp2p/interface/metrics'
|
|
|
5
5
|
import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr'
|
|
6
6
|
import type { Source, Sink } from 'it-stream-types'
|
|
7
7
|
|
|
8
|
-
const log = logger('libp2p:webrtc:
|
|
8
|
+
const log = logger('libp2p:webrtc:maconn')
|
|
9
9
|
|
|
10
10
|
interface WebRTCMultiaddrConnectionInit {
|
|
11
11
|
/**
|
|
@@ -65,8 +65,13 @@ export class WebRTCMultiaddrConnection implements MultiaddrConnection {
|
|
|
65
65
|
this.timeline = init.timeline
|
|
66
66
|
this.peerConnection = init.peerConnection
|
|
67
67
|
|
|
68
|
+
const initialState = this.peerConnection.connectionState
|
|
69
|
+
|
|
68
70
|
this.peerConnection.onconnectionstatechange = () => {
|
|
69
|
-
|
|
71
|
+
log.trace('peer connection state change', this.peerConnection.connectionState, 'initial state', initialState)
|
|
72
|
+
|
|
73
|
+
if (this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed' || this.peerConnection.connectionState === 'closed') {
|
|
74
|
+
// nothing else to do but close the connection
|
|
70
75
|
this.timeline.close = Date.now()
|
|
71
76
|
}
|
|
72
77
|
}
|
package/src/muxer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { logger } from '@libp2p/logger'
|
|
1
2
|
import { createStream } from './stream.js'
|
|
2
|
-
import { nopSink, nopSource } from './util.js'
|
|
3
|
-
import type {
|
|
3
|
+
import { drainAndClose, nopSink, nopSource } from './util.js'
|
|
4
|
+
import type { DataChannelOptions } from './index.js'
|
|
4
5
|
import type { Stream } from '@libp2p/interface/connection'
|
|
5
6
|
import type { CounterGroup } from '@libp2p/interface/metrics'
|
|
6
7
|
import type { StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface/stream-muxer'
|
|
@@ -8,6 +9,8 @@ import type { AbortOptions } from '@multiformats/multiaddr'
|
|
|
8
9
|
import type { Source, Sink } from 'it-stream-types'
|
|
9
10
|
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
10
11
|
|
|
12
|
+
const log = logger('libp2p:webrtc:muxer')
|
|
13
|
+
|
|
11
14
|
const PROTOCOL = '/webrtc'
|
|
12
15
|
|
|
13
16
|
export interface DataChannelMuxerFactoryInit {
|
|
@@ -17,19 +20,16 @@ export interface DataChannelMuxerFactoryInit {
|
|
|
17
20
|
peerConnection: RTCPeerConnection
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
23
|
+
* The protocol to use
|
|
21
24
|
*/
|
|
22
|
-
|
|
25
|
+
protocol?: string
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
|
-
*
|
|
28
|
+
* Optional metrics for this data channel muxer
|
|
26
29
|
*/
|
|
27
|
-
|
|
30
|
+
metrics?: CounterGroup
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
* The protocol to use
|
|
31
|
-
*/
|
|
32
|
-
protocol?: string
|
|
32
|
+
dataChannelOptions?: DataChannelOptions
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export class DataChannelMuxerFactory implements StreamMuxerFactory {
|
|
@@ -41,23 +41,23 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory {
|
|
|
41
41
|
private readonly peerConnection: RTCPeerConnection
|
|
42
42
|
private streamBuffer: Stream[] = []
|
|
43
43
|
private readonly metrics?: CounterGroup
|
|
44
|
-
private readonly dataChannelOptions?:
|
|
44
|
+
private readonly dataChannelOptions?: DataChannelOptions
|
|
45
45
|
|
|
46
46
|
constructor (init: DataChannelMuxerFactoryInit) {
|
|
47
47
|
this.peerConnection = init.peerConnection
|
|
48
48
|
this.metrics = init.metrics
|
|
49
49
|
this.protocol = init.protocol ?? PROTOCOL
|
|
50
|
-
this.dataChannelOptions = init.dataChannelOptions
|
|
50
|
+
this.dataChannelOptions = init.dataChannelOptions ?? {}
|
|
51
51
|
|
|
52
52
|
// store any datachannels opened before upgrade has been completed
|
|
53
53
|
this.peerConnection.ondatachannel = ({ channel }) => {
|
|
54
54
|
const stream = createStream({
|
|
55
55
|
channel,
|
|
56
56
|
direction: 'inbound',
|
|
57
|
-
dataChannelOptions: init.dataChannelOptions,
|
|
58
57
|
onEnd: () => {
|
|
59
58
|
this.streamBuffer = this.streamBuffer.filter(s => s.id !== stream.id)
|
|
60
|
-
}
|
|
59
|
+
},
|
|
60
|
+
...this.dataChannelOptions
|
|
61
61
|
})
|
|
62
62
|
this.streamBuffer.push(stream)
|
|
63
63
|
}
|
|
@@ -90,34 +90,15 @@ export class DataChannelMuxer implements StreamMuxer {
|
|
|
90
90
|
public protocol: string
|
|
91
91
|
|
|
92
92
|
private readonly peerConnection: RTCPeerConnection
|
|
93
|
-
private readonly dataChannelOptions
|
|
93
|
+
private readonly dataChannelOptions: DataChannelOptions
|
|
94
94
|
private readonly metrics?: CounterGroup
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
* Gracefully close all tracked streams and stop the muxer
|
|
98
|
-
*/
|
|
99
|
-
close: (options?: AbortOptions) => Promise<void> = async () => { }
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Abort all tracked streams and stop the muxer
|
|
103
|
-
*/
|
|
104
|
-
abort: (err: Error) => void = () => { }
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The stream source, a no-op as the transport natively supports multiplexing
|
|
108
|
-
*/
|
|
109
|
-
source: AsyncGenerator<Uint8Array, any, unknown> = nopSource()
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* The stream destination, a no-op as the transport natively supports multiplexing
|
|
113
|
-
*/
|
|
114
|
-
sink: Sink<Source<Uint8Array | Uint8ArrayList>, Promise<void>> = nopSink
|
|
115
|
-
|
|
116
96
|
constructor (readonly init: DataChannelMuxerInit) {
|
|
117
97
|
this.streams = init.streams
|
|
118
98
|
this.peerConnection = init.peerConnection
|
|
119
99
|
this.protocol = init.protocol ?? PROTOCOL
|
|
120
100
|
this.metrics = init.metrics
|
|
101
|
+
this.dataChannelOptions = init.dataChannelOptions ?? {}
|
|
121
102
|
|
|
122
103
|
/**
|
|
123
104
|
* Fired when a data channel has been added to the connection has been
|
|
@@ -129,19 +110,19 @@ export class DataChannelMuxer implements StreamMuxer {
|
|
|
129
110
|
const stream = createStream({
|
|
130
111
|
channel,
|
|
131
112
|
direction: 'inbound',
|
|
132
|
-
dataChannelOptions: this.dataChannelOptions,
|
|
133
113
|
onEnd: () => {
|
|
114
|
+
log.trace('stream %s %s %s onEnd', stream.direction, stream.id, stream.protocol)
|
|
115
|
+
drainAndClose(channel, `inbound ${stream.id} ${stream.protocol}`, this.dataChannelOptions.drainTimeout)
|
|
134
116
|
this.streams = this.streams.filter(s => s.id !== stream.id)
|
|
135
117
|
this.metrics?.increment({ stream_end: true })
|
|
136
118
|
init?.onStreamEnd?.(stream)
|
|
137
|
-
}
|
|
119
|
+
},
|
|
120
|
+
...this.dataChannelOptions
|
|
138
121
|
})
|
|
139
122
|
|
|
140
123
|
this.streams.push(stream)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
init.onIncomingStream(stream)
|
|
144
|
-
}
|
|
124
|
+
this.metrics?.increment({ incoming_stream: true })
|
|
125
|
+
init?.onIncomingStream?.(stream)
|
|
145
126
|
}
|
|
146
127
|
|
|
147
128
|
const onIncomingStream = init?.onIncomingStream
|
|
@@ -150,19 +131,52 @@ export class DataChannelMuxer implements StreamMuxer {
|
|
|
150
131
|
}
|
|
151
132
|
}
|
|
152
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Gracefully close all tracked streams and stop the muxer
|
|
136
|
+
*/
|
|
137
|
+
async close (options?: AbortOptions): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
await Promise.all(
|
|
140
|
+
this.streams.map(async stream => stream.close(options))
|
|
141
|
+
)
|
|
142
|
+
} catch (err: any) {
|
|
143
|
+
this.abort(err)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Abort all tracked streams and stop the muxer
|
|
149
|
+
*/
|
|
150
|
+
abort (err: Error): void {
|
|
151
|
+
for (const stream of this.streams) {
|
|
152
|
+
stream.abort(err)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The stream source, a no-op as the transport natively supports multiplexing
|
|
158
|
+
*/
|
|
159
|
+
source: AsyncGenerator<Uint8Array, any, unknown> = nopSource()
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The stream destination, a no-op as the transport natively supports multiplexing
|
|
163
|
+
*/
|
|
164
|
+
sink: Sink<Source<Uint8Array | Uint8ArrayList>, Promise<void>> = nopSink
|
|
165
|
+
|
|
153
166
|
newStream (): Stream {
|
|
154
167
|
// The spec says the label SHOULD be an empty string: https://github.com/libp2p/specs/blob/master/webrtc/README.md#rtcdatachannel-label
|
|
155
168
|
const channel = this.peerConnection.createDataChannel('')
|
|
156
169
|
const stream = createStream({
|
|
157
170
|
channel,
|
|
158
171
|
direction: 'outbound',
|
|
159
|
-
dataChannelOptions: this.dataChannelOptions,
|
|
160
172
|
onEnd: () => {
|
|
161
|
-
|
|
173
|
+
log.trace('stream %s %s %s onEnd', stream.direction, stream.id, stream.protocol)
|
|
174
|
+
drainAndClose(channel, `outbound ${stream.id} ${stream.protocol}`, this.dataChannelOptions.drainTimeout)
|
|
162
175
|
this.streams = this.streams.filter(s => s.id !== stream.id)
|
|
163
176
|
this.metrics?.increment({ stream_end: true })
|
|
164
177
|
this.init?.onStreamEnd?.(stream)
|
|
165
|
-
}
|
|
178
|
+
},
|
|
179
|
+
...this.dataChannelOptions
|
|
166
180
|
})
|
|
167
181
|
this.streams.push(stream)
|
|
168
182
|
this.metrics?.increment({ outgoing_stream: true })
|
package/src/pb/message.proto
CHANGED
|
@@ -2,7 +2,8 @@ syntax = "proto3";
|
|
|
2
2
|
|
|
3
3
|
message Message {
|
|
4
4
|
enum Flag {
|
|
5
|
-
// The sender will no longer send messages on the stream.
|
|
5
|
+
// The sender will no longer send messages on the stream. The recipient
|
|
6
|
+
// should send a FIN_ACK back to the sender.
|
|
6
7
|
FIN = 0;
|
|
7
8
|
|
|
8
9
|
// The sender will no longer read messages on the stream. Incoming data is
|
|
@@ -12,6 +13,10 @@ message Message {
|
|
|
12
13
|
// The sender abruptly terminates the sending part of the stream. The
|
|
13
14
|
// receiver can discard any data that it already received on that stream.
|
|
14
15
|
RESET = 2;
|
|
16
|
+
|
|
17
|
+
// The sender previously received a FIN.
|
|
18
|
+
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=1484907
|
|
19
|
+
FIN_ACK = 3;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
optional Flag flag = 1;
|
package/src/pb/message.ts
CHANGED
|
@@ -17,13 +17,15 @@ export namespace Message {
|
|
|
17
17
|
export enum Flag {
|
|
18
18
|
FIN = 'FIN',
|
|
19
19
|
STOP_SENDING = 'STOP_SENDING',
|
|
20
|
-
RESET = 'RESET'
|
|
20
|
+
RESET = 'RESET',
|
|
21
|
+
FIN_ACK = 'FIN_ACK'
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
enum __FlagValues {
|
|
24
25
|
FIN = 0,
|
|
25
26
|
STOP_SENDING = 1,
|
|
26
|
-
RESET = 2
|
|
27
|
+
RESET = 2,
|
|
28
|
+
FIN_ACK = 3
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export namespace Flag {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { CodeError } from '@libp2p/interface/errors'
|
|
2
|
+
import { logger } from '@libp2p/logger'
|
|
3
|
+
import { peerIdFromString } from '@libp2p/peer-id'
|
|
4
|
+
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'
|
|
5
|
+
import { pbStream } from 'it-protobuf-stream'
|
|
6
|
+
import pDefer, { type DeferredPromise } from 'p-defer'
|
|
7
|
+
import { type RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js'
|
|
8
|
+
import { Message } from './pb/message.js'
|
|
9
|
+
import { SIGNALING_PROTO_ID, splitAddr, type WebRTCTransportMetrics } from './transport.js'
|
|
10
|
+
import { parseRemoteAddress, readCandidatesUntilConnected, resolveOnConnected } from './util.js'
|
|
11
|
+
import type { DataChannelOptions } from '../index.js'
|
|
12
|
+
import type { Connection } from '@libp2p/interface/connection'
|
|
13
|
+
import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager'
|
|
14
|
+
import type { IncomingStreamData } from '@libp2p/interface-internal/registrar'
|
|
15
|
+
import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
|
|
16
|
+
|
|
17
|
+
const log = logger('libp2p:webrtc:initiate-connection')
|
|
18
|
+
|
|
19
|
+
export interface IncomingStreamOpts extends IncomingStreamData {
|
|
20
|
+
rtcConfiguration?: RTCConfiguration
|
|
21
|
+
dataChannelOptions?: Partial<DataChannelOptions>
|
|
22
|
+
signal: AbortSignal
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ConnectOptions {
|
|
26
|
+
peerConnection: RTCPeerConnection
|
|
27
|
+
multiaddr: Multiaddr
|
|
28
|
+
connectionManager: ConnectionManager
|
|
29
|
+
transportManager: TransportManager
|
|
30
|
+
dataChannelOptions?: Partial<DataChannelOptions>
|
|
31
|
+
signal?: AbortSignal
|
|
32
|
+
metrics?: WebRTCTransportMetrics
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function initiateConnection ({ peerConnection, signal, metrics, multiaddr: ma, connectionManager, transportManager }: ConnectOptions): Promise<{ remoteAddress: Multiaddr }> {
|
|
36
|
+
const { baseAddr, peerId } = splitAddr(ma)
|
|
37
|
+
|
|
38
|
+
metrics?.dialerEvents.increment({ open: true })
|
|
39
|
+
|
|
40
|
+
log.trace('dialing base address: %a', baseAddr)
|
|
41
|
+
|
|
42
|
+
const relayPeer = baseAddr.getPeerId()
|
|
43
|
+
|
|
44
|
+
if (relayPeer == null) {
|
|
45
|
+
throw new CodeError('Relay peer was missing', 'ERR_INVALID_ADDRESS')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const connections = connectionManager.getConnections(peerIdFromString(relayPeer))
|
|
49
|
+
let connection: Connection
|
|
50
|
+
let shouldCloseConnection = false
|
|
51
|
+
|
|
52
|
+
if (connections.length === 0) {
|
|
53
|
+
// use the transport manager to open a connection. Initiating a WebRTC
|
|
54
|
+
// connection takes place in the context of a dial - if we use the
|
|
55
|
+
// connection manager instead we can end up joining our own dial context
|
|
56
|
+
connection = await transportManager.dial(baseAddr, {
|
|
57
|
+
signal
|
|
58
|
+
})
|
|
59
|
+
// this connection is unmanaged by the connection manager so we should
|
|
60
|
+
// close it when we are done
|
|
61
|
+
shouldCloseConnection = true
|
|
62
|
+
} else {
|
|
63
|
+
connection = connections[0]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const stream = await connection.newStream(SIGNALING_PROTO_ID, {
|
|
68
|
+
signal,
|
|
69
|
+
runOnTransientConnection: true
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const messageStream = pbStream(stream).pb(Message)
|
|
73
|
+
const connectedPromise: DeferredPromise<void> = pDefer()
|
|
74
|
+
const sdpAbortedListener = (): void => {
|
|
75
|
+
connectedPromise.reject(new CodeError('SDP handshake aborted', 'ERR_SDP_HANDSHAKE_ABORTED'))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
resolveOnConnected(peerConnection, connectedPromise)
|
|
80
|
+
|
|
81
|
+
// reject the connectedPromise if the signal aborts
|
|
82
|
+
signal?.addEventListener('abort', sdpAbortedListener)
|
|
83
|
+
|
|
84
|
+
// we create the channel so that the RTCPeerConnection has a component for
|
|
85
|
+
// which to collect candidates. The label is not relevant to connection
|
|
86
|
+
// initiation but can be useful for debugging
|
|
87
|
+
const channel = peerConnection.createDataChannel('init')
|
|
88
|
+
|
|
89
|
+
// setup callback to write ICE candidates to the remote peer
|
|
90
|
+
peerConnection.onicecandidate = ({ candidate }) => {
|
|
91
|
+
// a null candidate means end-of-candidates, an empty string candidate
|
|
92
|
+
// means end-of-candidates for this generation, otherwise this should
|
|
93
|
+
// be a valid candidate object
|
|
94
|
+
// see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent
|
|
95
|
+
const data = JSON.stringify(candidate?.toJSON() ?? null)
|
|
96
|
+
|
|
97
|
+
log.trace('initiator sending ICE candidate %s', data)
|
|
98
|
+
|
|
99
|
+
void messageStream.write({
|
|
100
|
+
type: Message.Type.ICE_CANDIDATE,
|
|
101
|
+
data
|
|
102
|
+
}, {
|
|
103
|
+
signal
|
|
104
|
+
})
|
|
105
|
+
.catch(err => {
|
|
106
|
+
log.error('error sending ICE candidate', err)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
peerConnection.onicecandidateerror = (event) => {
|
|
110
|
+
log('initiator ICE candidate error', event)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// create an offer
|
|
114
|
+
const offerSdp = await peerConnection.createOffer()
|
|
115
|
+
|
|
116
|
+
log.trace('initiator send SDP offer %s', offerSdp.sdp)
|
|
117
|
+
|
|
118
|
+
// write the offer to the stream
|
|
119
|
+
await messageStream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp }, {
|
|
120
|
+
signal
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// set offer as local description
|
|
124
|
+
await peerConnection.setLocalDescription(offerSdp).catch(err => {
|
|
125
|
+
log.error('could not execute setLocalDescription', err)
|
|
126
|
+
throw new CodeError('Failed to set localDescription', 'ERR_SDP_HANDSHAKE_FAILED')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// read answer
|
|
130
|
+
const answerMessage = await messageStream.read({
|
|
131
|
+
signal
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (answerMessage.type !== Message.Type.SDP_ANSWER) {
|
|
135
|
+
throw new CodeError('remote should send an SDP answer', 'ERR_SDP_HANDSHAKE_FAILED')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
log.trace('initiator receive SDP answer %s', answerMessage.data)
|
|
139
|
+
|
|
140
|
+
const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data })
|
|
141
|
+
await peerConnection.setRemoteDescription(answerSdp).catch(err => {
|
|
142
|
+
log.error('could not execute setRemoteDescription', err)
|
|
143
|
+
throw new CodeError('Failed to set remoteDescription', 'ERR_SDP_HANDSHAKE_FAILED')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
log.trace('initiator read candidates until connected')
|
|
147
|
+
|
|
148
|
+
await readCandidatesUntilConnected(connectedPromise, peerConnection, messageStream, {
|
|
149
|
+
direction: 'initiator',
|
|
150
|
+
signal
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
log.trace('initiator connected, closing init channel')
|
|
154
|
+
channel.close()
|
|
155
|
+
|
|
156
|
+
log.trace('initiator closing signalling stream')
|
|
157
|
+
await messageStream.unwrap().unwrap().close({
|
|
158
|
+
signal
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const remoteAddress = parseRemoteAddress(peerConnection.currentRemoteDescription?.sdp ?? '')
|
|
162
|
+
|
|
163
|
+
log.trace('initiator connected to remote address %s', remoteAddress)
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
remoteAddress: multiaddr(remoteAddress).encapsulate(`/p2p/${peerId.toString()}`)
|
|
167
|
+
}
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
peerConnection.close()
|
|
170
|
+
stream.abort(err)
|
|
171
|
+
throw err
|
|
172
|
+
} finally {
|
|
173
|
+
// remove event listeners
|
|
174
|
+
signal?.removeEventListener('abort', sdpAbortedListener)
|
|
175
|
+
peerConnection.onicecandidate = null
|
|
176
|
+
peerConnection.onicecandidateerror = null
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
// if we had to open a connection to perform the SDP handshake
|
|
180
|
+
// close it because it's not tracked by the connection manager
|
|
181
|
+
if (shouldCloseConnection) {
|
|
182
|
+
try {
|
|
183
|
+
await connection.close({
|
|
184
|
+
signal
|
|
185
|
+
})
|
|
186
|
+
} catch (err: any) {
|
|
187
|
+
connection.abort(err)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -5,20 +5,27 @@ import type { ListenerEvents, Listener } from '@libp2p/interface/transport'
|
|
|
5
5
|
import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
|
|
6
6
|
import type { Multiaddr } from '@multiformats/multiaddr'
|
|
7
7
|
|
|
8
|
-
export interface
|
|
8
|
+
export interface WebRTCPeerListenerComponents {
|
|
9
9
|
peerId: PeerId
|
|
10
10
|
transportManager: TransportManager
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface WebRTCPeerListenerInit {
|
|
14
|
+
shutdownController: AbortController
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
export class WebRTCPeerListener extends EventEmitter<ListenerEvents> implements Listener {
|
|
14
18
|
private readonly peerId: PeerId
|
|
15
19
|
private readonly transportManager: TransportManager
|
|
20
|
+
private readonly shutdownController: AbortController
|
|
16
21
|
|
|
17
|
-
constructor (
|
|
22
|
+
constructor (components: WebRTCPeerListenerComponents, init: WebRTCPeerListenerInit) {
|
|
18
23
|
super()
|
|
19
24
|
|
|
20
|
-
this.peerId =
|
|
21
|
-
this.transportManager =
|
|
25
|
+
this.peerId = components.peerId
|
|
26
|
+
this.transportManager = components.transportManager
|
|
27
|
+
|
|
28
|
+
this.shutdownController = init.shutdownController
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
async listen (): Promise<void> {
|
|
@@ -39,6 +46,7 @@ export class WebRTCPeerListener extends EventEmitter<ListenerEvents> implements
|
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
async close (): Promise<void> {
|
|
49
|
+
this.shutdownController.abort()
|
|
42
50
|
this.safeDispatchEvent('close', {})
|
|
43
51
|
}
|
|
44
52
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { CodeError } from '@libp2p/interface/errors'
|
|
2
|
+
import { logger } from '@libp2p/logger'
|
|
3
|
+
import { pbStream } from 'it-protobuf-stream'
|
|
4
|
+
import pDefer, { type DeferredPromise } from 'p-defer'
|
|
5
|
+
import { type RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js'
|
|
6
|
+
import { Message } from './pb/message.js'
|
|
7
|
+
import { parseRemoteAddress, readCandidatesUntilConnected, resolveOnConnected } from './util.js'
|
|
8
|
+
import type { IncomingStreamData } from '@libp2p/interface-internal/registrar'
|
|
9
|
+
|
|
10
|
+
const log = logger('libp2p:webrtc:signaling-stream-handler')
|
|
11
|
+
|
|
12
|
+
export interface IncomingStreamOpts extends IncomingStreamData {
|
|
13
|
+
peerConnection: RTCPeerConnection
|
|
14
|
+
signal: AbortSignal
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function handleIncomingStream ({ peerConnection, stream, signal, connection }: IncomingStreamOpts): Promise<{ remoteAddress: string }> {
|
|
18
|
+
log.trace('new inbound signaling stream')
|
|
19
|
+
|
|
20
|
+
const messageStream = pbStream(stream).pb(Message)
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const connectedPromise: DeferredPromise<void> = pDefer()
|
|
24
|
+
const answerSentPromise: DeferredPromise<void> = pDefer()
|
|
25
|
+
|
|
26
|
+
signal.onabort = () => {
|
|
27
|
+
connectedPromise.reject(new CodeError('Timed out while trying to connect', 'ERR_TIMEOUT'))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// candidate callbacks
|
|
31
|
+
peerConnection.onicecandidate = ({ candidate }) => {
|
|
32
|
+
answerSentPromise.promise.then(
|
|
33
|
+
async () => {
|
|
34
|
+
// a null candidate means end-of-candidates, an empty string candidate
|
|
35
|
+
// means end-of-candidates for this generation, otherwise this should
|
|
36
|
+
// be a valid candidate object
|
|
37
|
+
// see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent
|
|
38
|
+
const data = JSON.stringify(candidate?.toJSON() ?? null)
|
|
39
|
+
|
|
40
|
+
log.trace('recipient sending ICE candidate %s', data)
|
|
41
|
+
|
|
42
|
+
await messageStream.write({
|
|
43
|
+
type: Message.Type.ICE_CANDIDATE,
|
|
44
|
+
data
|
|
45
|
+
}, {
|
|
46
|
+
signal
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
(err) => {
|
|
50
|
+
log.error('cannot set candidate since sending answer failed', err)
|
|
51
|
+
connectedPromise.reject(err)
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
resolveOnConnected(peerConnection, connectedPromise)
|
|
57
|
+
|
|
58
|
+
// read an SDP offer
|
|
59
|
+
const pbOffer = await messageStream.read({
|
|
60
|
+
signal
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (pbOffer.type !== Message.Type.SDP_OFFER) {
|
|
64
|
+
throw new CodeError(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `, 'ERR_SDP_HANDSHAKE_FAILED')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
log.trace('recipient receive SDP offer %s', pbOffer.data)
|
|
68
|
+
|
|
69
|
+
const offer = new RTCSessionDescription({
|
|
70
|
+
type: 'offer',
|
|
71
|
+
sdp: pbOffer.data
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await peerConnection.setRemoteDescription(offer).catch(err => {
|
|
75
|
+
log.error('could not execute setRemoteDescription', err)
|
|
76
|
+
throw new CodeError('Failed to set remoteDescription', 'ERR_SDP_HANDSHAKE_FAILED')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// create and write an SDP answer
|
|
80
|
+
const answer = await peerConnection.createAnswer().catch(err => {
|
|
81
|
+
log.error('could not execute createAnswer', err)
|
|
82
|
+
answerSentPromise.reject(err)
|
|
83
|
+
throw new CodeError('Failed to create answer', 'ERR_SDP_HANDSHAKE_FAILED')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
log.trace('recipient send SDP answer %s', answer.sdp)
|
|
87
|
+
|
|
88
|
+
// write the answer to the remote
|
|
89
|
+
await messageStream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp }, {
|
|
90
|
+
signal
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
await peerConnection.setLocalDescription(answer).catch(err => {
|
|
94
|
+
log.error('could not execute setLocalDescription', err)
|
|
95
|
+
answerSentPromise.reject(err)
|
|
96
|
+
throw new CodeError('Failed to set localDescription', 'ERR_SDP_HANDSHAKE_FAILED')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
answerSentPromise.resolve()
|
|
100
|
+
|
|
101
|
+
log.trace('recipient read candidates until connected')
|
|
102
|
+
|
|
103
|
+
// wait until candidates are connected
|
|
104
|
+
await readCandidatesUntilConnected(connectedPromise, peerConnection, messageStream, {
|
|
105
|
+
direction: 'recipient',
|
|
106
|
+
signal
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
log.trace('recipient connected, closing signaling stream')
|
|
110
|
+
await messageStream.unwrap().unwrap().close({
|
|
111
|
+
signal
|
|
112
|
+
})
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
if (peerConnection.connectionState !== 'connected') {
|
|
115
|
+
log.error('error while handling signaling stream from peer %a', connection.remoteAddr, err)
|
|
116
|
+
|
|
117
|
+
peerConnection.close()
|
|
118
|
+
throw err
|
|
119
|
+
} else {
|
|
120
|
+
log('error while handling signaling stream from peer %a, ignoring as the RTCPeerConnection is already connected', connection.remoteAddr, err)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const remoteAddress = parseRemoteAddress(peerConnection.currentRemoteDescription?.sdp ?? '')
|
|
125
|
+
|
|
126
|
+
log.trace('recipient connected to remote address %s', remoteAddress)
|
|
127
|
+
|
|
128
|
+
return { remoteAddress }
|
|
129
|
+
}
|