node-rtc-connection 1.0.19 → 2.0.4
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/README.md +94 -85
- package/dist/index.cjs +20 -5606
- package/dist/index.mjs +25 -5598
- package/dist/types/crypto/der.d.ts +107 -0
- package/dist/types/crypto/x509.d.ts +56 -0
- package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
- package/dist/types/dtls/RTCCertificate.d.ts +163 -0
- package/dist/types/dtls/cipher.d.ts +81 -0
- package/dist/types/dtls/connection.d.ts +81 -0
- package/dist/types/dtls/prf.d.ts +29 -0
- package/dist/types/dtls/protocol.d.ts +127 -0
- package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
- package/dist/types/foundation/RTCError.d.ts +152 -0
- package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
- package/dist/types/ice/ice-agent.d.ts +154 -0
- package/dist/types/ice/stun-message.d.ts +92 -0
- package/dist/types/index.d.ts +29 -0
- package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
- package/dist/types/sctp/association.d.ts +77 -0
- package/dist/types/sctp/chunks.d.ts +200 -0
- package/dist/types/sctp/crc32c.d.ts +24 -0
- package/dist/types/sctp/datachannel-manager.d.ts +51 -0
- package/dist/types/sctp/dcep.d.ts +56 -0
- package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
- package/dist/types/sdp/sdp-utils.d.ts +103 -0
- package/dist/types/stun/stun-client.d.ts +119 -0
- package/dist/types/transport-stack.d.ts +68 -0
- package/package.json +26 -21
- package/src/crypto/der.ts +205 -0
- package/src/crypto/x509.ts +146 -0
- package/src/datachannel/RTCDataChannel.ts +388 -0
- package/src/dtls/RTCCertificate.ts +396 -0
- package/src/dtls/cipher.ts +198 -0
- package/src/dtls/connection.ts +974 -0
- package/src/dtls/prf.ts +62 -0
- package/src/dtls/protocol.ts +204 -0
- package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
- package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
- package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
- package/src/ice/ice-agent.ts +609 -0
- package/src/ice/stun-message.ts +260 -0
- package/src/index.ts +72 -0
- package/src/peerconnection/RTCPeerConnection.ts +430 -0
- package/src/sctp/association.ts +523 -0
- package/src/sctp/chunks.ts +350 -0
- package/src/sctp/crc32c.ts +57 -0
- package/src/sctp/datachannel-manager.ts +187 -0
- package/src/sctp/dcep.ts +94 -0
- package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
- package/src/sdp/sdp-utils.ts +229 -0
- package/src/stun/{stun-client.js → stun-client.ts} +346 -187
- package/src/transport-stack.ts +165 -0
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/src/datachannel/RTCDataChannel.js +0 -354
- package/src/dtls/RTCCertificate.js +0 -310
- package/src/dtls/RTCDtlsTransport.js +0 -247
- package/src/ice/RTCIceTransport.js +0 -1018
- package/src/index.d.ts +0 -400
- package/src/index.js +0 -92
- package/src/network/network-transport.js +0 -478
- package/src/peerconnection/RTCPeerConnection.js +0 -875
- package/src/sctp/RTCSctpTransport.js +0 -253
- package/src/sdp/sdp-utils.js +0 -224
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file stun-client.
|
|
2
|
+
* @file stun-client.ts
|
|
3
3
|
* @description STUN (Session Traversal Utilities for NAT) client implementation
|
|
4
4
|
* @module stun/stun-client
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* STUN Protocol: RFC 5389
|
|
7
7
|
* TURN Protocol: RFC 5766
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import * as dgram from 'dgram';
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
import { EventEmitter } from 'events';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* STUN message types
|
|
@@ -21,24 +21,24 @@ const STUN_MESSAGE_TYPES = {
|
|
|
21
21
|
BINDING_REQUEST: 0x0001,
|
|
22
22
|
BINDING_RESPONSE: 0x0101,
|
|
23
23
|
BINDING_ERROR_RESPONSE: 0x0111,
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
// TURN
|
|
26
26
|
ALLOCATE_REQUEST: 0x0003,
|
|
27
27
|
ALLOCATE_RESPONSE: 0x0103,
|
|
28
28
|
ALLOCATE_ERROR_RESPONSE: 0x0113,
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
REFRESH_REQUEST: 0x0004,
|
|
31
31
|
REFRESH_RESPONSE: 0x0104,
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
SEND_INDICATION: 0x0016,
|
|
34
34
|
DATA_INDICATION: 0x0017,
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
CREATE_PERMISSION_REQUEST: 0x0008,
|
|
37
37
|
CREATE_PERMISSION_RESPONSE: 0x0108,
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
CHANNEL_BIND_REQUEST: 0x0009,
|
|
40
|
-
CHANNEL_BIND_RESPONSE: 0x0109
|
|
41
|
-
};
|
|
40
|
+
CHANNEL_BIND_RESPONSE: 0x0109,
|
|
41
|
+
} as const;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* STUN attribute types
|
|
@@ -48,30 +48,145 @@ const STUN_ATTRIBUTES = {
|
|
|
48
48
|
USERNAME: 0x0006,
|
|
49
49
|
MESSAGE_INTEGRITY: 0x0008,
|
|
50
50
|
ERROR_CODE: 0x0009,
|
|
51
|
-
UNKNOWN_ATTRIBUTES:
|
|
51
|
+
UNKNOWN_ATTRIBUTES: 0x000a,
|
|
52
52
|
REALM: 0x0014,
|
|
53
53
|
NONCE: 0x0015,
|
|
54
54
|
XOR_MAPPED_ADDRESS: 0x0020,
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
// TURN
|
|
57
|
-
CHANNEL_NUMBER:
|
|
58
|
-
LIFETIME:
|
|
57
|
+
CHANNEL_NUMBER: 0x000c,
|
|
58
|
+
LIFETIME: 0x000d,
|
|
59
59
|
XOR_PEER_ADDRESS: 0x0012,
|
|
60
60
|
DATA: 0x0013,
|
|
61
61
|
XOR_RELAYED_ADDRESS: 0x0016,
|
|
62
62
|
REQUESTED_TRANSPORT: 0x0019,
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
SOFTWARE: 0x8022,
|
|
65
|
-
FINGERPRINT: 0x8028
|
|
66
|
-
};
|
|
65
|
+
FINGERPRINT: 0x8028,
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
const MAGIC_COOKIE = 0x2112a442;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Constructor options for {@link STUNClient}.
|
|
72
|
+
*/
|
|
73
|
+
interface STUNClientOptions {
|
|
74
|
+
/** STUN/TURN server address */
|
|
75
|
+
server: string;
|
|
76
|
+
/** Server port */
|
|
77
|
+
port: number;
|
|
78
|
+
/** TURN username */
|
|
79
|
+
username?: string;
|
|
80
|
+
/** TURN password */
|
|
81
|
+
credential?: string;
|
|
82
|
+
/** Transport protocol (udp/tcp) */
|
|
83
|
+
transport?: string;
|
|
84
|
+
/** Additional query parameters from URL */
|
|
85
|
+
params?: Record<string, unknown>;
|
|
86
|
+
}
|
|
67
87
|
|
|
68
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Parsed IPv4 address info.
|
|
90
|
+
*/
|
|
91
|
+
interface AddressInfo {
|
|
92
|
+
family: string;
|
|
93
|
+
port: number;
|
|
94
|
+
address: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reflexive address info resolved from a STUN Binding response.
|
|
99
|
+
*/
|
|
100
|
+
interface ReflexiveAddress {
|
|
101
|
+
address: string;
|
|
102
|
+
port: number;
|
|
103
|
+
family: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Relay address info resolved from a TURN Allocate response.
|
|
108
|
+
*/
|
|
109
|
+
interface RelayAddress {
|
|
110
|
+
relayedAddress: string;
|
|
111
|
+
relayedPort: number;
|
|
112
|
+
lifetime: number;
|
|
113
|
+
type: 'relay';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Result of a TURN Refresh response.
|
|
118
|
+
*/
|
|
119
|
+
interface RefreshResult {
|
|
120
|
+
lifetime: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Result of a generic success response (CreatePermission / ChannelBind).
|
|
125
|
+
*/
|
|
126
|
+
interface OkResult {
|
|
127
|
+
ok: true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Union of every result shape a transaction may resolve with.
|
|
132
|
+
*/
|
|
133
|
+
type TransactionResult = ReflexiveAddress | RelayAddress | RefreshResult | OkResult;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A pending request transaction.
|
|
137
|
+
*/
|
|
138
|
+
interface Transaction {
|
|
139
|
+
type?: string;
|
|
140
|
+
resolve: (result: TransactionResult) => void;
|
|
141
|
+
reject: (error: Error) => void;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parsed STUN attributes object.
|
|
146
|
+
*/
|
|
147
|
+
interface ParsedAttributes {
|
|
148
|
+
xorMappedAddress?: AddressInfo | null;
|
|
149
|
+
xorRelayedAddress?: AddressInfo | null;
|
|
150
|
+
xorPeerAddress?: AddressInfo | null;
|
|
151
|
+
mappedAddress?: AddressInfo | null;
|
|
152
|
+
data?: Buffer;
|
|
153
|
+
lifetime?: number;
|
|
154
|
+
errorCode?: string;
|
|
155
|
+
realm?: string;
|
|
156
|
+
nonce?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Payload emitted with the 'data' event for relayed peer data.
|
|
161
|
+
*/
|
|
162
|
+
interface DataEventInfo {
|
|
163
|
+
address: string;
|
|
164
|
+
port: number;
|
|
165
|
+
family: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build product passed back from a request builder used with auth retry.
|
|
170
|
+
*/
|
|
171
|
+
interface RequestBuild {
|
|
172
|
+
transactionId: Buffer;
|
|
173
|
+
request: Buffer;
|
|
174
|
+
}
|
|
69
175
|
|
|
70
176
|
/**
|
|
71
177
|
* @class STUNClient
|
|
72
178
|
* @description STUN/TURN client for NAT traversal
|
|
73
179
|
*/
|
|
74
180
|
class STUNClient extends EventEmitter {
|
|
181
|
+
#server: string;
|
|
182
|
+
#port: number;
|
|
183
|
+
#username: string | undefined;
|
|
184
|
+
#credential: string | undefined;
|
|
185
|
+
#socket: dgram.Socket | null;
|
|
186
|
+
#transactions: Map<string, Transaction>;
|
|
187
|
+
#realm: string | null;
|
|
188
|
+
#nonce: string | null;
|
|
189
|
+
|
|
75
190
|
/**
|
|
76
191
|
* Create a STUN client
|
|
77
192
|
* @param {Object} options - Client options
|
|
@@ -82,43 +197,42 @@ class STUNClient extends EventEmitter {
|
|
|
82
197
|
* @param {string} [options.transport='udp'] - Transport protocol (udp/tcp)
|
|
83
198
|
* @param {Object} [options.params={}] - Additional query parameters from URL
|
|
84
199
|
*/
|
|
85
|
-
constructor(options) {
|
|
200
|
+
constructor(options: STUNClientOptions) {
|
|
86
201
|
super();
|
|
87
|
-
this
|
|
88
|
-
this
|
|
89
|
-
this
|
|
90
|
-
this
|
|
91
|
-
|
|
92
|
-
this
|
|
93
|
-
|
|
94
|
-
this
|
|
95
|
-
this
|
|
96
|
-
this.realm = null;
|
|
97
|
-
this.nonce = null;
|
|
202
|
+
this.#server = options.server;
|
|
203
|
+
this.#port = options.port;
|
|
204
|
+
this.#username = options.username;
|
|
205
|
+
this.#credential = options.credential;
|
|
206
|
+
|
|
207
|
+
this.#socket = null;
|
|
208
|
+
this.#transactions = new Map();
|
|
209
|
+
this.#realm = null;
|
|
210
|
+
this.#nonce = null;
|
|
98
211
|
}
|
|
99
212
|
|
|
100
213
|
/**
|
|
101
214
|
* Connect to the STUN/TURN server
|
|
102
215
|
* @returns {Promise<void>}
|
|
103
216
|
*/
|
|
104
|
-
async connect() {
|
|
105
|
-
if (this
|
|
217
|
+
async connect(): Promise<void> {
|
|
218
|
+
if (this.#socket) {
|
|
106
219
|
return;
|
|
107
220
|
}
|
|
108
221
|
|
|
109
|
-
return new Promise((resolve, reject) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
222
|
+
return new Promise<void>((resolve, reject) => {
|
|
223
|
+
const socket = dgram.createSocket('udp4');
|
|
224
|
+
this.#socket = socket;
|
|
225
|
+
|
|
226
|
+
socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo) => {
|
|
227
|
+
this.#handleMessage(msg, rinfo);
|
|
114
228
|
});
|
|
115
229
|
|
|
116
|
-
|
|
230
|
+
socket.on('error', (err: Error) => {
|
|
117
231
|
console.error('STUN socket error:', err);
|
|
118
232
|
reject(err);
|
|
119
233
|
});
|
|
120
234
|
|
|
121
|
-
|
|
235
|
+
socket.bind(() => {
|
|
122
236
|
resolve();
|
|
123
237
|
});
|
|
124
238
|
});
|
|
@@ -128,33 +242,33 @@ class STUNClient extends EventEmitter {
|
|
|
128
242
|
* Send a STUN Binding Request to get reflexive address
|
|
129
243
|
* @returns {Promise<Object>} Reflexive address info
|
|
130
244
|
*/
|
|
131
|
-
async getReflexiveAddress() {
|
|
245
|
+
async getReflexiveAddress(): Promise<TransactionResult> {
|
|
132
246
|
await this.connect();
|
|
133
247
|
|
|
134
248
|
const transactionId = crypto.randomBytes(12);
|
|
135
|
-
const request = this
|
|
249
|
+
const request = this.#createBindingRequest(transactionId);
|
|
136
250
|
|
|
137
|
-
return new Promise((resolve, reject) => {
|
|
251
|
+
return new Promise<TransactionResult>((resolve, reject) => {
|
|
138
252
|
const timeout = setTimeout(() => {
|
|
139
|
-
this
|
|
253
|
+
this.#transactions.delete(transactionId.toString('hex'));
|
|
140
254
|
reject(new Error('STUN request timeout'));
|
|
141
255
|
}, 5000);
|
|
142
256
|
|
|
143
|
-
this
|
|
144
|
-
resolve: (result) => {
|
|
257
|
+
this.#transactions.set(transactionId.toString('hex'), {
|
|
258
|
+
resolve: (result: TransactionResult) => {
|
|
145
259
|
clearTimeout(timeout);
|
|
146
260
|
resolve(result);
|
|
147
261
|
},
|
|
148
|
-
reject: (error) => {
|
|
262
|
+
reject: (error: Error) => {
|
|
149
263
|
clearTimeout(timeout);
|
|
150
264
|
reject(error);
|
|
151
|
-
}
|
|
265
|
+
},
|
|
152
266
|
});
|
|
153
267
|
|
|
154
|
-
this
|
|
268
|
+
this.#socket!.send(request, this.#port, this.#server, (err) => {
|
|
155
269
|
if (err) {
|
|
156
270
|
clearTimeout(timeout);
|
|
157
|
-
this
|
|
271
|
+
this.#transactions.delete(transactionId.toString('hex'));
|
|
158
272
|
reject(err);
|
|
159
273
|
}
|
|
160
274
|
});
|
|
@@ -166,26 +280,26 @@ class STUNClient extends EventEmitter {
|
|
|
166
280
|
* @param {number} [lifetime=600] - Allocation lifetime in seconds
|
|
167
281
|
* @returns {Promise<Object>} Relay address info
|
|
168
282
|
*/
|
|
169
|
-
async allocateRelay(lifetime = 600) {
|
|
170
|
-
if (!this
|
|
283
|
+
async allocateRelay(lifetime: number = 600): Promise<TransactionResult> {
|
|
284
|
+
if (!this.#username || !this.#credential) {
|
|
171
285
|
throw new Error('TURN requires username and credential');
|
|
172
286
|
}
|
|
173
287
|
|
|
174
288
|
await this.connect();
|
|
175
289
|
|
|
176
290
|
let transactionId = crypto.randomBytes(12);
|
|
177
|
-
let request = this
|
|
291
|
+
let request = this.#createAllocateRequest(transactionId, lifetime);
|
|
178
292
|
|
|
179
293
|
// First attempt without credentials to get realm and nonce
|
|
180
294
|
try {
|
|
181
|
-
return await this
|
|
295
|
+
return await this.#sendRequest(request, transactionId, 'allocate');
|
|
182
296
|
} catch (error) {
|
|
183
297
|
// If we get 401 Unauthorized, retry with credentials
|
|
184
|
-
if (error.message.includes('401') && this
|
|
298
|
+
if (error instanceof Error && error.message.includes('401') && this.#realm && this.#nonce) {
|
|
185
299
|
// Create new transaction ID for retry
|
|
186
300
|
transactionId = crypto.randomBytes(12);
|
|
187
|
-
request = this
|
|
188
|
-
return await this
|
|
301
|
+
request = this.#createAllocateRequest(transactionId, lifetime, true);
|
|
302
|
+
return await this.#sendRequest(request, transactionId, 'allocate');
|
|
189
303
|
}
|
|
190
304
|
throw error;
|
|
191
305
|
}
|
|
@@ -196,15 +310,36 @@ class STUNClient extends EventEmitter {
|
|
|
196
310
|
* @param {number} [lifetime=600] - Allocation lifetime in seconds
|
|
197
311
|
* @returns {Promise<Object>} Updated allocation info
|
|
198
312
|
*/
|
|
199
|
-
async refreshAllocation(lifetime = 600) {
|
|
200
|
-
if (!this
|
|
313
|
+
async refreshAllocation(lifetime: number = 600): Promise<TransactionResult> {
|
|
314
|
+
if (!this.#username || !this.#credential) {
|
|
201
315
|
throw new Error('TURN requires username and credential');
|
|
202
316
|
}
|
|
203
317
|
|
|
204
|
-
|
|
205
|
-
|
|
318
|
+
return this.#withAuthRetry('refresh', () => {
|
|
319
|
+
const transactionId = crypto.randomBytes(12);
|
|
320
|
+
return { transactionId, request: this.#createRefreshRequest(transactionId, lifetime) };
|
|
321
|
+
});
|
|
322
|
+
}
|
|
206
323
|
|
|
207
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Send an authenticated TURN request, retrying once on a 401 (stale-nonce or
|
|
326
|
+
* first-time challenge) after refreshing realm/nonce from the error.
|
|
327
|
+
* @param {string} type - request label for diagnostics
|
|
328
|
+
* @param {() => {transactionId: Buffer, request: Buffer}} build
|
|
329
|
+
* @returns {Promise<Object>}
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
async #withAuthRetry(type: string, build: () => RequestBuild): Promise<TransactionResult> {
|
|
333
|
+
const first = build();
|
|
334
|
+
try {
|
|
335
|
+
return await this.#sendRequest(first.request, first.transactionId, type);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error instanceof Error && error.message.includes('401') && this.#realm && this.#nonce) {
|
|
338
|
+
const retry = build(); // rebuilt with the refreshed realm/nonce
|
|
339
|
+
return this.#sendRequest(retry.request, retry.transactionId, type);
|
|
340
|
+
}
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
208
343
|
}
|
|
209
344
|
|
|
210
345
|
/**
|
|
@@ -212,15 +347,15 @@ class STUNClient extends EventEmitter {
|
|
|
212
347
|
* @param {string} peerAddress - Peer IP address
|
|
213
348
|
* @returns {Promise<void>}
|
|
214
349
|
*/
|
|
215
|
-
async createPermission(peerAddress) {
|
|
216
|
-
if (!this
|
|
350
|
+
async createPermission(peerAddress: string): Promise<void> {
|
|
351
|
+
if (!this.#username || !this.#credential) {
|
|
217
352
|
throw new Error('TURN requires username and credential');
|
|
218
353
|
}
|
|
219
354
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
355
|
+
await this.#withAuthRetry('createPermission', () => {
|
|
356
|
+
const transactionId = crypto.randomBytes(12);
|
|
357
|
+
return { transactionId, request: this.#createCreatePermissionRequest(transactionId, peerAddress) };
|
|
358
|
+
});
|
|
224
359
|
}
|
|
225
360
|
|
|
226
361
|
/**
|
|
@@ -230,17 +365,17 @@ class STUNClient extends EventEmitter {
|
|
|
230
365
|
* @param {Buffer} data - Data to send
|
|
231
366
|
* @returns {Promise<void>}
|
|
232
367
|
*/
|
|
233
|
-
async sendIndication(peerAddress, peerPort, data) {
|
|
234
|
-
if (!this
|
|
368
|
+
async sendIndication(peerAddress: string, peerPort: number, data: Buffer): Promise<void> {
|
|
369
|
+
if (!this.#username || !this.#credential) {
|
|
235
370
|
throw new Error('TURN requires username and credential');
|
|
236
371
|
}
|
|
237
372
|
|
|
238
373
|
const transactionId = crypto.randomBytes(12);
|
|
239
|
-
const indication = this
|
|
374
|
+
const indication = this.#createSendIndication(transactionId, peerAddress, peerPort, data);
|
|
240
375
|
|
|
241
376
|
// Indications are fire-and-forget, no response expected
|
|
242
|
-
return new Promise((resolve, reject) => {
|
|
243
|
-
this
|
|
377
|
+
return new Promise<void>((resolve, reject) => {
|
|
378
|
+
this.#socket!.send(indication, this.#port, this.#server, (err) => {
|
|
244
379
|
if (err) reject(err);
|
|
245
380
|
else resolve();
|
|
246
381
|
});
|
|
@@ -255,29 +390,29 @@ class STUNClient extends EventEmitter {
|
|
|
255
390
|
* @returns {Promise<Object>}
|
|
256
391
|
* @private
|
|
257
392
|
*/
|
|
258
|
-
|
|
259
|
-
return new Promise((resolve, reject) => {
|
|
393
|
+
#sendRequest(request: Buffer, transactionId: Buffer, requestType: string): Promise<TransactionResult> {
|
|
394
|
+
return new Promise<TransactionResult>((resolve, reject) => {
|
|
260
395
|
const timeout = setTimeout(() => {
|
|
261
|
-
this
|
|
396
|
+
this.#transactions.delete(transactionId.toString('hex'));
|
|
262
397
|
reject(new Error(`${requestType} request timeout`));
|
|
263
398
|
}, 5000);
|
|
264
399
|
|
|
265
|
-
this
|
|
400
|
+
this.#transactions.set(transactionId.toString('hex'), {
|
|
266
401
|
type: requestType,
|
|
267
|
-
resolve: (result) => {
|
|
402
|
+
resolve: (result: TransactionResult) => {
|
|
268
403
|
clearTimeout(timeout);
|
|
269
404
|
resolve(result);
|
|
270
405
|
},
|
|
271
|
-
reject: (error) => {
|
|
406
|
+
reject: (error: Error) => {
|
|
272
407
|
clearTimeout(timeout);
|
|
273
408
|
reject(error);
|
|
274
|
-
}
|
|
409
|
+
},
|
|
275
410
|
});
|
|
276
411
|
|
|
277
|
-
this
|
|
412
|
+
this.#socket!.send(request, this.#port, this.#server, (err) => {
|
|
278
413
|
if (err) {
|
|
279
414
|
clearTimeout(timeout);
|
|
280
|
-
this
|
|
415
|
+
this.#transactions.delete(transactionId.toString('hex'));
|
|
281
416
|
reject(err);
|
|
282
417
|
}
|
|
283
418
|
});
|
|
@@ -290,21 +425,21 @@ class STUNClient extends EventEmitter {
|
|
|
290
425
|
* @returns {Buffer} STUN message
|
|
291
426
|
* @private
|
|
292
427
|
*/
|
|
293
|
-
|
|
428
|
+
#createBindingRequest(transactionId: Buffer): Buffer {
|
|
294
429
|
const header = Buffer.alloc(20);
|
|
295
|
-
|
|
430
|
+
|
|
296
431
|
// Message Type (2 bytes)
|
|
297
432
|
header.writeUInt16BE(STUN_MESSAGE_TYPES.BINDING_REQUEST, 0);
|
|
298
|
-
|
|
433
|
+
|
|
299
434
|
// Message Length (2 bytes) - 0 for now, no attributes
|
|
300
435
|
header.writeUInt16BE(0, 2);
|
|
301
|
-
|
|
436
|
+
|
|
302
437
|
// Magic Cookie (4 bytes)
|
|
303
438
|
header.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
304
|
-
|
|
439
|
+
|
|
305
440
|
// Transaction ID (12 bytes)
|
|
306
441
|
transactionId.copy(header, 8);
|
|
307
|
-
|
|
442
|
+
|
|
308
443
|
return header;
|
|
309
444
|
}
|
|
310
445
|
|
|
@@ -316,8 +451,8 @@ class STUNClient extends EventEmitter {
|
|
|
316
451
|
* @returns {Buffer} STUN message
|
|
317
452
|
* @private
|
|
318
453
|
*/
|
|
319
|
-
|
|
320
|
-
const attributes = [];
|
|
454
|
+
#createAllocateRequest(transactionId: Buffer, lifetime: number, withAuth: boolean = false): Buffer {
|
|
455
|
+
const attributes: Buffer[] = [];
|
|
321
456
|
|
|
322
457
|
// REQUESTED-TRANSPORT (UDP = 17)
|
|
323
458
|
const transport = Buffer.alloc(8);
|
|
@@ -333,21 +468,21 @@ class STUNClient extends EventEmitter {
|
|
|
333
468
|
lifetimeAttr.writeUInt32BE(lifetime, 4);
|
|
334
469
|
attributes.push(lifetimeAttr);
|
|
335
470
|
|
|
336
|
-
if (withAuth && this
|
|
471
|
+
if (withAuth && this.#realm && this.#nonce) {
|
|
337
472
|
// USERNAME
|
|
338
|
-
const usernameAttr = this
|
|
473
|
+
const usernameAttr = this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!);
|
|
339
474
|
attributes.push(usernameAttr);
|
|
340
475
|
|
|
341
476
|
// REALM
|
|
342
|
-
const realmAttr = this
|
|
477
|
+
const realmAttr = this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm);
|
|
343
478
|
attributes.push(realmAttr);
|
|
344
479
|
|
|
345
480
|
// NONCE
|
|
346
|
-
const nonceAttr = this
|
|
481
|
+
const nonceAttr = this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce);
|
|
347
482
|
attributes.push(nonceAttr);
|
|
348
483
|
}
|
|
349
484
|
|
|
350
|
-
return this
|
|
485
|
+
return this.#createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
|
|
351
486
|
}
|
|
352
487
|
|
|
353
488
|
/**
|
|
@@ -357,21 +492,21 @@ class STUNClient extends EventEmitter {
|
|
|
357
492
|
* @returns {Buffer} STUN message
|
|
358
493
|
* @private
|
|
359
494
|
*/
|
|
360
|
-
|
|
361
|
-
const attributes = [];
|
|
495
|
+
#createCreatePermissionRequest(transactionId: Buffer, peerAddress: string): Buffer {
|
|
496
|
+
const attributes: Buffer[] = [];
|
|
362
497
|
|
|
363
498
|
// XOR-PEER-ADDRESS
|
|
364
|
-
const peerAttr = this
|
|
499
|
+
const peerAttr = this.#createXorPeerAddressAttribute(peerAddress, 0, transactionId);
|
|
365
500
|
attributes.push(peerAttr);
|
|
366
501
|
|
|
367
502
|
// Auth attributes
|
|
368
|
-
if (this
|
|
369
|
-
attributes.push(this
|
|
370
|
-
attributes.push(this
|
|
371
|
-
attributes.push(this
|
|
503
|
+
if (this.#realm && this.#nonce) {
|
|
504
|
+
attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!));
|
|
505
|
+
attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm));
|
|
506
|
+
attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce));
|
|
372
507
|
}
|
|
373
508
|
|
|
374
|
-
return this
|
|
509
|
+
return this.#createMessage(STUN_MESSAGE_TYPES.CREATE_PERMISSION_REQUEST, transactionId, attributes, true);
|
|
375
510
|
}
|
|
376
511
|
|
|
377
512
|
/**
|
|
@@ -383,21 +518,21 @@ class STUNClient extends EventEmitter {
|
|
|
383
518
|
* @returns {Buffer} STUN message
|
|
384
519
|
* @private
|
|
385
520
|
*/
|
|
386
|
-
|
|
387
|
-
const attributes = [];
|
|
521
|
+
#createSendIndication(transactionId: Buffer, peerAddress: string, peerPort: number, data: Buffer): Buffer {
|
|
522
|
+
const attributes: Buffer[] = [];
|
|
388
523
|
|
|
389
524
|
// XOR-PEER-ADDRESS
|
|
390
|
-
const peerAttr = this
|
|
525
|
+
const peerAttr = this.#createXorPeerAddressAttribute(peerAddress, peerPort, transactionId);
|
|
391
526
|
attributes.push(peerAttr);
|
|
392
527
|
|
|
393
528
|
// DATA
|
|
394
|
-
const dataAttr = Buffer.alloc(4 + data.length + (4 - (data.length % 4)) % 4);
|
|
529
|
+
const dataAttr = Buffer.alloc(4 + data.length + ((4 - (data.length % 4)) % 4));
|
|
395
530
|
dataAttr.writeUInt16BE(STUN_ATTRIBUTES.DATA, 0);
|
|
396
531
|
dataAttr.writeUInt16BE(data.length, 2);
|
|
397
532
|
data.copy(dataAttr, 4);
|
|
398
533
|
attributes.push(dataAttr);
|
|
399
534
|
|
|
400
|
-
return this
|
|
535
|
+
return this.#createMessage(STUN_MESSAGE_TYPES.SEND_INDICATION, transactionId, attributes, false);
|
|
401
536
|
}
|
|
402
537
|
|
|
403
538
|
/**
|
|
@@ -408,10 +543,10 @@ class STUNClient extends EventEmitter {
|
|
|
408
543
|
* @returns {Buffer} Attribute buffer
|
|
409
544
|
* @private
|
|
410
545
|
*/
|
|
411
|
-
|
|
546
|
+
#createXorPeerAddressAttribute(address: string, port: number, _transactionId: Buffer): Buffer {
|
|
412
547
|
const family = 0x01; // IPv4
|
|
413
548
|
const buffer = Buffer.alloc(4 + 8); // Type(2) + Length(2) + Reserved(1) + Family(1) + Port(2) + Address(4)
|
|
414
|
-
|
|
549
|
+
|
|
415
550
|
buffer.writeUInt16BE(STUN_ATTRIBUTES.XOR_PEER_ADDRESS, 0);
|
|
416
551
|
buffer.writeUInt16BE(8, 2);
|
|
417
552
|
buffer.writeUInt8(0, 4);
|
|
@@ -423,9 +558,9 @@ class STUNClient extends EventEmitter {
|
|
|
423
558
|
|
|
424
559
|
// XOR Address
|
|
425
560
|
const parts = address.split('.').map(Number);
|
|
426
|
-
const addrInt = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]
|
|
561
|
+
const addrInt = (parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]!;
|
|
427
562
|
const xorAddr = addrInt ^ MAGIC_COOKIE;
|
|
428
|
-
|
|
563
|
+
|
|
429
564
|
buffer.writeUInt32BE(xorAddr >>> 0, 8); // Ensure unsigned
|
|
430
565
|
|
|
431
566
|
return buffer;
|
|
@@ -438,8 +573,8 @@ class STUNClient extends EventEmitter {
|
|
|
438
573
|
* @returns {Buffer} STUN message
|
|
439
574
|
* @private
|
|
440
575
|
*/
|
|
441
|
-
|
|
442
|
-
const attributes = [];
|
|
576
|
+
#createRefreshRequest(transactionId: Buffer, lifetime: number): Buffer {
|
|
577
|
+
const attributes: Buffer[] = [];
|
|
443
578
|
|
|
444
579
|
// LIFETIME
|
|
445
580
|
const lifetimeAttr = Buffer.alloc(8);
|
|
@@ -449,22 +584,22 @@ class STUNClient extends EventEmitter {
|
|
|
449
584
|
attributes.push(lifetimeAttr);
|
|
450
585
|
|
|
451
586
|
// USERNAME
|
|
452
|
-
const usernameAttr = this
|
|
587
|
+
const usernameAttr = this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!);
|
|
453
588
|
attributes.push(usernameAttr);
|
|
454
589
|
|
|
455
590
|
// REALM
|
|
456
|
-
if (this
|
|
457
|
-
const realmAttr = this
|
|
591
|
+
if (this.#realm) {
|
|
592
|
+
const realmAttr = this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm);
|
|
458
593
|
attributes.push(realmAttr);
|
|
459
594
|
}
|
|
460
595
|
|
|
461
596
|
// NONCE
|
|
462
|
-
if (this
|
|
463
|
-
const nonceAttr = this
|
|
597
|
+
if (this.#nonce) {
|
|
598
|
+
const nonceAttr = this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce);
|
|
464
599
|
attributes.push(nonceAttr);
|
|
465
600
|
}
|
|
466
601
|
|
|
467
|
-
return this
|
|
602
|
+
return this.#createMessage(STUN_MESSAGE_TYPES.REFRESH_REQUEST, transactionId, attributes, true);
|
|
468
603
|
}
|
|
469
604
|
|
|
470
605
|
/**
|
|
@@ -476,11 +611,16 @@ class STUNClient extends EventEmitter {
|
|
|
476
611
|
* @returns {Buffer} Complete STUN message
|
|
477
612
|
* @private
|
|
478
613
|
*/
|
|
479
|
-
|
|
614
|
+
#createMessage(
|
|
615
|
+
messageType: number,
|
|
616
|
+
transactionId: Buffer,
|
|
617
|
+
attributes: Buffer[],
|
|
618
|
+
withIntegrity: boolean = false
|
|
619
|
+
): Buffer {
|
|
480
620
|
let attributesBuffer = Buffer.concat(attributes);
|
|
481
621
|
|
|
482
622
|
// Add MESSAGE-INTEGRITY if needed
|
|
483
|
-
if (withIntegrity && this
|
|
623
|
+
if (withIntegrity && this.#credential) {
|
|
484
624
|
const tempHeader = Buffer.alloc(20);
|
|
485
625
|
tempHeader.writeUInt16BE(messageType, 0);
|
|
486
626
|
tempHeader.writeUInt16BE(attributesBuffer.length + 24, 2); // +24 for MESSAGE-INTEGRITY
|
|
@@ -488,14 +628,14 @@ class STUNClient extends EventEmitter {
|
|
|
488
628
|
transactionId.copy(tempHeader, 8);
|
|
489
629
|
|
|
490
630
|
const tempMessage = Buffer.concat([tempHeader, attributesBuffer]);
|
|
491
|
-
|
|
631
|
+
|
|
492
632
|
// For TURN, compute key as MD5(username:realm:password) per RFC 5766
|
|
493
|
-
let key = this
|
|
494
|
-
if (this
|
|
495
|
-
const keyString = `${this
|
|
633
|
+
let key: string | Buffer = this.#credential;
|
|
634
|
+
if (this.#username && this.#realm) {
|
|
635
|
+
const keyString = `${this.#username}:${this.#realm}:${this.#credential}`;
|
|
496
636
|
key = crypto.createHash('md5').update(keyString).digest();
|
|
497
637
|
}
|
|
498
|
-
|
|
638
|
+
|
|
499
639
|
const hmac = crypto.createHmac('sha1', key);
|
|
500
640
|
hmac.update(tempMessage);
|
|
501
641
|
const integrity = hmac.digest();
|
|
@@ -525,16 +665,16 @@ class STUNClient extends EventEmitter {
|
|
|
525
665
|
* @returns {Buffer} Attribute buffer
|
|
526
666
|
* @private
|
|
527
667
|
*/
|
|
528
|
-
|
|
668
|
+
#createStringAttribute(type: number, value: string): Buffer {
|
|
529
669
|
const valueBuffer = Buffer.from(value, 'utf8');
|
|
530
670
|
const length = valueBuffer.length;
|
|
531
671
|
const padding = (4 - (length % 4)) % 4;
|
|
532
672
|
const buffer = Buffer.alloc(4 + length + padding);
|
|
533
|
-
|
|
673
|
+
|
|
534
674
|
buffer.writeUInt16BE(type, 0);
|
|
535
675
|
buffer.writeUInt16BE(length, 2);
|
|
536
676
|
valueBuffer.copy(buffer, 4);
|
|
537
|
-
|
|
677
|
+
|
|
538
678
|
return buffer;
|
|
539
679
|
}
|
|
540
680
|
|
|
@@ -544,7 +684,7 @@ class STUNClient extends EventEmitter {
|
|
|
544
684
|
* @param {Object} rinfo - Remote info
|
|
545
685
|
* @private
|
|
546
686
|
*/
|
|
547
|
-
|
|
687
|
+
#handleMessage(msg: Buffer, _rinfo: dgram.RemoteInfo): void {
|
|
548
688
|
if (msg.length < 20) {
|
|
549
689
|
return; // Invalid STUN message
|
|
550
690
|
}
|
|
@@ -558,14 +698,30 @@ class STUNClient extends EventEmitter {
|
|
|
558
698
|
return; // Not a STUN message
|
|
559
699
|
}
|
|
560
700
|
|
|
701
|
+
// DATA indications are server-initiated (relayed peer data) and carry a
|
|
702
|
+
// fresh transaction id that matches no pending request — handle them before
|
|
703
|
+
// the transaction lookup.
|
|
704
|
+
if (messageType === STUN_MESSAGE_TYPES.DATA_INDICATION) {
|
|
705
|
+
const attrs = this.#parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
|
|
706
|
+
if (attrs.xorPeerAddress && attrs.data) {
|
|
707
|
+
const info: DataEventInfo = {
|
|
708
|
+
address: attrs.xorPeerAddress.address,
|
|
709
|
+
port: attrs.xorPeerAddress.port,
|
|
710
|
+
family: attrs.xorPeerAddress.family || 'IPv4',
|
|
711
|
+
};
|
|
712
|
+
this.emit('data', attrs.data, info);
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
561
717
|
const transactionKey = transactionId.toString('hex');
|
|
562
|
-
const transaction = this
|
|
718
|
+
const transaction = this.#transactions.get(transactionKey);
|
|
563
719
|
|
|
564
720
|
if (!transaction) {
|
|
565
721
|
return; // Unknown transaction
|
|
566
722
|
}
|
|
567
723
|
|
|
568
|
-
const attributes = this
|
|
724
|
+
const attributes = this.#parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
|
|
569
725
|
|
|
570
726
|
// Handle STUN Binding responses
|
|
571
727
|
if (messageType === STUN_MESSAGE_TYPES.BINDING_RESPONSE) {
|
|
@@ -573,18 +729,18 @@ class STUNClient extends EventEmitter {
|
|
|
573
729
|
transaction.resolve({
|
|
574
730
|
address: attributes.xorMappedAddress.address,
|
|
575
731
|
port: attributes.xorMappedAddress.port,
|
|
576
|
-
family: attributes.xorMappedAddress.family
|
|
732
|
+
family: attributes.xorMappedAddress.family,
|
|
577
733
|
});
|
|
578
734
|
} else if (attributes.mappedAddress) {
|
|
579
735
|
transaction.resolve({
|
|
580
736
|
address: attributes.mappedAddress.address,
|
|
581
737
|
port: attributes.mappedAddress.port,
|
|
582
|
-
family: attributes.mappedAddress.family
|
|
738
|
+
family: attributes.mappedAddress.family,
|
|
583
739
|
});
|
|
584
740
|
} else {
|
|
585
741
|
transaction.reject(new Error('No mapped address in STUN response'));
|
|
586
742
|
}
|
|
587
|
-
this
|
|
743
|
+
this.#transactions.delete(transactionKey);
|
|
588
744
|
}
|
|
589
745
|
// Handle TURN Allocate responses
|
|
590
746
|
else if (messageType === STUN_MESSAGE_TYPES.ALLOCATE_RESPONSE) {
|
|
@@ -593,45 +749,44 @@ class STUNClient extends EventEmitter {
|
|
|
593
749
|
relayedAddress: attributes.xorRelayedAddress.address,
|
|
594
750
|
relayedPort: attributes.xorRelayedAddress.port,
|
|
595
751
|
lifetime: attributes.lifetime || 600,
|
|
596
|
-
type: 'relay'
|
|
752
|
+
type: 'relay',
|
|
597
753
|
});
|
|
598
754
|
} else {
|
|
599
755
|
transaction.reject(new Error('No relayed address in ALLOCATE response'));
|
|
600
756
|
}
|
|
601
|
-
this
|
|
757
|
+
this.#transactions.delete(transactionKey);
|
|
602
758
|
}
|
|
603
759
|
// Handle TURN Refresh responses
|
|
604
760
|
else if (messageType === STUN_MESSAGE_TYPES.REFRESH_RESPONSE) {
|
|
605
761
|
transaction.resolve({
|
|
606
|
-
lifetime: attributes.lifetime || 600
|
|
762
|
+
lifetime: attributes.lifetime || 600,
|
|
607
763
|
});
|
|
608
|
-
this
|
|
764
|
+
this.#transactions.delete(transactionKey);
|
|
609
765
|
}
|
|
610
|
-
// Handle
|
|
611
|
-
else if (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
});
|
|
618
|
-
}
|
|
766
|
+
// Handle TURN CreatePermission / ChannelBind success responses
|
|
767
|
+
else if (
|
|
768
|
+
messageType === STUN_MESSAGE_TYPES.CREATE_PERMISSION_RESPONSE ||
|
|
769
|
+
messageType === STUN_MESSAGE_TYPES.CHANNEL_BIND_RESPONSE
|
|
770
|
+
) {
|
|
771
|
+
transaction.resolve({ ok: true });
|
|
772
|
+
this.#transactions.delete(transactionKey);
|
|
619
773
|
}
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
774
|
+
// (DATA indications are handled earlier, before the transaction lookup.)
|
|
775
|
+
// Handle error responses generically: any class-of-error message
|
|
776
|
+
// (the 0x0110 bits set). This covers ALLOCATE (0x0113), CreatePermission
|
|
777
|
+
// (0x0118), Refresh (0x0114), Binding (0x0111), etc.
|
|
778
|
+
else if ((messageType & 0x0110) === 0x0110) {
|
|
779
|
+
// Store realm and nonce so the caller can retry with fresh credentials.
|
|
625
780
|
if (attributes.realm) {
|
|
626
|
-
this
|
|
781
|
+
this.#realm = attributes.realm;
|
|
627
782
|
}
|
|
628
783
|
if (attributes.nonce) {
|
|
629
|
-
this
|
|
784
|
+
this.#nonce = attributes.nonce;
|
|
630
785
|
}
|
|
631
786
|
|
|
632
787
|
const errorMsg = attributes.errorCode || 'Unknown error';
|
|
633
788
|
transaction.reject(new Error(`STUN error: ${errorMsg}`));
|
|
634
|
-
this
|
|
789
|
+
this.#transactions.delete(transactionKey);
|
|
635
790
|
}
|
|
636
791
|
}
|
|
637
792
|
|
|
@@ -642,8 +797,8 @@ class STUNClient extends EventEmitter {
|
|
|
642
797
|
* @returns {Object} Parsed attributes
|
|
643
798
|
* @private
|
|
644
799
|
*/
|
|
645
|
-
|
|
646
|
-
const attributes = {};
|
|
800
|
+
#parseAttributes(data: Buffer, transactionId: Buffer): ParsedAttributes {
|
|
801
|
+
const attributes: ParsedAttributes = {};
|
|
647
802
|
let offset = 0;
|
|
648
803
|
|
|
649
804
|
while (offset < data.length) {
|
|
@@ -659,27 +814,33 @@ class STUNClient extends EventEmitter {
|
|
|
659
814
|
|
|
660
815
|
switch (type) {
|
|
661
816
|
case STUN_ATTRIBUTES.XOR_MAPPED_ADDRESS:
|
|
662
|
-
attributes.xorMappedAddress = this
|
|
817
|
+
attributes.xorMappedAddress = this.#parseXorAddress(value, transactionId);
|
|
663
818
|
break;
|
|
664
819
|
case STUN_ATTRIBUTES.XOR_RELAYED_ADDRESS:
|
|
665
|
-
attributes.xorRelayedAddress = this
|
|
820
|
+
attributes.xorRelayedAddress = this.#parseXorAddress(value, transactionId);
|
|
821
|
+
break;
|
|
822
|
+
case STUN_ATTRIBUTES.XOR_PEER_ADDRESS:
|
|
823
|
+
attributes.xorPeerAddress = this.#parseXorAddress(value, transactionId);
|
|
824
|
+
break;
|
|
825
|
+
case STUN_ATTRIBUTES.DATA:
|
|
826
|
+
attributes.data = value;
|
|
666
827
|
break;
|
|
667
828
|
case STUN_ATTRIBUTES.MAPPED_ADDRESS:
|
|
668
|
-
attributes.mappedAddress = this
|
|
829
|
+
attributes.mappedAddress = this.#parseAddress(value);
|
|
669
830
|
break;
|
|
670
831
|
case STUN_ATTRIBUTES.LIFETIME:
|
|
671
832
|
attributes.lifetime = value.readUInt32BE(0);
|
|
672
833
|
break;
|
|
673
834
|
case STUN_ATTRIBUTES.ERROR_CODE:
|
|
674
|
-
attributes.errorCode = this
|
|
835
|
+
attributes.errorCode = this.#parseErrorCode(value);
|
|
675
836
|
break;
|
|
676
837
|
case STUN_ATTRIBUTES.REALM:
|
|
677
838
|
attributes.realm = value.toString('utf8');
|
|
678
|
-
this
|
|
839
|
+
this.#realm = attributes.realm;
|
|
679
840
|
break;
|
|
680
841
|
case STUN_ATTRIBUTES.NONCE:
|
|
681
842
|
attributes.nonce = value.toString('utf8');
|
|
682
|
-
this
|
|
843
|
+
this.#nonce = attributes.nonce;
|
|
683
844
|
break;
|
|
684
845
|
}
|
|
685
846
|
|
|
@@ -699,26 +860,22 @@ class STUNClient extends EventEmitter {
|
|
|
699
860
|
* @returns {Object} Address info
|
|
700
861
|
* @private
|
|
701
862
|
*/
|
|
702
|
-
|
|
863
|
+
#parseXorAddress(data: Buffer, _transactionId: Buffer): AddressInfo | null {
|
|
703
864
|
const family = data.readUInt8(1);
|
|
704
865
|
const xorPort = data.readUInt16BE(2);
|
|
705
|
-
|
|
866
|
+
|
|
706
867
|
// XOR port with magic cookie high 16 bits
|
|
707
868
|
const port = xorPort ^ (MAGIC_COOKIE >> 16);
|
|
708
869
|
|
|
709
|
-
if (family === 0x01) {
|
|
870
|
+
if (family === 0x01) {
|
|
871
|
+
// IPv4
|
|
710
872
|
const xorAddress = data.readUInt32BE(4);
|
|
711
873
|
const address = xorAddress ^ MAGIC_COOKIE;
|
|
712
|
-
|
|
874
|
+
|
|
713
875
|
return {
|
|
714
876
|
family: 'IPv4',
|
|
715
877
|
port,
|
|
716
|
-
address: [
|
|
717
|
-
(address >> 24) & 0xFF,
|
|
718
|
-
(address >> 16) & 0xFF,
|
|
719
|
-
(address >> 8) & 0xFF,
|
|
720
|
-
address & 0xFF
|
|
721
|
-
].join('.')
|
|
878
|
+
address: [(address >> 24) & 0xff, (address >> 16) & 0xff, (address >> 8) & 0xff, address & 0xff].join('.'),
|
|
722
879
|
};
|
|
723
880
|
}
|
|
724
881
|
|
|
@@ -731,16 +888,17 @@ class STUNClient extends EventEmitter {
|
|
|
731
888
|
* @returns {Object} Address info
|
|
732
889
|
* @private
|
|
733
890
|
*/
|
|
734
|
-
|
|
891
|
+
#parseAddress(data: Buffer): AddressInfo | null {
|
|
735
892
|
const family = data.readUInt8(1);
|
|
736
893
|
const port = data.readUInt16BE(2);
|
|
737
894
|
|
|
738
|
-
if (family === 0x01) {
|
|
895
|
+
if (family === 0x01) {
|
|
896
|
+
// IPv4
|
|
739
897
|
const address = data.slice(4, 8);
|
|
740
898
|
return {
|
|
741
899
|
family: 'IPv4',
|
|
742
900
|
port,
|
|
743
|
-
address: Array.from(address).join('.')
|
|
901
|
+
address: Array.from(address).join('.'),
|
|
744
902
|
};
|
|
745
903
|
}
|
|
746
904
|
|
|
@@ -753,25 +911,26 @@ class STUNClient extends EventEmitter {
|
|
|
753
911
|
* @returns {string} Error message
|
|
754
912
|
* @private
|
|
755
913
|
*/
|
|
756
|
-
|
|
914
|
+
#parseErrorCode(data: Buffer): string {
|
|
757
915
|
const errorClass = data.readUInt8(2) & 0x07;
|
|
758
916
|
const errorNumber = data.readUInt8(3);
|
|
759
917
|
const errorCode = errorClass * 100 + errorNumber;
|
|
760
918
|
const reason = data.slice(4).toString('utf8');
|
|
761
|
-
|
|
919
|
+
|
|
762
920
|
return `${errorCode} ${reason}`;
|
|
763
921
|
}
|
|
764
922
|
|
|
765
923
|
/**
|
|
766
924
|
* Close the client
|
|
767
925
|
*/
|
|
768
|
-
close() {
|
|
769
|
-
if (this
|
|
770
|
-
this
|
|
771
|
-
this
|
|
926
|
+
close(): void {
|
|
927
|
+
if (this.#socket) {
|
|
928
|
+
this.#socket.close();
|
|
929
|
+
this.#socket = null;
|
|
772
930
|
}
|
|
773
|
-
this
|
|
931
|
+
this.#transactions.clear();
|
|
774
932
|
}
|
|
775
933
|
}
|
|
776
934
|
|
|
777
|
-
|
|
935
|
+
export default STUNClient;
|
|
936
|
+
export { STUNClient };
|