node-rtc-connection 2.0.4 → 2.0.6
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/index.cjs +1 -0
- package/index.mjs +1 -0
- package/package.json +10 -47
- package/{dist/types → types}/stun/stun-client.d.ts +2 -0
- package/dist/index.cjs +0 -32
- package/dist/index.mjs +0 -43
- package/src/crypto/der.ts +0 -205
- package/src/crypto/x509.ts +0 -146
- package/src/datachannel/RTCDataChannel.ts +0 -388
- package/src/dtls/RTCCertificate.ts +0 -396
- package/src/dtls/cipher.ts +0 -198
- package/src/dtls/connection.ts +0 -974
- package/src/dtls/prf.ts +0 -62
- package/src/dtls/protocol.ts +0 -204
- package/src/foundation/ByteBufferQueue.ts +0 -237
- package/src/foundation/RTCError.ts +0 -276
- package/src/ice/RTCIceCandidate.ts +0 -349
- package/src/ice/ice-agent.ts +0 -609
- package/src/ice/stun-message.ts +0 -260
- package/src/index.ts +0 -72
- package/src/peerconnection/RTCPeerConnection.ts +0 -430
- package/src/sctp/association.ts +0 -523
- package/src/sctp/chunks.ts +0 -350
- package/src/sctp/crc32c.ts +0 -57
- package/src/sctp/datachannel-manager.ts +0 -187
- package/src/sctp/dcep.ts +0 -94
- package/src/sdp/RTCSessionDescription.ts +0 -115
- package/src/sdp/sdp-utils.ts +0 -229
- package/src/stun/stun-client.ts +0 -936
- package/src/transport-stack.ts +0 -165
- /package/{dist/types → types}/crypto/der.d.ts +0 -0
- /package/{dist/types → types}/crypto/x509.d.ts +0 -0
- /package/{dist/types → types}/datachannel/RTCDataChannel.d.ts +0 -0
- /package/{dist/types → types}/dtls/RTCCertificate.d.ts +0 -0
- /package/{dist/types → types}/dtls/cipher.d.ts +0 -0
- /package/{dist/types → types}/dtls/connection.d.ts +0 -0
- /package/{dist/types → types}/dtls/prf.d.ts +0 -0
- /package/{dist/types → types}/dtls/protocol.d.ts +0 -0
- /package/{dist/types → types}/foundation/ByteBufferQueue.d.ts +0 -0
- /package/{dist/types → types}/foundation/RTCError.d.ts +0 -0
- /package/{dist/types → types}/ice/RTCIceCandidate.d.ts +0 -0
- /package/{dist/types → types}/ice/ice-agent.d.ts +0 -0
- /package/{dist/types → types}/ice/stun-message.d.ts +0 -0
- /package/{dist/types → types}/index.d.ts +0 -0
- /package/{dist/types → types}/peerconnection/RTCPeerConnection.d.ts +0 -0
- /package/{dist/types → types}/sctp/association.d.ts +0 -0
- /package/{dist/types → types}/sctp/chunks.d.ts +0 -0
- /package/{dist/types → types}/sctp/crc32c.d.ts +0 -0
- /package/{dist/types → types}/sctp/datachannel-manager.d.ts +0 -0
- /package/{dist/types → types}/sctp/dcep.d.ts +0 -0
- /package/{dist/types → types}/sdp/RTCSessionDescription.d.ts +0 -0
- /package/{dist/types → types}/sdp/sdp-utils.d.ts +0 -0
- /package/{dist/types → types}/transport-stack.d.ts +0 -0
package/src/stun/stun-client.ts
DELETED
|
@@ -1,936 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file stun-client.ts
|
|
3
|
-
* @description STUN (Session Traversal Utilities for NAT) client implementation
|
|
4
|
-
* @module stun/stun-client
|
|
5
|
-
*
|
|
6
|
-
* STUN Protocol: RFC 5389
|
|
7
|
-
* TURN Protocol: RFC 5766
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
import * as dgram from 'dgram';
|
|
13
|
-
import * as crypto from 'crypto';
|
|
14
|
-
|
|
15
|
-
import { EventEmitter } from 'events';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* STUN message types
|
|
19
|
-
*/
|
|
20
|
-
const STUN_MESSAGE_TYPES = {
|
|
21
|
-
BINDING_REQUEST: 0x0001,
|
|
22
|
-
BINDING_RESPONSE: 0x0101,
|
|
23
|
-
BINDING_ERROR_RESPONSE: 0x0111,
|
|
24
|
-
|
|
25
|
-
// TURN
|
|
26
|
-
ALLOCATE_REQUEST: 0x0003,
|
|
27
|
-
ALLOCATE_RESPONSE: 0x0103,
|
|
28
|
-
ALLOCATE_ERROR_RESPONSE: 0x0113,
|
|
29
|
-
|
|
30
|
-
REFRESH_REQUEST: 0x0004,
|
|
31
|
-
REFRESH_RESPONSE: 0x0104,
|
|
32
|
-
|
|
33
|
-
SEND_INDICATION: 0x0016,
|
|
34
|
-
DATA_INDICATION: 0x0017,
|
|
35
|
-
|
|
36
|
-
CREATE_PERMISSION_REQUEST: 0x0008,
|
|
37
|
-
CREATE_PERMISSION_RESPONSE: 0x0108,
|
|
38
|
-
|
|
39
|
-
CHANNEL_BIND_REQUEST: 0x0009,
|
|
40
|
-
CHANNEL_BIND_RESPONSE: 0x0109,
|
|
41
|
-
} as const;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* STUN attribute types
|
|
45
|
-
*/
|
|
46
|
-
const STUN_ATTRIBUTES = {
|
|
47
|
-
MAPPED_ADDRESS: 0x0001,
|
|
48
|
-
USERNAME: 0x0006,
|
|
49
|
-
MESSAGE_INTEGRITY: 0x0008,
|
|
50
|
-
ERROR_CODE: 0x0009,
|
|
51
|
-
UNKNOWN_ATTRIBUTES: 0x000a,
|
|
52
|
-
REALM: 0x0014,
|
|
53
|
-
NONCE: 0x0015,
|
|
54
|
-
XOR_MAPPED_ADDRESS: 0x0020,
|
|
55
|
-
|
|
56
|
-
// TURN
|
|
57
|
-
CHANNEL_NUMBER: 0x000c,
|
|
58
|
-
LIFETIME: 0x000d,
|
|
59
|
-
XOR_PEER_ADDRESS: 0x0012,
|
|
60
|
-
DATA: 0x0013,
|
|
61
|
-
XOR_RELAYED_ADDRESS: 0x0016,
|
|
62
|
-
REQUESTED_TRANSPORT: 0x0019,
|
|
63
|
-
|
|
64
|
-
SOFTWARE: 0x8022,
|
|
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
|
-
}
|
|
87
|
-
|
|
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
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* @class STUNClient
|
|
178
|
-
* @description STUN/TURN client for NAT traversal
|
|
179
|
-
*/
|
|
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
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Create a STUN client
|
|
192
|
-
* @param {Object} options - Client options
|
|
193
|
-
* @param {string} options.server - STUN/TURN server address
|
|
194
|
-
* @param {number} options.port - Server port
|
|
195
|
-
* @param {string} [options.username] - TURN username
|
|
196
|
-
* @param {string} [options.credential] - TURN password
|
|
197
|
-
* @param {string} [options.transport='udp'] - Transport protocol (udp/tcp)
|
|
198
|
-
* @param {Object} [options.params={}] - Additional query parameters from URL
|
|
199
|
-
*/
|
|
200
|
-
constructor(options: STUNClientOptions) {
|
|
201
|
-
super();
|
|
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;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Connect to the STUN/TURN server
|
|
215
|
-
* @returns {Promise<void>}
|
|
216
|
-
*/
|
|
217
|
-
async connect(): Promise<void> {
|
|
218
|
-
if (this.#socket) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
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);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
socket.on('error', (err: Error) => {
|
|
231
|
-
console.error('STUN socket error:', err);
|
|
232
|
-
reject(err);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
socket.bind(() => {
|
|
236
|
-
resolve();
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Send a STUN Binding Request to get reflexive address
|
|
243
|
-
* @returns {Promise<Object>} Reflexive address info
|
|
244
|
-
*/
|
|
245
|
-
async getReflexiveAddress(): Promise<TransactionResult> {
|
|
246
|
-
await this.connect();
|
|
247
|
-
|
|
248
|
-
const transactionId = crypto.randomBytes(12);
|
|
249
|
-
const request = this.#createBindingRequest(transactionId);
|
|
250
|
-
|
|
251
|
-
return new Promise<TransactionResult>((resolve, reject) => {
|
|
252
|
-
const timeout = setTimeout(() => {
|
|
253
|
-
this.#transactions.delete(transactionId.toString('hex'));
|
|
254
|
-
reject(new Error('STUN request timeout'));
|
|
255
|
-
}, 5000);
|
|
256
|
-
|
|
257
|
-
this.#transactions.set(transactionId.toString('hex'), {
|
|
258
|
-
resolve: (result: TransactionResult) => {
|
|
259
|
-
clearTimeout(timeout);
|
|
260
|
-
resolve(result);
|
|
261
|
-
},
|
|
262
|
-
reject: (error: Error) => {
|
|
263
|
-
clearTimeout(timeout);
|
|
264
|
-
reject(error);
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
this.#socket!.send(request, this.#port, this.#server, (err) => {
|
|
269
|
-
if (err) {
|
|
270
|
-
clearTimeout(timeout);
|
|
271
|
-
this.#transactions.delete(transactionId.toString('hex'));
|
|
272
|
-
reject(err);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Send a TURN Allocate Request to get relay address
|
|
280
|
-
* @param {number} [lifetime=600] - Allocation lifetime in seconds
|
|
281
|
-
* @returns {Promise<Object>} Relay address info
|
|
282
|
-
*/
|
|
283
|
-
async allocateRelay(lifetime: number = 600): Promise<TransactionResult> {
|
|
284
|
-
if (!this.#username || !this.#credential) {
|
|
285
|
-
throw new Error('TURN requires username and credential');
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
await this.connect();
|
|
289
|
-
|
|
290
|
-
let transactionId = crypto.randomBytes(12);
|
|
291
|
-
let request = this.#createAllocateRequest(transactionId, lifetime);
|
|
292
|
-
|
|
293
|
-
// First attempt without credentials to get realm and nonce
|
|
294
|
-
try {
|
|
295
|
-
return await this.#sendRequest(request, transactionId, 'allocate');
|
|
296
|
-
} catch (error) {
|
|
297
|
-
// If we get 401 Unauthorized, retry with credentials
|
|
298
|
-
if (error instanceof Error && error.message.includes('401') && this.#realm && this.#nonce) {
|
|
299
|
-
// Create new transaction ID for retry
|
|
300
|
-
transactionId = crypto.randomBytes(12);
|
|
301
|
-
request = this.#createAllocateRequest(transactionId, lifetime, true);
|
|
302
|
-
return await this.#sendRequest(request, transactionId, 'allocate');
|
|
303
|
-
}
|
|
304
|
-
throw error;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Send a TURN Refresh Request to keep allocation alive
|
|
310
|
-
* @param {number} [lifetime=600] - Allocation lifetime in seconds
|
|
311
|
-
* @returns {Promise<Object>} Updated allocation info
|
|
312
|
-
*/
|
|
313
|
-
async refreshAllocation(lifetime: number = 600): Promise<TransactionResult> {
|
|
314
|
-
if (!this.#username || !this.#credential) {
|
|
315
|
-
throw new Error('TURN requires username and credential');
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return this.#withAuthRetry('refresh', () => {
|
|
319
|
-
const transactionId = crypto.randomBytes(12);
|
|
320
|
-
return { transactionId, request: this.#createRefreshRequest(transactionId, lifetime) };
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
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
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Create a TURN Permission for a peer
|
|
347
|
-
* @param {string} peerAddress - Peer IP address
|
|
348
|
-
* @returns {Promise<void>}
|
|
349
|
-
*/
|
|
350
|
-
async createPermission(peerAddress: string): Promise<void> {
|
|
351
|
-
if (!this.#username || !this.#credential) {
|
|
352
|
-
throw new Error('TURN requires username and credential');
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
await this.#withAuthRetry('createPermission', () => {
|
|
356
|
-
const transactionId = crypto.randomBytes(12);
|
|
357
|
-
return { transactionId, request: this.#createCreatePermissionRequest(transactionId, peerAddress) };
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Send data to a peer via TURN Send Indication
|
|
363
|
-
* @param {string} peerAddress - Peer IP address
|
|
364
|
-
* @param {number} peerPort - Peer port
|
|
365
|
-
* @param {Buffer} data - Data to send
|
|
366
|
-
* @returns {Promise<void>}
|
|
367
|
-
*/
|
|
368
|
-
async sendIndication(peerAddress: string, peerPort: number, data: Buffer): Promise<void> {
|
|
369
|
-
if (!this.#username || !this.#credential) {
|
|
370
|
-
throw new Error('TURN requires username and credential');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const transactionId = crypto.randomBytes(12);
|
|
374
|
-
const indication = this.#createSendIndication(transactionId, peerAddress, peerPort, data);
|
|
375
|
-
|
|
376
|
-
// Indications are fire-and-forget, no response expected
|
|
377
|
-
return new Promise<void>((resolve, reject) => {
|
|
378
|
-
this.#socket!.send(indication, this.#port, this.#server, (err) => {
|
|
379
|
-
if (err) reject(err);
|
|
380
|
-
else resolve();
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Send a TURN request
|
|
387
|
-
* @param {Buffer} request - Request message
|
|
388
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
389
|
-
* @param {string} requestType - Type of request
|
|
390
|
-
* @returns {Promise<Object>}
|
|
391
|
-
* @private
|
|
392
|
-
*/
|
|
393
|
-
#sendRequest(request: Buffer, transactionId: Buffer, requestType: string): Promise<TransactionResult> {
|
|
394
|
-
return new Promise<TransactionResult>((resolve, reject) => {
|
|
395
|
-
const timeout = setTimeout(() => {
|
|
396
|
-
this.#transactions.delete(transactionId.toString('hex'));
|
|
397
|
-
reject(new Error(`${requestType} request timeout`));
|
|
398
|
-
}, 5000);
|
|
399
|
-
|
|
400
|
-
this.#transactions.set(transactionId.toString('hex'), {
|
|
401
|
-
type: requestType,
|
|
402
|
-
resolve: (result: TransactionResult) => {
|
|
403
|
-
clearTimeout(timeout);
|
|
404
|
-
resolve(result);
|
|
405
|
-
},
|
|
406
|
-
reject: (error: Error) => {
|
|
407
|
-
clearTimeout(timeout);
|
|
408
|
-
reject(error);
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
this.#socket!.send(request, this.#port, this.#server, (err) => {
|
|
413
|
-
if (err) {
|
|
414
|
-
clearTimeout(timeout);
|
|
415
|
-
this.#transactions.delete(transactionId.toString('hex'));
|
|
416
|
-
reject(err);
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Create a STUN Binding Request
|
|
424
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
425
|
-
* @returns {Buffer} STUN message
|
|
426
|
-
* @private
|
|
427
|
-
*/
|
|
428
|
-
#createBindingRequest(transactionId: Buffer): Buffer {
|
|
429
|
-
const header = Buffer.alloc(20);
|
|
430
|
-
|
|
431
|
-
// Message Type (2 bytes)
|
|
432
|
-
header.writeUInt16BE(STUN_MESSAGE_TYPES.BINDING_REQUEST, 0);
|
|
433
|
-
|
|
434
|
-
// Message Length (2 bytes) - 0 for now, no attributes
|
|
435
|
-
header.writeUInt16BE(0, 2);
|
|
436
|
-
|
|
437
|
-
// Magic Cookie (4 bytes)
|
|
438
|
-
header.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
439
|
-
|
|
440
|
-
// Transaction ID (12 bytes)
|
|
441
|
-
transactionId.copy(header, 8);
|
|
442
|
-
|
|
443
|
-
return header;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Create a TURN Allocate Request
|
|
448
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
449
|
-
* @param {number} lifetime - Allocation lifetime in seconds
|
|
450
|
-
* @param {boolean} withAuth - Include authentication
|
|
451
|
-
* @returns {Buffer} STUN message
|
|
452
|
-
* @private
|
|
453
|
-
*/
|
|
454
|
-
#createAllocateRequest(transactionId: Buffer, lifetime: number, withAuth: boolean = false): Buffer {
|
|
455
|
-
const attributes: Buffer[] = [];
|
|
456
|
-
|
|
457
|
-
// REQUESTED-TRANSPORT (UDP = 17)
|
|
458
|
-
const transport = Buffer.alloc(8);
|
|
459
|
-
transport.writeUInt16BE(STUN_ATTRIBUTES.REQUESTED_TRANSPORT, 0);
|
|
460
|
-
transport.writeUInt16BE(4, 2);
|
|
461
|
-
transport.writeUInt8(17, 4); // UDP
|
|
462
|
-
attributes.push(transport);
|
|
463
|
-
|
|
464
|
-
// LIFETIME
|
|
465
|
-
const lifetimeAttr = Buffer.alloc(8);
|
|
466
|
-
lifetimeAttr.writeUInt16BE(STUN_ATTRIBUTES.LIFETIME, 0);
|
|
467
|
-
lifetimeAttr.writeUInt16BE(4, 2);
|
|
468
|
-
lifetimeAttr.writeUInt32BE(lifetime, 4);
|
|
469
|
-
attributes.push(lifetimeAttr);
|
|
470
|
-
|
|
471
|
-
if (withAuth && this.#realm && this.#nonce) {
|
|
472
|
-
// USERNAME
|
|
473
|
-
const usernameAttr = this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!);
|
|
474
|
-
attributes.push(usernameAttr);
|
|
475
|
-
|
|
476
|
-
// REALM
|
|
477
|
-
const realmAttr = this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm);
|
|
478
|
-
attributes.push(realmAttr);
|
|
479
|
-
|
|
480
|
-
// NONCE
|
|
481
|
-
const nonceAttr = this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce);
|
|
482
|
-
attributes.push(nonceAttr);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return this.#createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Create a TURN CreatePermission Request
|
|
490
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
491
|
-
* @param {string} peerAddress - Peer IP address
|
|
492
|
-
* @returns {Buffer} STUN message
|
|
493
|
-
* @private
|
|
494
|
-
*/
|
|
495
|
-
#createCreatePermissionRequest(transactionId: Buffer, peerAddress: string): Buffer {
|
|
496
|
-
const attributes: Buffer[] = [];
|
|
497
|
-
|
|
498
|
-
// XOR-PEER-ADDRESS
|
|
499
|
-
const peerAttr = this.#createXorPeerAddressAttribute(peerAddress, 0, transactionId);
|
|
500
|
-
attributes.push(peerAttr);
|
|
501
|
-
|
|
502
|
-
// Auth attributes
|
|
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));
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return this.#createMessage(STUN_MESSAGE_TYPES.CREATE_PERMISSION_REQUEST, transactionId, attributes, true);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Create a TURN Send Indication
|
|
514
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
515
|
-
* @param {string} peerAddress - Peer IP address
|
|
516
|
-
* @param {number} peerPort - Peer port
|
|
517
|
-
* @param {Buffer} data - Data to send
|
|
518
|
-
* @returns {Buffer} STUN message
|
|
519
|
-
* @private
|
|
520
|
-
*/
|
|
521
|
-
#createSendIndication(transactionId: Buffer, peerAddress: string, peerPort: number, data: Buffer): Buffer {
|
|
522
|
-
const attributes: Buffer[] = [];
|
|
523
|
-
|
|
524
|
-
// XOR-PEER-ADDRESS
|
|
525
|
-
const peerAttr = this.#createXorPeerAddressAttribute(peerAddress, peerPort, transactionId);
|
|
526
|
-
attributes.push(peerAttr);
|
|
527
|
-
|
|
528
|
-
// DATA
|
|
529
|
-
const dataAttr = Buffer.alloc(4 + data.length + ((4 - (data.length % 4)) % 4));
|
|
530
|
-
dataAttr.writeUInt16BE(STUN_ATTRIBUTES.DATA, 0);
|
|
531
|
-
dataAttr.writeUInt16BE(data.length, 2);
|
|
532
|
-
data.copy(dataAttr, 4);
|
|
533
|
-
attributes.push(dataAttr);
|
|
534
|
-
|
|
535
|
-
return this.#createMessage(STUN_MESSAGE_TYPES.SEND_INDICATION, transactionId, attributes, false);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Create XOR-PEER-ADDRESS attribute
|
|
540
|
-
* @param {string} address - IP address
|
|
541
|
-
* @param {number} port - Port
|
|
542
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
543
|
-
* @returns {Buffer} Attribute buffer
|
|
544
|
-
* @private
|
|
545
|
-
*/
|
|
546
|
-
#createXorPeerAddressAttribute(address: string, port: number, _transactionId: Buffer): Buffer {
|
|
547
|
-
const family = 0x01; // IPv4
|
|
548
|
-
const buffer = Buffer.alloc(4 + 8); // Type(2) + Length(2) + Reserved(1) + Family(1) + Port(2) + Address(4)
|
|
549
|
-
|
|
550
|
-
buffer.writeUInt16BE(STUN_ATTRIBUTES.XOR_PEER_ADDRESS, 0);
|
|
551
|
-
buffer.writeUInt16BE(8, 2);
|
|
552
|
-
buffer.writeUInt8(0, 4);
|
|
553
|
-
buffer.writeUInt8(family, 5);
|
|
554
|
-
|
|
555
|
-
// XOR Port
|
|
556
|
-
const xorPort = port ^ (MAGIC_COOKIE >> 16);
|
|
557
|
-
buffer.writeUInt16BE(xorPort, 6);
|
|
558
|
-
|
|
559
|
-
// XOR Address
|
|
560
|
-
const parts = address.split('.').map(Number);
|
|
561
|
-
const addrInt = (parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]!;
|
|
562
|
-
const xorAddr = addrInt ^ MAGIC_COOKIE;
|
|
563
|
-
|
|
564
|
-
buffer.writeUInt32BE(xorAddr >>> 0, 8); // Ensure unsigned
|
|
565
|
-
|
|
566
|
-
return buffer;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Create a TURN Refresh Request
|
|
571
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
572
|
-
* @param {number} lifetime - Allocation lifetime in seconds
|
|
573
|
-
* @returns {Buffer} STUN message
|
|
574
|
-
* @private
|
|
575
|
-
*/
|
|
576
|
-
#createRefreshRequest(transactionId: Buffer, lifetime: number): Buffer {
|
|
577
|
-
const attributes: Buffer[] = [];
|
|
578
|
-
|
|
579
|
-
// LIFETIME
|
|
580
|
-
const lifetimeAttr = Buffer.alloc(8);
|
|
581
|
-
lifetimeAttr.writeUInt16BE(STUN_ATTRIBUTES.LIFETIME, 0);
|
|
582
|
-
lifetimeAttr.writeUInt16BE(4, 2);
|
|
583
|
-
lifetimeAttr.writeUInt32BE(lifetime, 4);
|
|
584
|
-
attributes.push(lifetimeAttr);
|
|
585
|
-
|
|
586
|
-
// USERNAME
|
|
587
|
-
const usernameAttr = this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!);
|
|
588
|
-
attributes.push(usernameAttr);
|
|
589
|
-
|
|
590
|
-
// REALM
|
|
591
|
-
if (this.#realm) {
|
|
592
|
-
const realmAttr = this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm);
|
|
593
|
-
attributes.push(realmAttr);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// NONCE
|
|
597
|
-
if (this.#nonce) {
|
|
598
|
-
const nonceAttr = this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce);
|
|
599
|
-
attributes.push(nonceAttr);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
return this.#createMessage(STUN_MESSAGE_TYPES.REFRESH_REQUEST, transactionId, attributes, true);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Create a STUN message with attributes
|
|
607
|
-
* @param {number} messageType - Message type
|
|
608
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
609
|
-
* @param {Array<Buffer>} attributes - Attribute buffers
|
|
610
|
-
* @param {boolean} withIntegrity - Add MESSAGE-INTEGRITY
|
|
611
|
-
* @returns {Buffer} Complete STUN message
|
|
612
|
-
* @private
|
|
613
|
-
*/
|
|
614
|
-
#createMessage(
|
|
615
|
-
messageType: number,
|
|
616
|
-
transactionId: Buffer,
|
|
617
|
-
attributes: Buffer[],
|
|
618
|
-
withIntegrity: boolean = false
|
|
619
|
-
): Buffer {
|
|
620
|
-
let attributesBuffer = Buffer.concat(attributes);
|
|
621
|
-
|
|
622
|
-
// Add MESSAGE-INTEGRITY if needed
|
|
623
|
-
if (withIntegrity && this.#credential) {
|
|
624
|
-
const tempHeader = Buffer.alloc(20);
|
|
625
|
-
tempHeader.writeUInt16BE(messageType, 0);
|
|
626
|
-
tempHeader.writeUInt16BE(attributesBuffer.length + 24, 2); // +24 for MESSAGE-INTEGRITY
|
|
627
|
-
tempHeader.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
628
|
-
transactionId.copy(tempHeader, 8);
|
|
629
|
-
|
|
630
|
-
const tempMessage = Buffer.concat([tempHeader, attributesBuffer]);
|
|
631
|
-
|
|
632
|
-
// For TURN, compute key as MD5(username:realm:password) per RFC 5766
|
|
633
|
-
let key: string | Buffer = this.#credential;
|
|
634
|
-
if (this.#username && this.#realm) {
|
|
635
|
-
const keyString = `${this.#username}:${this.#realm}:${this.#credential}`;
|
|
636
|
-
key = crypto.createHash('md5').update(keyString).digest();
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const hmac = crypto.createHmac('sha1', key);
|
|
640
|
-
hmac.update(tempMessage);
|
|
641
|
-
const integrity = hmac.digest();
|
|
642
|
-
|
|
643
|
-
const integrityAttr = Buffer.alloc(4 + integrity.length);
|
|
644
|
-
integrityAttr.writeUInt16BE(STUN_ATTRIBUTES.MESSAGE_INTEGRITY, 0);
|
|
645
|
-
integrityAttr.writeUInt16BE(integrity.length, 2);
|
|
646
|
-
integrity.copy(integrityAttr, 4);
|
|
647
|
-
|
|
648
|
-
attributesBuffer = Buffer.concat([attributesBuffer, integrityAttr]);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Create final message
|
|
652
|
-
const header = Buffer.alloc(20);
|
|
653
|
-
header.writeUInt16BE(messageType, 0);
|
|
654
|
-
header.writeUInt16BE(attributesBuffer.length, 2);
|
|
655
|
-
header.writeUInt32BE(MAGIC_COOKIE, 4);
|
|
656
|
-
transactionId.copy(header, 8);
|
|
657
|
-
|
|
658
|
-
return Buffer.concat([header, attributesBuffer]);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Create a string attribute
|
|
663
|
-
* @param {number} type - Attribute type
|
|
664
|
-
* @param {string} value - String value
|
|
665
|
-
* @returns {Buffer} Attribute buffer
|
|
666
|
-
* @private
|
|
667
|
-
*/
|
|
668
|
-
#createStringAttribute(type: number, value: string): Buffer {
|
|
669
|
-
const valueBuffer = Buffer.from(value, 'utf8');
|
|
670
|
-
const length = valueBuffer.length;
|
|
671
|
-
const padding = (4 - (length % 4)) % 4;
|
|
672
|
-
const buffer = Buffer.alloc(4 + length + padding);
|
|
673
|
-
|
|
674
|
-
buffer.writeUInt16BE(type, 0);
|
|
675
|
-
buffer.writeUInt16BE(length, 2);
|
|
676
|
-
valueBuffer.copy(buffer, 4);
|
|
677
|
-
|
|
678
|
-
return buffer;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Handle incoming STUN message
|
|
683
|
-
* @param {Buffer} msg - Message buffer
|
|
684
|
-
* @param {Object} rinfo - Remote info
|
|
685
|
-
* @private
|
|
686
|
-
*/
|
|
687
|
-
#handleMessage(msg: Buffer, _rinfo: dgram.RemoteInfo): void {
|
|
688
|
-
if (msg.length < 20) {
|
|
689
|
-
return; // Invalid STUN message
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
const messageType = msg.readUInt16BE(0);
|
|
693
|
-
const messageLength = msg.readUInt16BE(2);
|
|
694
|
-
const magicCookie = msg.readUInt32BE(4);
|
|
695
|
-
const transactionId = msg.slice(8, 20);
|
|
696
|
-
|
|
697
|
-
if (magicCookie !== MAGIC_COOKIE) {
|
|
698
|
-
return; // Not a STUN message
|
|
699
|
-
}
|
|
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
|
-
|
|
717
|
-
const transactionKey = transactionId.toString('hex');
|
|
718
|
-
const transaction = this.#transactions.get(transactionKey);
|
|
719
|
-
|
|
720
|
-
if (!transaction) {
|
|
721
|
-
return; // Unknown transaction
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const attributes = this.#parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
|
|
725
|
-
|
|
726
|
-
// Handle STUN Binding responses
|
|
727
|
-
if (messageType === STUN_MESSAGE_TYPES.BINDING_RESPONSE) {
|
|
728
|
-
if (attributes.xorMappedAddress) {
|
|
729
|
-
transaction.resolve({
|
|
730
|
-
address: attributes.xorMappedAddress.address,
|
|
731
|
-
port: attributes.xorMappedAddress.port,
|
|
732
|
-
family: attributes.xorMappedAddress.family,
|
|
733
|
-
});
|
|
734
|
-
} else if (attributes.mappedAddress) {
|
|
735
|
-
transaction.resolve({
|
|
736
|
-
address: attributes.mappedAddress.address,
|
|
737
|
-
port: attributes.mappedAddress.port,
|
|
738
|
-
family: attributes.mappedAddress.family,
|
|
739
|
-
});
|
|
740
|
-
} else {
|
|
741
|
-
transaction.reject(new Error('No mapped address in STUN response'));
|
|
742
|
-
}
|
|
743
|
-
this.#transactions.delete(transactionKey);
|
|
744
|
-
}
|
|
745
|
-
// Handle TURN Allocate responses
|
|
746
|
-
else if (messageType === STUN_MESSAGE_TYPES.ALLOCATE_RESPONSE) {
|
|
747
|
-
if (attributes.xorRelayedAddress) {
|
|
748
|
-
transaction.resolve({
|
|
749
|
-
relayedAddress: attributes.xorRelayedAddress.address,
|
|
750
|
-
relayedPort: attributes.xorRelayedAddress.port,
|
|
751
|
-
lifetime: attributes.lifetime || 600,
|
|
752
|
-
type: 'relay',
|
|
753
|
-
});
|
|
754
|
-
} else {
|
|
755
|
-
transaction.reject(new Error('No relayed address in ALLOCATE response'));
|
|
756
|
-
}
|
|
757
|
-
this.#transactions.delete(transactionKey);
|
|
758
|
-
}
|
|
759
|
-
// Handle TURN Refresh responses
|
|
760
|
-
else if (messageType === STUN_MESSAGE_TYPES.REFRESH_RESPONSE) {
|
|
761
|
-
transaction.resolve({
|
|
762
|
-
lifetime: attributes.lifetime || 600,
|
|
763
|
-
});
|
|
764
|
-
this.#transactions.delete(transactionKey);
|
|
765
|
-
}
|
|
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);
|
|
773
|
-
}
|
|
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.
|
|
780
|
-
if (attributes.realm) {
|
|
781
|
-
this.#realm = attributes.realm;
|
|
782
|
-
}
|
|
783
|
-
if (attributes.nonce) {
|
|
784
|
-
this.#nonce = attributes.nonce;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const errorMsg = attributes.errorCode || 'Unknown error';
|
|
788
|
-
transaction.reject(new Error(`STUN error: ${errorMsg}`));
|
|
789
|
-
this.#transactions.delete(transactionKey);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Parse STUN attributes
|
|
795
|
-
* @param {Buffer} data - Attributes data
|
|
796
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
797
|
-
* @returns {Object} Parsed attributes
|
|
798
|
-
* @private
|
|
799
|
-
*/
|
|
800
|
-
#parseAttributes(data: Buffer, transactionId: Buffer): ParsedAttributes {
|
|
801
|
-
const attributes: ParsedAttributes = {};
|
|
802
|
-
let offset = 0;
|
|
803
|
-
|
|
804
|
-
while (offset < data.length) {
|
|
805
|
-
if (offset + 4 > data.length) break;
|
|
806
|
-
|
|
807
|
-
const type = data.readUInt16BE(offset);
|
|
808
|
-
const length = data.readUInt16BE(offset + 2);
|
|
809
|
-
offset += 4;
|
|
810
|
-
|
|
811
|
-
if (offset + length > data.length) break;
|
|
812
|
-
|
|
813
|
-
const value = data.slice(offset, offset + length);
|
|
814
|
-
|
|
815
|
-
switch (type) {
|
|
816
|
-
case STUN_ATTRIBUTES.XOR_MAPPED_ADDRESS:
|
|
817
|
-
attributes.xorMappedAddress = this.#parseXorAddress(value, transactionId);
|
|
818
|
-
break;
|
|
819
|
-
case STUN_ATTRIBUTES.XOR_RELAYED_ADDRESS:
|
|
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;
|
|
827
|
-
break;
|
|
828
|
-
case STUN_ATTRIBUTES.MAPPED_ADDRESS:
|
|
829
|
-
attributes.mappedAddress = this.#parseAddress(value);
|
|
830
|
-
break;
|
|
831
|
-
case STUN_ATTRIBUTES.LIFETIME:
|
|
832
|
-
attributes.lifetime = value.readUInt32BE(0);
|
|
833
|
-
break;
|
|
834
|
-
case STUN_ATTRIBUTES.ERROR_CODE:
|
|
835
|
-
attributes.errorCode = this.#parseErrorCode(value);
|
|
836
|
-
break;
|
|
837
|
-
case STUN_ATTRIBUTES.REALM:
|
|
838
|
-
attributes.realm = value.toString('utf8');
|
|
839
|
-
this.#realm = attributes.realm;
|
|
840
|
-
break;
|
|
841
|
-
case STUN_ATTRIBUTES.NONCE:
|
|
842
|
-
attributes.nonce = value.toString('utf8');
|
|
843
|
-
this.#nonce = attributes.nonce;
|
|
844
|
-
break;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Pad to 4-byte boundary
|
|
848
|
-
offset += length;
|
|
849
|
-
const padding = (4 - (length % 4)) % 4;
|
|
850
|
-
offset += padding;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
return attributes;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Parse XOR-MAPPED-ADDRESS attribute
|
|
858
|
-
* @param {Buffer} data - Attribute data
|
|
859
|
-
* @param {Buffer} transactionId - Transaction ID
|
|
860
|
-
* @returns {Object} Address info
|
|
861
|
-
* @private
|
|
862
|
-
*/
|
|
863
|
-
#parseXorAddress(data: Buffer, _transactionId: Buffer): AddressInfo | null {
|
|
864
|
-
const family = data.readUInt8(1);
|
|
865
|
-
const xorPort = data.readUInt16BE(2);
|
|
866
|
-
|
|
867
|
-
// XOR port with magic cookie high 16 bits
|
|
868
|
-
const port = xorPort ^ (MAGIC_COOKIE >> 16);
|
|
869
|
-
|
|
870
|
-
if (family === 0x01) {
|
|
871
|
-
// IPv4
|
|
872
|
-
const xorAddress = data.readUInt32BE(4);
|
|
873
|
-
const address = xorAddress ^ MAGIC_COOKIE;
|
|
874
|
-
|
|
875
|
-
return {
|
|
876
|
-
family: 'IPv4',
|
|
877
|
-
port,
|
|
878
|
-
address: [(address >> 24) & 0xff, (address >> 16) & 0xff, (address >> 8) & 0xff, address & 0xff].join('.'),
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
return null;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* Parse MAPPED-ADDRESS attribute
|
|
887
|
-
* @param {Buffer} data - Attribute data
|
|
888
|
-
* @returns {Object} Address info
|
|
889
|
-
* @private
|
|
890
|
-
*/
|
|
891
|
-
#parseAddress(data: Buffer): AddressInfo | null {
|
|
892
|
-
const family = data.readUInt8(1);
|
|
893
|
-
const port = data.readUInt16BE(2);
|
|
894
|
-
|
|
895
|
-
if (family === 0x01) {
|
|
896
|
-
// IPv4
|
|
897
|
-
const address = data.slice(4, 8);
|
|
898
|
-
return {
|
|
899
|
-
family: 'IPv4',
|
|
900
|
-
port,
|
|
901
|
-
address: Array.from(address).join('.'),
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
return null;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
/**
|
|
909
|
-
* Parse ERROR-CODE attribute
|
|
910
|
-
* @param {Buffer} data - Attribute data
|
|
911
|
-
* @returns {string} Error message
|
|
912
|
-
* @private
|
|
913
|
-
*/
|
|
914
|
-
#parseErrorCode(data: Buffer): string {
|
|
915
|
-
const errorClass = data.readUInt8(2) & 0x07;
|
|
916
|
-
const errorNumber = data.readUInt8(3);
|
|
917
|
-
const errorCode = errorClass * 100 + errorNumber;
|
|
918
|
-
const reason = data.slice(4).toString('utf8');
|
|
919
|
-
|
|
920
|
-
return `${errorCode} ${reason}`;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Close the client
|
|
925
|
-
*/
|
|
926
|
-
close(): void {
|
|
927
|
-
if (this.#socket) {
|
|
928
|
-
this.#socket.close();
|
|
929
|
-
this.#socket = null;
|
|
930
|
-
}
|
|
931
|
-
this.#transactions.clear();
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
export default STUNClient;
|
|
936
|
-
export { STUNClient };
|