smoldot 3.5.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
@@ -39,6 +39,17 @@ function startWithBytecode(options) {
39
39
  });
40
40
  }
41
41
  exports.startWithBytecode = startWithBytecode;
42
+ /**
43
+ * Human-readable reason for a socket error. An `AggregateError` (every address of a
44
+ * multi-address host failed) has an empty `message`; its causes are in `errors`.
45
+ */
46
+ function errorReason(error) {
47
+ var _a;
48
+ return ((_a = error.errors) === null || _a === void 0 ? void 0 : _a.map((cause) => cause.message).filter(Boolean).join("; "))
49
+ || error.message
50
+ || error.code
51
+ || "Error";
52
+ }
42
53
  /**
43
54
  * Tries to open a new connection using the given configuration.
44
55
  *
@@ -86,7 +97,7 @@ function connect(config) {
86
97
  socket.onerror = () => { };
87
98
  };
88
99
  socket.onerror = (event) => {
89
- config.onConnectionReset(event.message);
100
+ config.onConnectionReset(errorReason(event.error));
90
101
  socket.onopen = () => { };
91
102
  socket.onclose = () => { };
92
103
  socket.onmessage = () => { };
@@ -126,30 +137,35 @@ function connect(config) {
126
137
  });
127
138
  // Number of bytes queued using `socket.write` and where `write` has returned false.
128
139
  const drainingBytes = { num: 0 };
140
+ // Set by `reset()`. Events that arrive after that must be ignored. This can't be
141
+ // `socket.destroyed`: NodeJS marks the socket destroyed before emitting `close`, so
142
+ // that check would swallow every close, including remote ones.
143
+ let resetCalled = false;
144
+ // Message of the last `error` event, reported through the `close` event that follows it.
145
+ let lastError = null;
129
146
  socket.setNoDelay();
130
147
  socket.on('connect', () => {
131
- if (socket.destroyed)
148
+ if (resetCalled)
132
149
  return;
133
150
  config.onWritableBytes(socket.writableHighWaterMark);
134
151
  });
152
+ socket.on('error', (error) => {
153
+ lastError = errorReason(error);
154
+ });
135
155
  socket.on('close', (hasError) => {
136
- if (socket.destroyed)
156
+ if (resetCalled)
137
157
  return;
138
- // NodeJS doesn't provide a reason why the closing happened, but only
139
- // whether it was caused by an error.
140
- const message = hasError ? "Error" : "Closed gracefully";
141
- config.onConnectionReset(message);
158
+ config.onConnectionReset(lastError !== null && lastError !== void 0 ? lastError : (hasError ? "Error" : "Closed gracefully"));
142
159
  });
143
- socket.on('error', () => { });
144
160
  socket.on('data', (message) => {
145
- if (socket.destroyed)
161
+ if (resetCalled)
146
162
  return;
147
163
  config.onMessage(new Uint8Array(message.buffer));
148
164
  });
149
165
  socket.on('drain', () => {
150
166
  // The bytes queued using `socket.write` and where `write` has returned false have now
151
167
  // been sent. Notify the API that it can write more data.
152
- if (socket.destroyed)
168
+ if (resetCalled)
153
169
  return;
154
170
  const val = drainingBytes.num;
155
171
  drainingBytes.num = 0;
@@ -157,6 +173,7 @@ function connect(config) {
157
173
  });
158
174
  return {
159
175
  reset: () => {
176
+ resetCalled = true;
160
177
  socket.destroy();
161
178
  },
162
179
  send: (data) => {