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.
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  });
12
12
  };
13
13
  import { start as innerStart } from './internals/client.js';
14
+ import { resolveDnsOverHttps } from './internals/dns-over-https.js';
14
15
  export { AddChainError, AlreadyDestroyedError, CrashError, JsonRpcDisabledError, QueueFullError } from './public-types.js';
15
16
  /**
16
17
  * Initializes a new client. This is a pre-requisite to connecting to a blockchain.
@@ -185,17 +186,46 @@ function connect(config) {
185
186
  openOutSubstream: () => { throw new Error('Connection is closed'); },
186
187
  };
187
188
  }
188
- const { targetPort, ipVersion, targetIp, remoteTlsCertificateSha256 } = config.address;
189
+ const { targetPort, remoteTlsCertificateSha256 } = config.address;
189
190
  const state = {
190
191
  pc: undefined,
191
192
  dataChannels: new Map(),
192
193
  nextStreamId: 0,
193
194
  isFirstOutSubstream: true,
195
+ dnsAbort: null,
194
196
  };
197
+ // The SDP answer needs the literal IP address of the remote.
198
+ // Browsers expose no DNS API, so a multiaddress with a domain name is resolved over
199
+ // DNS-over-HTTPS. Resolution is started here and is only awaited in
200
+ // `onnegotiationneeded`, where the IP is actually needed.
201
+ let target;
202
+ if ("targetIp" in config.address) {
203
+ target = Promise.resolve({ targetIp: config.address.targetIp, ipVersion: config.address.ipVersion });
204
+ }
205
+ else {
206
+ const { hostname, family } = config.address;
207
+ const controller = new AbortController();
208
+ state.dnsAbort = controller;
209
+ const timeout = setTimeout(() => controller.abort(), 10000);
210
+ const finished = () => {
211
+ clearTimeout(timeout);
212
+ state.dnsAbort = null;
213
+ };
214
+ target = resolveDnsOverHttps(hostname, family, controller.signal).then((ip) => {
215
+ finished();
216
+ return { targetIp: ip, ipVersion: ip.includes(':') ? 6 : 4 };
217
+ }, (error) => {
218
+ finished();
219
+ throw error;
220
+ });
221
+ target.catch(() => { });
222
+ }
195
223
  // Kills all the JavaScript objects (the connection and all its substreams), ensuring that no
196
224
  // callback will be called again. Doesn't report anything to smoldot, as this should be done
197
225
  // by the caller.
198
226
  const killAllJs = () => {
227
+ if (state.dnsAbort)
228
+ state.dnsAbort.abort();
199
229
  // The `RTCPeerConnection` is created pretty quickly. It is however still possible for
200
230
  // smoldot to cancel the opening, in which case `pc` will still be undefined.
201
231
  if (!state.pc) {
@@ -216,6 +246,18 @@ function connect(config) {
216
246
  state.dataChannels.clear();
217
247
  state.pc.close(); // Not necessarily necessary, but it doesn't hurt to do so.
218
248
  };
249
+ // Due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1659672>,
250
+ // connections from Firefox to a localhost WebRTC server always fail:
251
+ // Firefox gathers no loopback ICE candidate.
252
+ //
253
+ // Returns `true` if the connection was refused, in which case the caller must stop.
254
+ const refuseIfFirefoxLoopback = (ip) => {
255
+ if ((ip !== '127.0.0.1' && ip !== '::1') || navigator.userAgent.indexOf('Firefox') === -1)
256
+ return false;
257
+ killAllJs();
258
+ config.onConnectionReset("Firefox can't connect to a localhost WebRTC server");
259
+ return true;
260
+ };
219
261
  // Function that configures a newly-opened channel and adds it to the map. Used for both
220
262
  // inbound and outbound substreams.
221
263
  const addChannel = (dataChannel, direction) => {
@@ -278,19 +320,10 @@ function connect(config) {
278
320
  RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }).then((localCertificate) => __awaiter(this, void 0, void 0, function* () {
279
321
  if (state.pc === null)
280
322
  return;
281
- // Due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1659672>, connections from
282
- // Firefox to a localhost WebRTC server always fails. Since this bug has been opened
283
- // for three years at the time of writing, it is unlikely to be fixed in the short
284
- // term. In order to provider better user feedback, we straight up refuse connecting
285
- // and stop the connection.
286
- // Note that this is just a hint. Failing to detect this will lead to the WebRTC
287
- // handshake timing out.
288
- // TODO: eventually remove this if the Firefox bug is fixed
289
- if ((targetIp == 'localhost' || targetIp == '127.0.0.1' || targetIp == '::1') && navigator.userAgent.indexOf('Firefox') !== -1) {
290
- killAllJs();
291
- config.onConnectionReset("Firefox can't connect to a localhost WebRTC server");
323
+ // A literal loopback address can be refused before anything is created. A domain
324
+ // name is checked once resolved, in `onnegotiationneeded`.
325
+ if ("targetIp" in config.address && refuseIfFirefoxLoopback(config.address.targetIp))
292
326
  return;
293
- }
294
327
  // Create a new WebRTC connection.
295
328
  state.pc = new RTCPeerConnection({ certificates: [localCertificate] });
296
329
  // We need to build the multihash corresponding to the local certificate.
@@ -365,6 +398,25 @@ function connect(config) {
365
398
  sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:' + ufragPwd);
366
399
  sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:' + ufragPwd);
367
400
  yield state.pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });
401
+ // The answer needs the literal IP address of the remote, resolution was already stared.
402
+ let targetIp;
403
+ let ipVersion;
404
+ try {
405
+ ({ targetIp, ipVersion } = yield target);
406
+ }
407
+ catch (error) {
408
+ // `reset()` aborts the resolution, and a failure reported through
409
+ // `onconnectionstatechange` closes the connection.
410
+ if (state.pc.signalingState === "closed")
411
+ return;
412
+ killAllJs();
413
+ config.onConnectionReset("DNS resolution failed: " + (error instanceof Error ? error.message : String(error)));
414
+ return;
415
+ }
416
+ if (state.pc.signalingState === "closed")
417
+ return;
418
+ if (refuseIfFirefoxLoopback(targetIp))
419
+ return;
368
420
  // Transform certificate hash into fingerprint (upper-hex; each byte separated by ":").
369
421
  const fingerprint = Array.from(remoteTlsCertificateSha256).map((n) => ("0" + n.toString(16)).slice(-2).toUpperCase()).join(':');
370
422
  // Note that the trailing line feed is important, as otherwise Chrome
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoldot",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Light client that connects to Polkadot and Substrate-based blockchains",
5
5
  "contributors": [
6
6
  "Parity Technologies <admin@parity.io>",