smoldot 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.startWithBytecode = exports.QueueFullError = exports.JsonRpcDisabledError = exports.CrashError = exports.AlreadyDestroyedError = exports.AddChainError = void 0;
16
16
  const client_js_1 = require("./internals/client.js");
17
+ const dns_over_https_js_1 = require("./internals/dns-over-https.js");
17
18
  var public_types_js_1 = require("./public-types.js");
18
19
  Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return public_types_js_1.AddChainError; } });
19
20
  Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return public_types_js_1.AlreadyDestroyedError; } });
@@ -194,17 +195,46 @@ function connect(config) {
194
195
  openOutSubstream: () => { throw new Error('Connection is closed'); },
195
196
  };
196
197
  }
197
- const { targetPort, ipVersion, targetIp, remoteTlsCertificateSha256 } = config.address;
198
+ const { targetPort, remoteTlsCertificateSha256 } = config.address;
198
199
  const state = {
199
200
  pc: undefined,
200
201
  dataChannels: new Map(),
201
202
  nextStreamId: 0,
202
203
  isFirstOutSubstream: true,
204
+ dnsAbort: null,
203
205
  };
206
+ // The SDP answer needs the literal IP address of the remote.
207
+ // Browsers expose no DNS API, so a multiaddress with a domain name is resolved over
208
+ // DNS-over-HTTPS. Resolution is started here and is only awaited in
209
+ // `onnegotiationneeded`, where the IP is actually needed.
210
+ let target;
211
+ if ("targetIp" in config.address) {
212
+ target = Promise.resolve({ targetIp: config.address.targetIp, ipVersion: config.address.ipVersion });
213
+ }
214
+ else {
215
+ const { hostname, family } = config.address;
216
+ const controller = new AbortController();
217
+ state.dnsAbort = controller;
218
+ const timeout = setTimeout(() => controller.abort(), 10000);
219
+ const finished = () => {
220
+ clearTimeout(timeout);
221
+ state.dnsAbort = null;
222
+ };
223
+ target = (0, dns_over_https_js_1.resolveDnsOverHttps)(hostname, family, controller.signal).then((ip) => {
224
+ finished();
225
+ return { targetIp: ip, ipVersion: ip.includes(':') ? 6 : 4 };
226
+ }, (error) => {
227
+ finished();
228
+ throw error;
229
+ });
230
+ target.catch(() => { });
231
+ }
204
232
  // Kills all the JavaScript objects (the connection and all its substreams), ensuring that no
205
233
  // callback will be called again. Doesn't report anything to smoldot, as this should be done
206
234
  // by the caller.
207
235
  const killAllJs = () => {
236
+ if (state.dnsAbort)
237
+ state.dnsAbort.abort();
208
238
  // The `RTCPeerConnection` is created pretty quickly. It is however still possible for
209
239
  // smoldot to cancel the opening, in which case `pc` will still be undefined.
210
240
  if (!state.pc) {
@@ -225,6 +255,18 @@ function connect(config) {
225
255
  state.dataChannels.clear();
226
256
  state.pc.close(); // Not necessarily necessary, but it doesn't hurt to do so.
227
257
  };
258
+ // Due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1659672>,
259
+ // connections from Firefox to a localhost WebRTC server always fail:
260
+ // Firefox gathers no loopback ICE candidate.
261
+ //
262
+ // Returns `true` if the connection was refused, in which case the caller must stop.
263
+ const refuseIfFirefoxLoopback = (ip) => {
264
+ if ((ip !== '127.0.0.1' && ip !== '::1') || navigator.userAgent.indexOf('Firefox') === -1)
265
+ return false;
266
+ killAllJs();
267
+ config.onConnectionReset("Firefox can't connect to a localhost WebRTC server");
268
+ return true;
269
+ };
228
270
  // Function that configures a newly-opened channel and adds it to the map. Used for both
229
271
  // inbound and outbound substreams.
230
272
  const addChannel = (dataChannel, direction) => {
@@ -287,19 +329,10 @@ function connect(config) {
287
329
  RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }).then((localCertificate) => __awaiter(this, void 0, void 0, function* () {
288
330
  if (state.pc === null)
289
331
  return;
290
- // Due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1659672>, connections from
291
- // Firefox to a localhost WebRTC server always fails. Since this bug has been opened
292
- // for three years at the time of writing, it is unlikely to be fixed in the short
293
- // term. In order to provider better user feedback, we straight up refuse connecting
294
- // and stop the connection.
295
- // Note that this is just a hint. Failing to detect this will lead to the WebRTC
296
- // handshake timing out.
297
- // TODO: eventually remove this if the Firefox bug is fixed
298
- if ((targetIp == 'localhost' || targetIp == '127.0.0.1' || targetIp == '::1') && navigator.userAgent.indexOf('Firefox') !== -1) {
299
- killAllJs();
300
- config.onConnectionReset("Firefox can't connect to a localhost WebRTC server");
332
+ // A literal loopback address can be refused before anything is created. A domain
333
+ // name is checked once resolved, in `onnegotiationneeded`.
334
+ if ("targetIp" in config.address && refuseIfFirefoxLoopback(config.address.targetIp))
301
335
  return;
302
- }
303
336
  // Create a new WebRTC connection.
304
337
  state.pc = new RTCPeerConnection({ certificates: [localCertificate] });
305
338
  // We need to build the multihash corresponding to the local certificate.
@@ -374,6 +407,25 @@ function connect(config) {
374
407
  sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:' + ufragPwd);
375
408
  sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:' + ufragPwd);
376
409
  yield state.pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });
410
+ // The answer needs the literal IP address of the remote, resolution was already stared.
411
+ let targetIp;
412
+ let ipVersion;
413
+ try {
414
+ ({ targetIp, ipVersion } = yield target);
415
+ }
416
+ catch (error) {
417
+ // `reset()` aborts the resolution, and a failure reported through
418
+ // `onconnectionstatechange` closes the connection.
419
+ if (state.pc.signalingState === "closed")
420
+ return;
421
+ killAllJs();
422
+ config.onConnectionReset("DNS resolution failed: " + (error instanceof Error ? error.message : String(error)));
423
+ return;
424
+ }
425
+ if (state.pc.signalingState === "closed")
426
+ return;
427
+ if (refuseIfFirefoxLoopback(targetIp))
428
+ return;
377
429
  // Transform certificate hash into fingerprint (upper-hex; each byte separated by ":").
378
430
  const fingerprint = Array.from(remoteTlsCertificateSha256).map((n) => ("0" + n.toString(16)).slice(-2).toUpperCase()).join(':');
379
431
  // Note that the trailing line feed is important, as otherwise Chrome