smoldot 2.0.8 → 2.0.10

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.
@@ -140,7 +140,7 @@ export interface ConnectionConfig {
140
140
  *
141
141
  * This function must only be called for connections of type "multi-stream".
142
142
  */
143
- onStreamReset: (streamId: number) => void;
143
+ onStreamReset: (streamId: number, message: string) => void;
144
144
  /**
145
145
  * Callback called when some data sent using {@link Connection.send} has effectively been
146
146
  * written on the stream, meaning that some buffer space is now free.
@@ -150,10 +150,10 @@ export function start(options, wasmModule, platformBindings) {
150
150
  throw new Error();
151
151
  state.instance.instance.streamWritableBytes(connectionId, numExtra, streamId);
152
152
  },
153
- onStreamReset(streamId) {
153
+ onStreamReset(streamId, message) {
154
154
  if (state.instance.status !== "ready")
155
155
  throw new Error();
156
- state.instance.instance.streamReset(connectionId, streamId);
156
+ state.instance.instance.streamReset(connectionId, streamId, message);
157
157
  },
158
158
  }));
159
159
  break;
@@ -111,7 +111,7 @@ export interface Instance {
111
111
  streamWritableBytes: (connectionId: number, numExtra: number, streamId?: number) => void;
112
112
  streamMessage: (connectionId: number, message: Uint8Array, streamId?: number) => void;
113
113
  streamOpened: (connectionId: number, streamId: number, direction: 'inbound' | 'outbound') => void;
114
- streamReset: (connectionId: number, streamId: number) => void;
114
+ streamReset: (connectionId: number, streamId: number, message: string) => void;
115
115
  }
116
116
  /**
117
117
  * Starts a new instance using the given configuration.
@@ -435,10 +435,12 @@ export function startLocalInstance(config, wasmModule, eventCallback) {
435
435
  return;
436
436
  state.instance.exports.connection_stream_opened(connectionId, streamId, direction === 'outbound' ? 1 : 0);
437
437
  },
438
- streamReset: (connectionId, streamId) => {
438
+ streamReset: (connectionId, streamId, message) => {
439
439
  if (!state.instance)
440
440
  return;
441
- state.instance.exports.stream_reset(connectionId, streamId);
441
+ state.bufferIndices[0] = new TextEncoder().encode(message);
442
+ state.instance.exports.stream_reset(connectionId, streamId, 0);
443
+ delete state.bufferIndices[0];
442
444
  },
443
445
  };
444
446
  });
@@ -193,9 +193,9 @@ export function connectToInstanceServer(config) {
193
193
  const msg = { ty: "stream-writable-bytes", connectionId, numExtra, streamId };
194
194
  portToServer.postMessage(msg);
195
195
  },
196
- streamReset(connectionId, streamId) {
196
+ streamReset(connectionId, streamId, message) {
197
197
  state.connections.get(connectionId).delete(streamId);
198
- const msg = { ty: "stream-reset", connectionId, streamId };
198
+ const msg = { ty: "stream-reset", connectionId, streamId, message };
199
199
  portToServer.postMessage(msg);
200
200
  },
201
201
  };
@@ -329,7 +329,7 @@ export function startInstanceServer(config, initPortToClient) {
329
329
  if (!state.connections.has(message.connectionId))
330
330
  return;
331
331
  // The stream might have been reset locally in the past.
332
- if (message.streamId && !state.connections.get(message.connectionId).has(message.streamId))
332
+ if (message.streamId !== undefined && !state.connections.get(message.connectionId).has(message.streamId))
333
333
  return;
334
334
  state.instance.streamMessage(message.connectionId, message.message, message.streamId);
335
335
  break;
@@ -347,7 +347,7 @@ export function startInstanceServer(config, initPortToClient) {
347
347
  if (!state.connections.has(message.connectionId))
348
348
  return;
349
349
  // The stream might have been reset locally in the past.
350
- if (message.streamId && !state.connections.get(message.connectionId).has(message.streamId))
350
+ if (message.streamId !== undefined && !state.connections.get(message.connectionId).has(message.streamId))
351
351
  return;
352
352
  state.instance.streamWritableBytes(message.connectionId, message.numExtra, message.streamId);
353
353
  break;
@@ -357,10 +357,10 @@ export function startInstanceServer(config, initPortToClient) {
357
357
  if (!state.connections.has(message.connectionId))
358
358
  return;
359
359
  // The stream might have been reset locally in the past.
360
- if (message.streamId && !state.connections.get(message.connectionId).has(message.streamId))
360
+ if (!state.connections.get(message.connectionId).has(message.streamId))
361
361
  return;
362
362
  state.connections.get(message.connectionId).delete(message.streamId);
363
- state.instance.streamReset(message.connectionId, message.streamId);
363
+ state.instance.streamReset(message.connectionId, message.streamId, message.message);
364
364
  break;
365
365
  }
366
366
  }
@@ -146,56 +146,52 @@ function connect(config) {
146
146
  }
147
147
  else if (config.address.ty === "webrtc") {
148
148
  const { targetPort, ipVersion, targetIp, remoteTlsCertificateSha256 } = config.address;
149
- // TODO: detect localhost for Firefox? https://bugzilla.mozilla.org/show_bug.cgi?id=1659672
150
- // Note that `pc` can be the connection, but also null or undefined.
151
- // `undefined` means "certificate generation in progress", while `null` means "opening must
152
- // be cancelled".
153
- // While it would be better to use for example a string instead of `null`, using `null` lets
154
- // us use the `!` operator more easily and leads to more readable code.
155
- let pc = undefined;
156
- // Contains the data channels that are open and have been reported to smoldot.
157
- const dataChannels = new Map();
158
- // SHA256 hash of the DTLS certificate of the local node. Unknown as long as it hasn't been
159
- // generated.
160
- // TODO: could be merged with `pc` in one variable, and maybe even the other fields as well
161
- let localTlsCertificateSha256;
149
+ const state = {
150
+ pc: undefined,
151
+ dataChannels: new Map(),
152
+ nextStreamId: 0,
153
+ isFirstOutSubstream: true,
154
+ };
162
155
  // Kills all the JavaScript objects (the connection and all its substreams), ensuring that no
163
156
  // callback will be called again. Doesn't report anything to smoldot, as this should be done
164
157
  // by the caller.
165
158
  const killAllJs = () => {
166
159
  // The `RTCPeerConnection` is created pretty quickly. It is however still possible for
167
160
  // smoldot to cancel the opening, in which case `pc` will still be undefined.
168
- if (!pc) {
169
- console.assert(dataChannels.size === 0, "substreams exist while pc is undef");
170
- pc = null;
161
+ if (!state.pc) {
162
+ console.assert(state.dataChannels.size === 0, "substreams exist while pc is undef");
163
+ state.pc = null;
171
164
  return;
172
165
  }
173
- pc.onconnectionstatechange = null;
174
- pc.onnegotiationneeded = null;
175
- pc.ondatachannel = null;
176
- for (const channel of Array.from(dataChannels.values())) {
166
+ state.pc.onconnectionstatechange = null;
167
+ state.pc.onnegotiationneeded = null;
168
+ state.pc.ondatachannel = null;
169
+ for (const channel of Array.from(state.dataChannels.values())) {
177
170
  channel.channel.onopen = null;
178
171
  channel.channel.onerror = null;
179
172
  channel.channel.onclose = null;
180
173
  channel.channel.onbufferedamountlow = null;
181
174
  channel.channel.onmessage = null;
182
175
  }
183
- dataChannels.clear();
184
- pc.close(); // Not necessarily necessary, but it doesn't hurt to do so.
176
+ state.dataChannels.clear();
177
+ state.pc.close(); // Not necessarily necessary, but it doesn't hurt to do so.
185
178
  };
186
179
  // Function that configures a newly-opened channel and adds it to the map. Used for both
187
180
  // inbound and outbound substreams.
188
181
  const addChannel = (dataChannel, direction) => {
189
- const dataChannelId = dataChannel.id;
182
+ const streamId = state.nextStreamId;
183
+ state.nextStreamId += 1;
190
184
  dataChannel.binaryType = 'arraybuffer';
191
185
  let isOpen = { value: false };
192
186
  dataChannel.onopen = () => {
193
187
  console.assert(!isOpen.value, "substream opened twice");
194
188
  isOpen.value = true;
195
- config.onStreamOpened(dataChannelId, direction);
196
- config.onWritableBytes(65536, dataChannelId);
189
+ config.onStreamOpened(streamId, direction);
190
+ config.onWritableBytes(65536, streamId);
197
191
  };
198
- dataChannel.onerror = dataChannel.onclose = (_error) => {
192
+ dataChannel.onerror = dataChannel.onclose = (event) => {
193
+ // Note that Firefox doesn't support <https://developer.mozilla.org/en-US/docs/Web/API/RTCErrorEvent>.
194
+ const message = (event instanceof RTCErrorEvent) ? event.error.toString() : "RTCDataChannel closed";
199
195
  if (!isOpen.value) {
200
196
  // Substream wasn't opened yet and thus has failed to open. The API has no
201
197
  // mechanism to report substream openings failures. We could try opening it
@@ -203,24 +199,30 @@ function connect(config) {
203
199
  // entire connection.
204
200
  killAllJs();
205
201
  // Note that the event doesn't give any additional reason for the failure.
206
- config.onConnectionReset("data channel failed to open");
202
+ config.onConnectionReset("data channel failed to open: " + message);
207
203
  }
208
204
  else {
209
205
  // Substream was open and is now closed. Normal situation.
210
- config.onStreamReset(dataChannelId);
206
+ dataChannel.onopen = null;
207
+ dataChannel.onerror = null;
208
+ dataChannel.onclose = null;
209
+ dataChannel.onbufferedamountlow = null;
210
+ dataChannel.onmessage = null;
211
+ state.dataChannels.delete(streamId);
212
+ config.onStreamReset(streamId, message);
211
213
  }
212
214
  };
213
215
  dataChannel.onbufferedamountlow = () => {
214
- const channel = dataChannels.get(dataChannelId);
216
+ const channel = state.dataChannels.get(streamId);
215
217
  const val = channel.bufferedBytes;
216
218
  channel.bufferedBytes = 0;
217
- config.onWritableBytes(val, dataChannelId);
219
+ config.onWritableBytes(val, streamId);
218
220
  };
219
221
  dataChannel.onmessage = (m) => {
220
222
  // The `data` field is an `ArrayBuffer`.
221
- config.onMessage(new Uint8Array(m.data), dataChannelId);
223
+ config.onMessage(new Uint8Array(m.data), streamId);
222
224
  };
223
- dataChannels.set(dataChannelId, { channel: dataChannel, bufferedBytes: 0 });
225
+ state.dataChannels.set(streamId, { channel: dataChannel, bufferedBytes: 0 });
224
226
  };
225
227
  // It is possible for the browser to use multiple different certificates.
226
228
  // In order for our local certificate to be deterministic, we need to generate it manually and
@@ -228,10 +230,23 @@ function connect(config) {
228
230
  // According to <https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-generatecertificate>,
229
231
  // browsers are guaranteed to support `{ name: "ECDSA", namedCurve: "P-256" }`.
230
232
  RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }).then((localCertificate) => __awaiter(this, void 0, void 0, function* () {
231
- if (pc === null)
233
+ if (state.pc === null)
234
+ return;
235
+ // Due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1659672>, connections from
236
+ // Firefox to a localhost WebRTC server always fails. Since this bug has been opened
237
+ // for three years at the time of writing, it is unlikely to be fixed in the short
238
+ // term. In order to provider better user feedback, we straight up refuse connecting
239
+ // and stop the connection.
240
+ // Note that this is just a hint. Failing to detect this will lead to the WebRTC
241
+ // handshake timing out.
242
+ // TODO: eventually remove this if the Firefox bug is fixed
243
+ if ((targetIp == 'localhost' || targetIp == '127.0.0.1' || targetIp == '::1') && navigator.userAgent.indexOf('Firefox') !== -1) {
244
+ killAllJs();
245
+ config.onConnectionReset("Firefox can't connect to a localhost WebRTC server");
232
246
  return;
247
+ }
233
248
  // Create a new WebRTC connection.
234
- pc = new RTCPeerConnection({ certificates: [localCertificate] });
249
+ state.pc = new RTCPeerConnection({ certificates: [localCertificate] });
235
250
  // We need to build the multihash corresponding to the local certificate.
236
251
  // While there exists a `RTCPeerConnection.getFingerprints` function, Firefox notably
237
252
  // doesn't support it.
@@ -254,7 +269,7 @@ function connect(config) {
254
269
  }
255
270
  }
256
271
  else {
257
- const localSdpOffer = yield pc.createOffer();
272
+ const localSdpOffer = yield state.pc.createOffer();
258
273
  // Note that this regex is not strict. The browser isn't a malicious actor, and the
259
274
  // objective of this regex is not to detect invalid input.
260
275
  const localSdpOfferFingerprintMatch = localSdpOffer.sdp.match(/a(\s*)=(\s*)fingerprint:(\s*)(sha|SHA)-256(\s*)(([a-fA-F0-9]{2}(:)*){32})/);
@@ -268,23 +283,23 @@ function connect(config) {
268
283
  config.onConnectionReset('Failed to obtain the browser certificate fingerprint');
269
284
  return;
270
285
  }
271
- localTlsCertificateSha256 = new Uint8Array(32);
286
+ let localTlsCertificateSha256 = new Uint8Array(32);
272
287
  localTlsCertificateSha256.set(localTlsCertificateHex.split(':').map((s) => parseInt(s, 16)), 0);
273
288
  // `onconnectionstatechange` is used to detect when the connection has closed or has failed
274
289
  // to open.
275
290
  // Note that smoldot will think that the connection is open even when it is still opening.
276
291
  // Therefore we don't care about events concerning the fact that the connection is now fully
277
292
  // open.
278
- pc.onconnectionstatechange = (_event) => {
279
- if (pc.connectionState == "closed" || pc.connectionState == "disconnected" || pc.connectionState == "failed") {
293
+ state.pc.onconnectionstatechange = (_event) => {
294
+ if (state.pc.connectionState == "closed" || state.pc.connectionState == "disconnected" || state.pc.connectionState == "failed") {
280
295
  killAllJs();
281
- config.onConnectionReset("WebRTC state transitioned to " + pc.connectionState);
296
+ config.onConnectionReset("WebRTC state transitioned to " + state.pc.connectionState);
282
297
  }
283
298
  };
284
- pc.onnegotiationneeded = (_event) => __awaiter(this, void 0, void 0, function* () {
299
+ state.pc.onnegotiationneeded = (_event) => __awaiter(this, void 0, void 0, function* () {
285
300
  var _a;
286
301
  // Create a new offer and set it as local description.
287
- let sdpOffer = (yield pc.createOffer()).sdp;
302
+ let sdpOffer = (yield state.pc.createOffer()).sdp;
288
303
  // We check that the locally-generated SDP offer has a data channel with the UDP
289
304
  // protocol. If that isn't the case, the connection will likely fail.
290
305
  if (sdpOffer.match(/^m=application(\s+)(\d+)(\s+)UDP\/DTLS\/SCTP(\s+)webrtc-datachannel$/m) === null) {
@@ -303,7 +318,7 @@ function connect(config) {
303
318
  const ufragPwd = "libp2p+webrtc+v1/" + browserGeneratedPwd;
304
319
  sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:' + ufragPwd);
305
320
  sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:' + ufragPwd);
306
- yield pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });
321
+ yield state.pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });
307
322
  // Transform certificate hash into fingerprint (upper-hex; each byte separated by ":").
308
323
  const fingerprint = Array.from(remoteTlsCertificateSha256).map((n) => ("0" + n.toString(16)).slice(-2).toUpperCase()).join(':');
309
324
  // Note that the trailing line feed is important, as otherwise Chrome
@@ -364,9 +379,9 @@ function connect(config) {
364
379
  // A transport address for a candidate that can be used for connectivity
365
380
  // checks (RFC8839).
366
381
  "a=candidate:1 1 UDP 1 " + targetIp + " " + String(targetPort) + " typ host" + "\n";
367
- yield pc.setRemoteDescription({ type: "answer", sdp: remoteSdp });
382
+ yield state.pc.setRemoteDescription({ type: "answer", sdp: remoteSdp });
368
383
  });
369
- pc.ondatachannel = ({ channel }) => {
384
+ state.pc.ondatachannel = ({ channel }) => {
370
385
  // TODO: is the substream maybe already open? according to the Internet it seems that no but it's unclear
371
386
  addChannel(channel, 'inbound');
372
387
  };
@@ -382,18 +397,18 @@ function connect(config) {
382
397
  killAllJs();
383
398
  }
384
399
  else {
385
- const channel = dataChannels.get(streamId);
400
+ const channel = state.dataChannels.get(streamId);
386
401
  channel.channel.onopen = null;
387
402
  channel.channel.onerror = null;
388
403
  channel.channel.onclose = null;
389
404
  channel.channel.onbufferedamountlow = null;
390
405
  channel.channel.onmessage = null;
391
406
  channel.channel.close();
392
- dataChannels.delete(streamId);
407
+ state.dataChannels.delete(streamId);
393
408
  }
394
409
  },
395
410
  send: (data, streamId) => {
396
- const channel = dataChannels.get(streamId);
411
+ const channel = state.dataChannels.get(streamId);
397
412
  channel.channel.send(data);
398
413
  channel.bufferedBytes += data.length;
399
414
  },
@@ -403,7 +418,10 @@ function connect(config) {
403
418
  // therefore `pc` is guaranteed to be non-null.
404
419
  // Note that the label passed to `createDataChannel` is required to be empty as
405
420
  // per the libp2p WebRTC specification.
406
- addChannel(pc.createDataChannel(""), 'outbound');
421
+ // TODO: adjusting the options based on the first substream is a bit hacky
422
+ const opts = state.isFirstOutSubstream ? { negotiated: true, id: 0 } : {};
423
+ state.isFirstOutSubstream = false;
424
+ addChannel(state.pc.createDataChannel("", opts), 'outbound');
407
425
  }
408
426
  };
409
427
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoldot",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "description": "Light client that connects to Polkadot and Substrate-based blockchains",
5
5
  "contributors": [
6
6
  "Parity Technologies <admin@parity.io>",