Comparing version 1.0.4 to 1.0.5
/// <reference lib="dom" /> | ||
import { Client, ClientOptions } from './client.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError, LogCallback } from './client.js'; | ||
import { Client, ClientOptions } from './public-types.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError, LogCallback } from './public-types.js'; | ||
/** | ||
@@ -5,0 +5,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. |
@@ -5,36 +5,13 @@ "use strict"; | ||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.start = exports.QueueFullError = exports.MalformedJsonRpcError = exports.JsonRpcDisabledError = exports.CrashError = exports.AlreadyDestroyedError = exports.AddChainError = void 0; | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
/// <reference lib="dom" /> | ||
const client_js_1 = require("./client.js"); | ||
const instance_js_1 = require("./instance/instance.js"); | ||
const base64_js_1 = require("./base64.js"); | ||
const pako_1 = require("pako"); | ||
const wasm_js_1 = require("./instance/autogen/wasm.js"); | ||
var client_js_2 = require("./client.js"); | ||
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return client_js_2.AddChainError; } }); | ||
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return client_js_2.AlreadyDestroyedError; } }); | ||
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return client_js_2.CrashError; } }); | ||
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return client_js_2.JsonRpcDisabledError; } }); | ||
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return client_js_2.MalformedJsonRpcError; } }); | ||
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return client_js_2.QueueFullError; } }); | ||
const no_auto_bytecode_browser_js_1 = require("./no-auto-bytecode-browser.js"); | ||
const bytecode_browser_js_1 = require("./bytecode-browser.js"); | ||
var public_types_js_1 = require("./public-types.js"); | ||
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return public_types_js_1.AddChainError; } }); | ||
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return public_types_js_1.AlreadyDestroyedError; } }); | ||
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return public_types_js_1.CrashError; } }); | ||
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return public_types_js_1.JsonRpcDisabledError; } }); | ||
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return public_types_js_1.MalformedJsonRpcError; } }); | ||
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return public_types_js_1.QueueFullError; } }); | ||
/** | ||
@@ -49,478 +26,4 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. | ||
options = options || {}; | ||
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a | ||
// different file. | ||
// This is suboptimal compared to using `instantiateStreaming`, but it is the most | ||
// cross-platform cross-bundler approach. | ||
const wasmModule = WebAssembly.compile((0, pako_1.inflate)((0, base64_js_1.classicDecode)(wasm_js_1.default))); | ||
return (0, client_js_1.start)(options, wasmModule, { | ||
registerShouldPeriodicallyYield: (callback) => { | ||
if (typeof document === 'undefined') // We might be in a web worker. | ||
return [false, () => { }]; | ||
const wrappedCallback = () => callback(document.visibilityState === 'visible'); | ||
document.addEventListener('visibilitychange', wrappedCallback); | ||
return [document.visibilityState === 'visible', () => { document.removeEventListener('visibilitychange', wrappedCallback); }]; | ||
}, | ||
performanceNow: () => { | ||
return performance.now(); | ||
}, | ||
getRandomValues: (buffer) => { | ||
const crypto = globalThis.crypto; | ||
if (!crypto) | ||
throw new Error('randomness not available'); | ||
// Browsers have this completely undocumented behavior (it's not even part of a spec) | ||
// that for some reason `getRandomValues` can't be called on arrayviews back by | ||
// `SharedArrayBuffer`s and they throw an exception if you try. | ||
if (buffer.buffer instanceof ArrayBuffer) | ||
crypto.getRandomValues(buffer); | ||
else { | ||
const tmpArray = new Uint8Array(buffer.length); | ||
crypto.getRandomValues(tmpArray); | ||
buffer.set(tmpArray); | ||
} | ||
}, | ||
connect: (config) => { | ||
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false, (options === null || options === void 0 ? void 0 : options.forbidWebRtc) || false); | ||
} | ||
}); | ||
return (0, no_auto_bytecode_browser_js_1.startWithBytecode)(Object.assign({ bytecode: (0, bytecode_browser_js_1.compileBytecode)() }, options)); | ||
} | ||
exports.start = start; | ||
/** | ||
* Tries to open a new connection using the given configuration. | ||
* | ||
* @see Connection | ||
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol. | ||
*/ | ||
function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) { | ||
// Attempt to parse the multiaddress. | ||
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940) | ||
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/); | ||
const webRTCParsed = config.address.match(/^\/(ip4|ip6)\/(.*?)\/udp\/(.*?)\/webrtc-direct\/certhash\/(.*?)$/); | ||
if (wsParsed != null) { | ||
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss'; | ||
if ((proto == 'ws' && forbidWs) || | ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) || | ||
(proto == 'wss' && forbidWss)) { | ||
throw new instance_js_1.ConnectionError('Connection type not allowed'); | ||
} | ||
const url = (wsParsed[1] == 'ip6') ? | ||
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) : | ||
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]); | ||
const connection = new WebSocket(url); | ||
connection.binaryType = 'arraybuffer'; | ||
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 }; | ||
const checkBufferedAmount = () => { | ||
if (connection.readyState != 1) | ||
return; | ||
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths | ||
// of all the data that has been sent, but that might not be the case. For this | ||
// reason, we use `bufferedAmount` as a hint rather than a correct value. | ||
const bufferedAmount = connection.bufferedAmount; | ||
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount; | ||
if (wasSent < 0) | ||
wasSent = 0; | ||
bufferedAmountCheck.quenedUnreportedBytes -= wasSent; | ||
if (bufferedAmountCheck.quenedUnreportedBytes != 0) { | ||
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout); | ||
bufferedAmountCheck.nextTimeout *= 2; | ||
if (bufferedAmountCheck.nextTimeout > 500) | ||
bufferedAmountCheck.nextTimeout = 500; | ||
} | ||
// Note: it is important to call `onWritableBytes` at the very end, as it might | ||
// trigger a call to `send`. | ||
if (wasSent != 0) | ||
config.onWritableBytes(wasSent); | ||
}; | ||
connection.onopen = () => { | ||
config.onOpen({ | ||
type: 'single-stream', handshake: 'multistream-select-noise-yamux', | ||
initialWritableBytes: 1024 * 1024, writeClosable: false, | ||
}); | ||
}; | ||
connection.onclose = (event) => { | ||
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : ""); | ||
config.onConnectionReset(message); | ||
}; | ||
connection.onmessage = (msg) => { | ||
config.onMessage(new Uint8Array(msg.data)); | ||
}; | ||
return { | ||
reset: () => { | ||
connection.onopen = null; | ||
connection.onclose = null; | ||
connection.onmessage = null; | ||
connection.onerror = null; | ||
connection.close(); | ||
}, | ||
send: (data) => { | ||
connection.send(data); | ||
if (bufferedAmountCheck.quenedUnreportedBytes == 0) { | ||
bufferedAmountCheck.nextTimeout = 10; | ||
setTimeout(checkBufferedAmount, 10); | ||
} | ||
bufferedAmountCheck.quenedUnreportedBytes += data.length; | ||
}, | ||
closeSend: () => { throw new Error('Wrong connection type'); }, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else if (webRTCParsed != null) { | ||
const targetPort = webRTCParsed[3]; | ||
if (forbidWebRTC || targetPort === '0') { | ||
throw new instance_js_1.ConnectionError('Connection type not allowed'); | ||
} | ||
const ipVersion = webRTCParsed[1] == 'ip4' ? '4' : '6'; | ||
const targetIp = webRTCParsed[2]; | ||
const remoteCertMultibase = webRTCParsed[4]; | ||
// The payload of `/certhash` is the hash of the self-generated certificate that the | ||
// server presents. | ||
// This function throws an exception if the certhash isn't correct. For this reason, this call | ||
// is performed as part of the parsing of the multiaddr. | ||
const remoteCertMultihash = (0, base64_js_1.multibaseBase64Decode)(remoteCertMultibase); | ||
const remoteCertSha256Hash = multihashToSha256(remoteCertMultihash); | ||
// TODO: detect localhost for Firefox? https://bugzilla.mozilla.org/show_bug.cgi?id=1659672 | ||
// Note that `pc` can be the connection, but also null or undefined. | ||
// `undefined` means "certificate generation in progress", while `null` means "opening must | ||
// be cancelled". | ||
// While it would be better to use for example a string instead of `null`, using `null` lets | ||
// us use the `!` operator more easily and leads to more readable code. | ||
let pc = undefined; | ||
// Contains the data channels that are open and have been reported to smoldot. | ||
const dataChannels = new Map(); | ||
// For various reasons explained below, we open a data channel in advance without reporting it | ||
// to smoldot. This data channel is stored in this variable. Once it is reported to smoldot, | ||
// it is inserted in `dataChannels`. | ||
let handshakeDataChannel; | ||
// Multihash-encoded DTLS certificate of the local node. Unknown as long as it hasn't been | ||
// generated. | ||
// TODO: could be merged with `pc` in one variable, and maybe even the other fields as well | ||
let localTlsCertificateMultihash; | ||
// Kills all the JavaScript objects (the connection and all its substreams), ensuring that no | ||
// callback will be called again. Doesn't report anything to smoldot, as this should be done | ||
// by the caller. | ||
const killAllJs = () => { | ||
// The `RTCPeerConnection` is created pretty quickly. It is however still possible for | ||
// smoldot to cancel the opening, in which case `pc` will still be undefined. | ||
if (!pc) { | ||
console.assert(dataChannels.size === 0 && !handshakeDataChannel, "substreams exist while pc is undef"); | ||
pc = null; | ||
return; | ||
} | ||
pc.onconnectionstatechange = null; | ||
pc.onnegotiationneeded = null; | ||
pc.ondatachannel = null; | ||
for (const channel of Array.from(dataChannels.values())) { | ||
channel.channel.onopen = null; | ||
channel.channel.onerror = null; | ||
channel.channel.onclose = null; | ||
channel.channel.onbufferedamountlow = null; | ||
channel.channel.onmessage = null; | ||
} | ||
dataChannels.clear(); | ||
if (handshakeDataChannel) { | ||
handshakeDataChannel.onopen = null; | ||
handshakeDataChannel.onerror = null; | ||
handshakeDataChannel.onclose = null; | ||
handshakeDataChannel.onbufferedamountlow = null; | ||
handshakeDataChannel.onmessage = null; | ||
} | ||
handshakeDataChannel = undefined; | ||
pc.close(); // Not necessarily necessary, but it doesn't hurt to do so. | ||
}; | ||
// Function that configures a newly-opened channel and adds it to the map. Used for both | ||
// inbound and outbound substreams. | ||
const addChannel = (dataChannel, direction) => { | ||
const dataChannelId = dataChannel.id; | ||
dataChannel.binaryType = 'arraybuffer'; | ||
let isOpen = false; | ||
dataChannel.onopen = () => { | ||
console.assert(!isOpen, "substream opened twice"); | ||
isOpen = true; | ||
if (direction === 'first-outbound') { | ||
console.assert(dataChannels.size === 0, "dataChannels not empty when opening"); | ||
console.assert(handshakeDataChannel === dataChannel, "handshake substream mismatch"); | ||
config.onOpen({ | ||
type: 'multi-stream', | ||
handshake: 'webrtc', | ||
// `addChannel` can never be called before the local certificate is generated, so this | ||
// value is always defined. | ||
localTlsCertificateMultihash: localTlsCertificateMultihash, | ||
remoteTlsCertificateMultihash: remoteCertMultihash | ||
}); | ||
} | ||
else { | ||
console.assert(direction !== 'outbound' || !handshakeDataChannel, "handshakeDataChannel still defined"); | ||
config.onStreamOpened(dataChannelId, direction, 65536); | ||
} | ||
}; | ||
dataChannel.onerror = dataChannel.onclose = (_error) => { | ||
// A couple of different things could be happening here. | ||
if (handshakeDataChannel === dataChannel && !isOpen) { | ||
// The handshake data channel that we have opened ahead of time failed to open. As this | ||
// happens before we have reported the WebRTC connection as a whole as being open, we | ||
// need to report that the connection has failed to open. | ||
killAllJs(); | ||
// Note that the event doesn't give any additional reason for the failure. | ||
config.onConnectionReset("handshake data channel failed to open"); | ||
} | ||
else if (handshakeDataChannel === dataChannel) { | ||
// The handshake data channel has been closed before we reported it to smoldot. This | ||
// isn't really a problem. We just update the state and continue running. If smoldot | ||
// requests a substream, another one will be opened. It could be a valid implementation | ||
// to also just kill the entire connection, however doing so is a bit too intrusive and | ||
// punches through abstraction layers. | ||
handshakeDataChannel.onopen = null; | ||
handshakeDataChannel.onerror = null; | ||
handshakeDataChannel.onclose = null; | ||
handshakeDataChannel.onbufferedamountlow = null; | ||
handshakeDataChannel.onmessage = null; | ||
handshakeDataChannel = undefined; | ||
} | ||
else if (!isOpen) { | ||
// Substream wasn't opened yet and thus has failed to open. The API has no mechanism to | ||
// report substream openings failures. We could try opening it again, but given that | ||
// it's unlikely to succeed, we simply opt to kill the entire connection. | ||
killAllJs(); | ||
// Note that the event doesn't give any additional reason for the failure. | ||
config.onConnectionReset("data channel failed to open"); | ||
} | ||
else { | ||
// Substream was open and is now closed. Normal situation. | ||
config.onStreamReset(dataChannelId); | ||
} | ||
}; | ||
dataChannel.onbufferedamountlow = () => { | ||
const channel = dataChannels.get(dataChannelId); | ||
const val = channel.bufferedBytes; | ||
channel.bufferedBytes = 0; | ||
config.onWritableBytes(val, dataChannelId); | ||
}; | ||
dataChannel.onmessage = (m) => { | ||
// The `data` field is an `ArrayBuffer`. | ||
config.onMessage(new Uint8Array(m.data), dataChannelId); | ||
}; | ||
if (direction !== 'first-outbound') | ||
dataChannels.set(dataChannelId, { channel: dataChannel, bufferedBytes: 0 }); | ||
else | ||
handshakeDataChannel = dataChannel; | ||
}; | ||
// It is possible for the browser to use multiple different certificates. | ||
// In order for our local certificate to be deterministic, we need to generate it manually and | ||
// set it explicitly as part of the configuration. | ||
// According to <https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-generatecertificate>, | ||
// browsers are guaranteed to support `{ name: "ECDSA", namedCurve: "P-256" }`. | ||
RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }).then((localCertificate) => __awaiter(this, void 0, void 0, function* () { | ||
if (pc === null) | ||
return; | ||
// Create a new WebRTC connection. | ||
pc = new RTCPeerConnection({ certificates: [localCertificate] }); | ||
// We need to build the multihash corresponding to the local certificate. | ||
// While there exists a `RTCPeerConnection.getFingerprints` function, Firefox notably | ||
// doesn't support it. | ||
// See <https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate#browser_compatibility> | ||
// An alternative to `getFingerprints` is to ask the browser to generate an SDP offer and | ||
// extract from fingerprint from it. Because we explicitly provide a certificate, we have | ||
// the guarantee that the list of certificates will always be the same whenever an SDP offer | ||
// is generated by the browser. However, while this alternative does work on Firefox, it | ||
// doesn't on Chrome, as the SDP offer is for some reason missing the fingerprints. | ||
// Therefore, our strategy is to use `getFingerprints` when it is available (i.e. every | ||
// browser except Firefox), and parse the SDP offer when it is not (i.e. Firefox). In the | ||
// future, only `getFingerprints` would be used. | ||
let localTlsCertificateHex; | ||
if (localCertificate.getFingerprints) { | ||
for (const { algorithm, value } of localCertificate.getFingerprints()) { | ||
if (algorithm === 'sha-256') { | ||
localTlsCertificateHex = value; | ||
break; | ||
} | ||
} | ||
} | ||
else { | ||
const localSdpOffer = yield pc.createOffer(); | ||
// Note that this regex is not strict. The browser isn't a malicious actor, and the | ||
// objective of this regex is not to detect invalid input. | ||
const localSdpOfferFingerprintMatch = localSdpOffer.sdp.match(/a(\s*)=(\s*)fingerprint:(\s*)(sha|SHA)-256(\s*)(([a-fA-F0-9]{2}(:)*){32})/); | ||
if (localSdpOfferFingerprintMatch) { | ||
localTlsCertificateHex = localSdpOfferFingerprintMatch[6]; | ||
} | ||
} | ||
if (localTlsCertificateHex === undefined) { | ||
// Because we've already returned from the `connect` function at this point, we pretend | ||
// that the connection has failed to open. | ||
config.onConnectionReset('Failed to obtain the browser certificate fingerprint'); | ||
return; | ||
} | ||
localTlsCertificateMultihash = new Uint8Array(34); | ||
localTlsCertificateMultihash.set([0x12, 32], 0); | ||
localTlsCertificateMultihash.set(localTlsCertificateHex.split(':').map((s) => parseInt(s, 16)), 2); | ||
// `onconnectionstatechange` is used to detect when the connection has closed or has failed | ||
// to open. | ||
// Note that smoldot will think that the connection is open even when it is still opening. | ||
// Therefore we don't care about events concerning the fact that the connection is now fully | ||
// open. | ||
pc.onconnectionstatechange = (_event) => { | ||
if (pc.connectionState == "closed" || pc.connectionState == "disconnected" || pc.connectionState == "failed") { | ||
killAllJs(); | ||
config.onConnectionReset("WebRTC state transitioned to " + pc.connectionState); | ||
} | ||
}; | ||
pc.onnegotiationneeded = (_event) => __awaiter(this, void 0, void 0, function* () { | ||
var _a; | ||
// Create a new offer and set it as local description. | ||
let sdpOffer = (yield pc.createOffer()).sdp; | ||
// We check that the locally-generated SDP offer has a data channel with the UDP | ||
// protocol. If that isn't the case, the connection will likely fail. | ||
if (sdpOffer.match(/^m=application(\s+)(\d+)(\s+)UDP\/DTLS\/SCTP(\s+)webrtc-datachannel$/m) === null) { | ||
console.error("Local offer doesn't contain UDP data channel. WebRTC connections will likely fail. Please report this issue."); | ||
} | ||
// According to the libp2p WebRTC spec, the ufrag and pwd are the same | ||
// randomly-generated string on both sides, and must be prefixed with | ||
// `libp2p-webrtc-v1:`. We modify the local description to ensure that. | ||
// While we could randomly generate a new string, we just grab the one that the | ||
// browser has generated, in order to make sure that it respects the constraints | ||
// of the ICE protocol. | ||
const browserGeneratedPwd = (_a = sdpOffer.match(/^a=ice-pwd:(.+)$/m)) === null || _a === void 0 ? void 0 : _a.at(1); | ||
if (browserGeneratedPwd === undefined) { | ||
console.error("Failed to set ufrag to pwd. WebRTC connections will likely fail. Please report this issue."); | ||
} | ||
const ufragPwd = "libp2p+webrtc+v1/" + browserGeneratedPwd; | ||
sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:' + ufragPwd); | ||
sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:' + ufragPwd); | ||
yield pc.setLocalDescription({ type: 'offer', sdp: sdpOffer }); | ||
// Transform certificate hash into fingerprint (upper-hex; each byte separated by ":"). | ||
const fingerprint = Array.from(remoteCertSha256Hash).map((n) => ("0" + n.toString(16)).slice(-2).toUpperCase()).join(':'); | ||
// Note that the trailing line feed is important, as otherwise Chrome | ||
// fails to parse the payload. | ||
const remoteSdp = | ||
// Version of the SDP protocol. Always 0. (RFC8866) | ||
"v=0" + "\n" + | ||
// Identifies the creator of the SDP document. We are allowed to use dummy values | ||
// (`-` and `0.0.0.0`) to remain anonymous, which we do. Note that "IN" means | ||
// "Internet" (and not "input"). (RFC8866) | ||
"o=- 0 0 IN IP" + ipVersion + " " + targetIp + "\n" + | ||
// Name for the session. We are allowed to pass a dummy `-`. (RFC8866) | ||
"s=-" + "\n" + | ||
// Start and end of the validity of the session. `0 0` means that the session never | ||
// expires. (RFC8866) | ||
"t=0 0" + "\n" + | ||
// A lite implementation is only appropriate for devices that will | ||
// always be connected to the public Internet and have a public | ||
// IP address at which it can receive packets from any | ||
// correspondent. ICE will not function when a lite implementation | ||
// is placed behind a NAT (RFC8445). | ||
"a=ice-lite" + "\n" + | ||
// A `m=` line describes a request to establish a certain protocol. | ||
// The protocol in this line (i.e. `TCP/DTLS/SCTP` or `UDP/DTLS/SCTP`) must always be | ||
// the same as the one in the offer. We know that this is true because checked above. | ||
// The `<fmt>` component must always be `webrtc-datachannel` for WebRTC. | ||
// The rest of the SDP payload adds attributes to this specific media stream. | ||
// RFCs: 8839, 8866, 8841 | ||
"m=application " + targetPort + " " + "UDP/DTLS/SCTP webrtc-datachannel" + "\n" + | ||
// Indicates the IP address of the remote. | ||
// Note that "IN" means "Internet" (and not "input"). | ||
"c=IN IP" + ipVersion + " " + targetIp + "\n" + | ||
// Media ID - uniquely identifies this media stream (RFC9143). | ||
"a=mid:0" + "\n" + | ||
// Indicates that we are complying with RFC8839 (as oppposed to the legacy RFC5245). | ||
"a=ice-options:ice2" + "\n" + | ||
// ICE username and password, which are used for establishing and | ||
// maintaining the ICE connection. (RFC8839) | ||
// These values are set according to the libp2p WebRTC specification. | ||
"a=ice-ufrag:" + ufragPwd + "\n" + | ||
"a=ice-pwd:" + ufragPwd + "\n" + | ||
// Fingerprint of the certificate that the server will use during the TLS | ||
// handshake. (RFC8122) | ||
// MUST be derived from the certificate used by the answerer (server). | ||
"a=fingerprint:sha-256 " + fingerprint + "\n" + | ||
// Indicates that the remote DTLS server will only listen for incoming | ||
// connections. (RFC5763) | ||
// The answerer (server) MUST not be located behind a NAT (RFC6135). | ||
"a=setup:passive" + "\n" + | ||
// The SCTP port (RFC8841) | ||
// Note it's different from the "m=" line port value, which | ||
// indicates the port of the underlying transport-layer protocol | ||
// (UDP or TCP) | ||
"a=sctp-port:5000" + "\n" + | ||
// The maximum SCTP user message size (in bytes) (RFC8841) | ||
// Setting this field is part of the libp2p spec. | ||
"a=max-message-size:16384" + "\n" + | ||
// A transport address for a candidate that can be used for connectivity | ||
// checks (RFC8839). | ||
"a=candidate:1 1 UDP 1 " + targetIp + " " + targetPort + " typ host" + "\n"; | ||
yield pc.setRemoteDescription({ type: "answer", sdp: remoteSdp }); | ||
}); | ||
pc.ondatachannel = ({ channel }) => { | ||
// TODO: is the substream maybe already open? according to the Internet it seems that no but it's unclear | ||
addChannel(channel, 'inbound'); | ||
}; | ||
// Creating a `RTCPeerConnection` doesn't actually do anything before `createDataChannel` is | ||
// called. Smoldot's API, however, requires you to treat entire connections as open or | ||
// closed. We know, according to the libp2p WebRTC specification, that every connection | ||
// always starts with a substream where a handshake is performed. After we've reported that | ||
// the connection is open, smoldot will open a substream in order to perform the handshake. | ||
// Instead of following this API, we open this substream in advance, and will notify smoldot | ||
// that the connection is open when the substream is open. | ||
// Note that the label passed to `createDataChannel` is required to be empty as per the | ||
// libp2p WebRTC specification. | ||
addChannel(pc.createDataChannel("", { id: 0, negotiated: true }), 'first-outbound'); | ||
})); | ||
return { | ||
reset: (streamId) => { | ||
// If `streamId` is undefined, then the whole connection must be destroyed. | ||
if (streamId === undefined) { | ||
killAllJs(); | ||
} | ||
else { | ||
const channel = dataChannels.get(streamId); | ||
channel.channel.onopen = null; | ||
channel.channel.onerror = null; | ||
channel.channel.onclose = null; | ||
channel.channel.onbufferedamountlow = null; | ||
channel.channel.onmessage = null; | ||
channel.channel.close(); | ||
dataChannels.delete(streamId); | ||
} | ||
}, | ||
send: (data, streamId) => { | ||
const channel = dataChannels.get(streamId); | ||
channel.channel.send(data); | ||
channel.bufferedBytes += data.length; | ||
}, | ||
closeSend: () => { throw new Error('Wrong connection type'); }, | ||
openOutSubstream: () => { | ||
// `openOutSubstream` can only be called after we have called `config.onOpen`, therefore | ||
// `pc` is guaranteed to be non-null. | ||
// As explained above, we open a data channel ahead of time. If this data channel is still | ||
// there, we report it. | ||
if (handshakeDataChannel) { | ||
// Do this asynchronously because calling callbacks within callbacks is error-prone. | ||
(() => __awaiter(this, void 0, void 0, function* () { | ||
// We need to check again if `handshakeDataChannel` is still defined, as the | ||
// connection might have been closed. | ||
if (handshakeDataChannel) { | ||
config.onStreamOpened(handshakeDataChannel.id, 'outbound', 1024 * 1024); | ||
dataChannels.set(handshakeDataChannel.id, { channel: handshakeDataChannel, bufferedBytes: 0 }); | ||
handshakeDataChannel = undefined; | ||
} | ||
}))(); | ||
} | ||
else { | ||
// Note that the label passed to `createDataChannel` is required to be empty as per the | ||
// libp2p WebRTC specification. | ||
addChannel(pc.createDataChannel(""), 'outbound'); | ||
} | ||
} | ||
}; | ||
} | ||
else { | ||
throw new instance_js_1.ConnectionError('Unrecognized multiaddr format'); | ||
} | ||
} | ||
/// Parses a multihash-multibase-encoded string into a SHA256 hash. | ||
/// | ||
/// Throws an exception if the multihash algorithm isn't SHA256. | ||
const multihashToSha256 = (certMultihash) => { | ||
if (certMultihash.length != 34 || certMultihash[0] != 0x12 || certMultihash[1] != 32) { | ||
throw new Error('Certificate multihash is not SHA-256'); | ||
} | ||
return new Uint8Array(certMultihash.slice(2)); | ||
}; |
@@ -1,3 +0,3 @@ | ||
import { Client, ClientOptions } from './client.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './client.js'; | ||
import { Client, ClientOptions } from './public-types.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './public-types.js'; | ||
/** | ||
@@ -4,0 +4,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. |
@@ -5,33 +5,13 @@ "use strict"; | ||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.start = exports.JsonRpcDisabledError = exports.QueueFullError = exports.MalformedJsonRpcError = exports.CrashError = exports.AlreadyDestroyedError = exports.AddChainError = void 0; | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
const client_js_1 = require("./client.js"); | ||
const instance_js_1 = require("./instance/instance.js"); | ||
const wasm_js_1 = require("./instance/autogen/wasm.js"); | ||
var client_js_2 = require("./client.js"); | ||
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return client_js_2.AddChainError; } }); | ||
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return client_js_2.AlreadyDestroyedError; } }); | ||
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return client_js_2.CrashError; } }); | ||
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return client_js_2.MalformedJsonRpcError; } }); | ||
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return client_js_2.QueueFullError; } }); | ||
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return client_js_2.JsonRpcDisabledError; } }); | ||
const no_auto_bytecode_deno_js_1 = require("./no-auto-bytecode-deno.js"); | ||
const bytecode_deno_js_1 = require("./bytecode-deno.js"); | ||
var public_types_js_1 = require("./public-types.js"); | ||
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return public_types_js_1.AddChainError; } }); | ||
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return public_types_js_1.AlreadyDestroyedError; } }); | ||
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return public_types_js_1.CrashError; } }); | ||
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return public_types_js_1.MalformedJsonRpcError; } }); | ||
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return public_types_js_1.QueueFullError; } }); | ||
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return public_types_js_1.JsonRpcDisabledError; } }); | ||
/** | ||
@@ -46,257 +26,4 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. | ||
options = options || {}; | ||
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a | ||
// different file. | ||
// This is suboptimal compared to using `instantiateStreaming`, but it is the most | ||
// cross-platform cross-bundler approach. | ||
const wasmModule = zlibInflate(trustedBase64Decode(wasm_js_1.default)).then(((bytecode) => WebAssembly.compile(bytecode))); | ||
return (0, client_js_1.start)(options || {}, wasmModule, { | ||
registerShouldPeriodicallyYield: (_callback) => { | ||
return [true, () => { }]; | ||
}, | ||
performanceNow: () => { | ||
return performance.now(); | ||
}, | ||
getRandomValues: (buffer) => { | ||
const crypto = globalThis.crypto; | ||
if (!crypto) | ||
throw new Error('randomness not available'); | ||
crypto.getRandomValues(buffer); | ||
}, | ||
connect: (config) => { | ||
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidTcp) || false, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false); | ||
} | ||
}); | ||
return (0, no_auto_bytecode_deno_js_1.startWithBytecode)(Object.assign({ bytecode: (0, bytecode_deno_js_1.compileBytecode)() }, options)); | ||
} | ||
exports.start = start; | ||
/** | ||
* Applies the zlib inflate algorithm on the buffer. | ||
*/ | ||
function zlibInflate(buffer) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// This code has been copy-pasted from the official streams draft specification. | ||
// At the moment, it is found here: https://wicg.github.io/compression/#example-deflate-compress | ||
const ds = new DecompressionStream('deflate'); | ||
const writer = ds.writable.getWriter(); | ||
writer.write(buffer); | ||
writer.close(); | ||
const output = []; | ||
const reader = ds.readable.getReader(); | ||
let totalSize = 0; | ||
while (true) { | ||
const { value, done } = yield reader.read(); | ||
if (done) | ||
break; | ||
output.push(value); | ||
totalSize += value.byteLength; | ||
} | ||
const concatenated = new Uint8Array(totalSize); | ||
let offset = 0; | ||
for (const array of output) { | ||
concatenated.set(array, offset); | ||
offset += array.byteLength; | ||
} | ||
return concatenated; | ||
}); | ||
} | ||
/** | ||
* Decodes a base64 string. | ||
* | ||
* The input is assumed to be correct. | ||
*/ | ||
function trustedBase64Decode(base64) { | ||
// This code is a bit sketchy due to the fact that we decode into a string, but it seems to | ||
// work. | ||
const binaryString = atob(base64); | ||
const size = binaryString.length; | ||
const bytes = new Uint8Array(size); | ||
for (let i = 0; i < size; i++) { | ||
bytes[i] = binaryString.charCodeAt(i); | ||
} | ||
return bytes; | ||
} | ||
/** | ||
* Tries to open a new connection using the given configuration. | ||
* | ||
* @see Connection | ||
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol. | ||
*/ | ||
function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) { | ||
// Attempt to parse the multiaddress. | ||
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940) | ||
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/); | ||
const tcpParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)$/); | ||
if (wsParsed != null) { | ||
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss'; | ||
if ((proto == 'ws' && forbidWs) || | ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) || | ||
(proto == 'wss' && forbidWss)) { | ||
throw new instance_js_1.ConnectionError('Connection type not allowed'); | ||
} | ||
const url = (wsParsed[1] == 'ip6') ? | ||
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) : | ||
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]); | ||
const socket = new WebSocket(url); | ||
socket.binaryType = 'arraybuffer'; | ||
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 }; | ||
const checkBufferedAmount = () => { | ||
if (socket.readyState != 1) | ||
return; | ||
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths | ||
// of all the data that has been sent, but that might not be the case. For this | ||
// reason, we use `bufferedAmount` as a hint rather than a correct value. | ||
const bufferedAmount = socket.bufferedAmount; | ||
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount; | ||
if (wasSent < 0) | ||
wasSent = 0; | ||
bufferedAmountCheck.quenedUnreportedBytes -= wasSent; | ||
if (bufferedAmountCheck.quenedUnreportedBytes != 0) { | ||
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout); | ||
bufferedAmountCheck.nextTimeout *= 2; | ||
if (bufferedAmountCheck.nextTimeout > 500) | ||
bufferedAmountCheck.nextTimeout = 500; | ||
} | ||
// Note: it is important to call `onWritableBytes` at the very end, as it might | ||
// trigger a call to `send`. | ||
if (wasSent != 0) | ||
config.onWritableBytes(wasSent); | ||
}; | ||
socket.onopen = () => { | ||
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false }); | ||
}; | ||
socket.onclose = (event) => { | ||
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : ""); | ||
config.onConnectionReset(message); | ||
}; | ||
socket.onmessage = (msg) => { | ||
config.onMessage(new Uint8Array(msg.data)); | ||
}; | ||
return { | ||
reset: () => { | ||
// We can't set these fields to null because the TypeScript definitions don't | ||
// allow it, but we can set them to dummy values. | ||
socket.onopen = () => { }; | ||
socket.onclose = () => { }; | ||
socket.onmessage = () => { }; | ||
socket.onerror = () => { }; | ||
socket.close(); | ||
}, | ||
send: (data) => { | ||
// The WebSocket library that we use seems to spontaneously transition connections | ||
// to the "closed" state but not call the `onclosed` callback immediately. Calling | ||
// `send` on that object throws an exception. In order to avoid panicking smoldot, | ||
// we thus absorb any exception thrown here. | ||
// See also <https://github.com/paritytech/smoldot/issues/2937>. | ||
try { | ||
socket.send(data); | ||
if (bufferedAmountCheck.quenedUnreportedBytes == 0) { | ||
bufferedAmountCheck.nextTimeout = 10; | ||
setTimeout(checkBufferedAmount, 10); | ||
} | ||
bufferedAmountCheck.quenedUnreportedBytes += data.length; | ||
} | ||
catch (_error) { } | ||
}, | ||
closeSend: () => { throw new Error('Wrong connection type'); }, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else if (tcpParsed != null) { | ||
if (forbidTcp) { | ||
throw new instance_js_1.ConnectionError('TCP connections not available'); | ||
} | ||
const socket = { | ||
destroyed: false, | ||
inner: Deno.connect({ | ||
hostname: tcpParsed[2], | ||
port: parseInt(tcpParsed[3], 10), | ||
}).catch((error) => { | ||
socket.destroyed = true; | ||
config.onConnectionReset(error.toString()); | ||
return null; | ||
}) | ||
}; | ||
socket.inner = socket.inner.then((established) => { | ||
// TODO: at the time of writing of this comment, `setNoDelay` is still unstable | ||
//established.setNoDelay(); | ||
if (socket.destroyed) | ||
return established; | ||
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: true }); | ||
// Spawns an asynchronous task that continuously reads from the socket. | ||
// Every time data is read, the task re-executes itself in order to continue reading. | ||
// The task ends automatically if an EOF or error is detected, which should also happen | ||
// if the user calls `close()`. | ||
const read = (readBuffer) => __awaiter(this, void 0, void 0, function* () { | ||
if (socket.destroyed || established === null) | ||
return; | ||
let outcome = null; | ||
try { | ||
outcome = yield established.read(readBuffer); | ||
} | ||
catch (error) { | ||
// The type of `error` is unclear, but we assume that it implements `Error` | ||
outcome = error.toString(); | ||
} | ||
if (socket.destroyed) | ||
return; | ||
if (typeof outcome !== 'number' || outcome === null) { | ||
// The socket is reported closed, but `socket.destroyed` is still `false` (see | ||
// check above). As such, we must inform the inner layers. | ||
socket.destroyed = true; | ||
config.onConnectionReset(outcome === null ? "EOF when reading socket" : outcome); | ||
return; | ||
} | ||
console.assert(outcome !== 0); // `read` guarantees to return a non-zero value. | ||
config.onMessage(readBuffer.slice(0, outcome)); | ||
return read(readBuffer); | ||
}); | ||
read(new Uint8Array(32768)); | ||
return established; | ||
}); | ||
return { | ||
reset: () => { | ||
socket.destroyed = true; | ||
socket.inner.then((connec) => connec.close()); | ||
}, | ||
send: (data) => { | ||
let dataCopy = Uint8Array.from(data); // Deep copy of the data | ||
socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () { | ||
while (dataCopy.length > 0) { | ||
if (socket.destroyed || c === null) | ||
return c; | ||
let outcome; | ||
try { | ||
outcome = yield c.write(dataCopy); | ||
config.onWritableBytes(dataCopy.length); | ||
} | ||
catch (error) { | ||
// The type of `error` is unclear, but we assume that it implements `Error` | ||
outcome = error.toString(); | ||
} | ||
if (typeof outcome !== 'number') { | ||
// The socket is reported closed, but `socket.destroyed` is still | ||
// `false` (see check above). As such, we must inform the inner layers. | ||
socket.destroyed = true; | ||
config.onConnectionReset(outcome); | ||
return c; | ||
} | ||
// Note that, contrary to `read`, it is possible for `outcome` to be 0. | ||
// This happen if the write had to be interrupted, and the only thing | ||
// we have to do is try writing again. | ||
dataCopy = dataCopy.slice(outcome); | ||
} | ||
return c; | ||
})); | ||
}, | ||
closeSend: () => { | ||
socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () { | ||
yield (c === null || c === void 0 ? void 0 : c.closeWrite()); | ||
return c; | ||
})); | ||
}, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else { | ||
throw new instance_js_1.ConnectionError('Unrecognized multiaddr format'); | ||
} | ||
} |
@@ -1,3 +0,3 @@ | ||
import { Client, ClientOptions } from './client.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './client.js'; | ||
import { Client, ClientOptions } from './public-types.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './public-types.js'; | ||
/** | ||
@@ -4,0 +4,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. |
@@ -7,30 +7,11 @@ "use strict"; | ||
exports.start = exports.JsonRpcDisabledError = exports.QueueFullError = exports.MalformedJsonRpcError = exports.CrashError = exports.AlreadyDestroyedError = exports.AddChainError = void 0; | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
// Note: if you modify these imports, please test both the ModuleJS and CommonJS generated | ||
// bindings. JavaScript being JavaScript, some libraries (such as `websocket`) have issues working | ||
// with both at the same time. | ||
const client_js_1 = require("./client.js"); | ||
const instance_js_1 = require("./instance/instance.js"); | ||
const wasm_js_1 = require("./instance/autogen/wasm.js"); | ||
const ws_1 = require("ws"); | ||
const pako_1 = require("pako"); | ||
const node_perf_hooks_1 = require("node:perf_hooks"); | ||
const node_net_1 = require("node:net"); | ||
const node_crypto_1 = require("node:crypto"); | ||
var client_js_2 = require("./client.js"); | ||
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return client_js_2.AddChainError; } }); | ||
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return client_js_2.AlreadyDestroyedError; } }); | ||
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return client_js_2.CrashError; } }); | ||
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return client_js_2.MalformedJsonRpcError; } }); | ||
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return client_js_2.QueueFullError; } }); | ||
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return client_js_2.JsonRpcDisabledError; } }); | ||
const no_auto_bytecode_nodejs_js_1 = require("./no-auto-bytecode-nodejs.js"); | ||
const bytecode_nodejs_js_1 = require("./bytecode-nodejs.js"); | ||
var public_types_js_1 = require("./public-types.js"); | ||
Object.defineProperty(exports, "AddChainError", { enumerable: true, get: function () { return public_types_js_1.AddChainError; } }); | ||
Object.defineProperty(exports, "AlreadyDestroyedError", { enumerable: true, get: function () { return public_types_js_1.AlreadyDestroyedError; } }); | ||
Object.defineProperty(exports, "CrashError", { enumerable: true, get: function () { return public_types_js_1.CrashError; } }); | ||
Object.defineProperty(exports, "MalformedJsonRpcError", { enumerable: true, get: function () { return public_types_js_1.MalformedJsonRpcError; } }); | ||
Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: function () { return public_types_js_1.QueueFullError; } }); | ||
Object.defineProperty(exports, "JsonRpcDisabledError", { enumerable: true, get: function () { return public_types_js_1.JsonRpcDisabledError; } }); | ||
/** | ||
@@ -45,186 +26,4 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. | ||
options = options || {}; | ||
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a | ||
// different file. | ||
// This is suboptimal compared to using `instantiateStreaming`, but it is the most | ||
// cross-platform cross-bundler approach. | ||
const wasmModule = WebAssembly.compile((0, pako_1.inflate)(Buffer.from(wasm_js_1.default, 'base64'))); | ||
return (0, client_js_1.start)(options || {}, wasmModule, { | ||
registerShouldPeriodicallyYield: (_callback) => { | ||
return [true, () => { }]; | ||
}, | ||
performanceNow: () => { | ||
return node_perf_hooks_1.performance.now(); | ||
}, | ||
getRandomValues: (buffer) => { | ||
if (buffer.length >= 1024 * 1024) | ||
throw new Error('getRandomValues buffer too large'); | ||
(0, node_crypto_1.randomFillSync)(buffer); | ||
}, | ||
connect: (config) => { | ||
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidTcp) || false, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false); | ||
} | ||
}); | ||
return (0, no_auto_bytecode_nodejs_js_1.startWithBytecode)(Object.assign({ bytecode: (0, bytecode_nodejs_js_1.compileBytecode)() }, options)); | ||
} | ||
exports.start = start; | ||
/** | ||
* Tries to open a new connection using the given configuration. | ||
* | ||
* @see Connection | ||
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol. | ||
*/ | ||
function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) { | ||
// Attempt to parse the multiaddress. | ||
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940) | ||
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/); | ||
const tcpParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)$/); | ||
if (wsParsed != null) { | ||
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss'; | ||
if ((proto == 'ws' && forbidWs) || | ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) || | ||
(proto == 'wss' && forbidWss)) { | ||
throw new instance_js_1.ConnectionError('Connection type not allowed'); | ||
} | ||
const url = (wsParsed[1] == 'ip6') ? | ||
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) : | ||
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]); | ||
const socket = new ws_1.WebSocket(url); | ||
socket.binaryType = 'arraybuffer'; | ||
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 }; | ||
const checkBufferedAmount = () => { | ||
if (socket.readyState != 1) | ||
return; | ||
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths | ||
// of all the data that has been sent, but that seems to not be the case. It is | ||
// unclear whether this is intended or a bug, but is is likely that `bufferedAmount` | ||
// also includes WebSocket headers. For this reason, we use `bufferedAmount` as a hint | ||
// rather than a correct value. | ||
const bufferedAmount = socket.bufferedAmount; | ||
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount; | ||
if (wasSent < 0) | ||
wasSent = 0; | ||
bufferedAmountCheck.quenedUnreportedBytes -= wasSent; | ||
if (bufferedAmountCheck.quenedUnreportedBytes != 0) { | ||
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout); | ||
bufferedAmountCheck.nextTimeout *= 2; | ||
if (bufferedAmountCheck.nextTimeout > 500) | ||
bufferedAmountCheck.nextTimeout = 500; | ||
} | ||
// Note: it is important to call `onWritableBytes` at the very end, as it might | ||
// trigger a call to `send`. | ||
if (wasSent != 0) | ||
config.onWritableBytes(wasSent); | ||
}; | ||
socket.onopen = () => { | ||
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false }); | ||
}; | ||
socket.onclose = (event) => { | ||
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : ""); | ||
config.onConnectionReset(message); | ||
socket.onopen = () => { }; | ||
socket.onclose = () => { }; | ||
socket.onmessage = () => { }; | ||
socket.onerror = () => { }; | ||
}; | ||
socket.onerror = (event) => { | ||
config.onConnectionReset(event.message); | ||
socket.onopen = () => { }; | ||
socket.onclose = () => { }; | ||
socket.onmessage = () => { }; | ||
socket.onerror = () => { }; | ||
}; | ||
socket.onmessage = (msg) => { | ||
config.onMessage(new Uint8Array(msg.data)); | ||
}; | ||
return { | ||
reset: () => { | ||
// We can't set these fields to null because the TypeScript definitions don't | ||
// allow it, but we can set them to dummy values. | ||
socket.onopen = () => { }; | ||
socket.onclose = () => { }; | ||
socket.onmessage = () => { }; | ||
socket.onerror = () => { }; | ||
socket.close(); | ||
}, | ||
send: (data) => { | ||
socket.send(data); | ||
if (bufferedAmountCheck.quenedUnreportedBytes == 0) { | ||
bufferedAmountCheck.nextTimeout = 10; | ||
setTimeout(checkBufferedAmount, 10); | ||
} | ||
bufferedAmountCheck.quenedUnreportedBytes += data.length; | ||
}, | ||
closeSend: () => { throw new Error('Wrong connection type'); }, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else if (tcpParsed != null) { | ||
// `net` module will be missing when we're not in NodeJS. | ||
if (forbidTcp) { | ||
throw new instance_js_1.ConnectionError('TCP connections not available'); | ||
} | ||
const socket = (0, node_net_1.createConnection)({ | ||
host: tcpParsed[2], | ||
port: parseInt(tcpParsed[3], 10), | ||
}); | ||
// Number of bytes queued using `socket.write` and where `write` has returned false. | ||
const drainingBytes = { num: 0 }; | ||
socket.setNoDelay(); | ||
socket.on('connect', () => { | ||
if (socket.destroyed) | ||
return; | ||
config.onOpen({ | ||
type: 'single-stream', handshake: 'multistream-select-noise-yamux', | ||
initialWritableBytes: socket.writableHighWaterMark, writeClosable: true | ||
}); | ||
}); | ||
socket.on('close', (hasError) => { | ||
if (socket.destroyed) | ||
return; | ||
// NodeJS doesn't provide a reason why the closing happened, but only | ||
// whether it was caused by an error. | ||
const message = hasError ? "Error" : "Closed gracefully"; | ||
config.onConnectionReset(message); | ||
}); | ||
socket.on('error', () => { }); | ||
socket.on('data', (message) => { | ||
if (socket.destroyed) | ||
return; | ||
config.onMessage(new Uint8Array(message.buffer)); | ||
}); | ||
socket.on('drain', () => { | ||
// The bytes queued using `socket.write` and where `write` has returned false have now | ||
// been sent. Notify the API that it can write more data. | ||
if (socket.destroyed) | ||
return; | ||
const val = drainingBytes.num; | ||
drainingBytes.num = 0; | ||
config.onWritableBytes(val); | ||
}); | ||
return { | ||
reset: () => { | ||
socket.destroy(); | ||
}, | ||
send: (data) => { | ||
const dataLen = data.length; | ||
const allWritten = socket.write(data); | ||
if (allWritten) { | ||
setImmediate(() => { | ||
if (!socket.writable) | ||
return; | ||
config.onWritableBytes(dataLen); | ||
}); | ||
} | ||
else { | ||
drainingBytes.num += dataLen; | ||
} | ||
}, | ||
closeSend: () => { | ||
socket.end(); | ||
}, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else { | ||
throw new instance_js_1.ConnectionError('Unrecognized multiaddr format'); | ||
} | ||
} |
/// <reference lib="dom" /> | ||
import { Client, ClientOptions } from './client.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError, LogCallback } from './client.js'; | ||
import { Client, ClientOptions } from './public-types.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError, LogCallback } from './public-types.js'; | ||
/** | ||
@@ -5,0 +5,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. |
// Smoldot | ||
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. | ||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
/// <reference lib="dom" /> | ||
import { start as innerStart } from './client.js'; | ||
import { ConnectionError } from './instance/instance.js'; | ||
import { classicDecode, multibaseBase64Decode } from './base64.js'; | ||
import { inflate } from 'pako'; | ||
import { default as wasmBase64 } from './instance/autogen/wasm.js'; | ||
export { AddChainError, AlreadyDestroyedError, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError } from './client.js'; | ||
import { startWithBytecode } from './no-auto-bytecode-browser.js'; | ||
import { compileBytecode } from './bytecode-browser.js'; | ||
export { AddChainError, AlreadyDestroyedError, CrashError, JsonRpcDisabledError, MalformedJsonRpcError, QueueFullError } from './public-types.js'; | ||
/** | ||
@@ -39,477 +16,3 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. | ||
options = options || {}; | ||
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a | ||
// different file. | ||
// This is suboptimal compared to using `instantiateStreaming`, but it is the most | ||
// cross-platform cross-bundler approach. | ||
const wasmModule = WebAssembly.compile(inflate(classicDecode(wasmBase64))); | ||
return innerStart(options, wasmModule, { | ||
registerShouldPeriodicallyYield: (callback) => { | ||
if (typeof document === 'undefined') // We might be in a web worker. | ||
return [false, () => { }]; | ||
const wrappedCallback = () => callback(document.visibilityState === 'visible'); | ||
document.addEventListener('visibilitychange', wrappedCallback); | ||
return [document.visibilityState === 'visible', () => { document.removeEventListener('visibilitychange', wrappedCallback); }]; | ||
}, | ||
performanceNow: () => { | ||
return performance.now(); | ||
}, | ||
getRandomValues: (buffer) => { | ||
const crypto = globalThis.crypto; | ||
if (!crypto) | ||
throw new Error('randomness not available'); | ||
// Browsers have this completely undocumented behavior (it's not even part of a spec) | ||
// that for some reason `getRandomValues` can't be called on arrayviews back by | ||
// `SharedArrayBuffer`s and they throw an exception if you try. | ||
if (buffer.buffer instanceof ArrayBuffer) | ||
crypto.getRandomValues(buffer); | ||
else { | ||
const tmpArray = new Uint8Array(buffer.length); | ||
crypto.getRandomValues(tmpArray); | ||
buffer.set(tmpArray); | ||
} | ||
}, | ||
connect: (config) => { | ||
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false, (options === null || options === void 0 ? void 0 : options.forbidWebRtc) || false); | ||
} | ||
}); | ||
return startWithBytecode(Object.assign({ bytecode: compileBytecode() }, options)); | ||
} | ||
/** | ||
* Tries to open a new connection using the given configuration. | ||
* | ||
* @see Connection | ||
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol. | ||
*/ | ||
function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) { | ||
// Attempt to parse the multiaddress. | ||
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940) | ||
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/); | ||
const webRTCParsed = config.address.match(/^\/(ip4|ip6)\/(.*?)\/udp\/(.*?)\/webrtc-direct\/certhash\/(.*?)$/); | ||
if (wsParsed != null) { | ||
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss'; | ||
if ((proto == 'ws' && forbidWs) || | ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) || | ||
(proto == 'wss' && forbidWss)) { | ||
throw new ConnectionError('Connection type not allowed'); | ||
} | ||
const url = (wsParsed[1] == 'ip6') ? | ||
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) : | ||
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]); | ||
const connection = new WebSocket(url); | ||
connection.binaryType = 'arraybuffer'; | ||
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 }; | ||
const checkBufferedAmount = () => { | ||
if (connection.readyState != 1) | ||
return; | ||
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths | ||
// of all the data that has been sent, but that might not be the case. For this | ||
// reason, we use `bufferedAmount` as a hint rather than a correct value. | ||
const bufferedAmount = connection.bufferedAmount; | ||
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount; | ||
if (wasSent < 0) | ||
wasSent = 0; | ||
bufferedAmountCheck.quenedUnreportedBytes -= wasSent; | ||
if (bufferedAmountCheck.quenedUnreportedBytes != 0) { | ||
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout); | ||
bufferedAmountCheck.nextTimeout *= 2; | ||
if (bufferedAmountCheck.nextTimeout > 500) | ||
bufferedAmountCheck.nextTimeout = 500; | ||
} | ||
// Note: it is important to call `onWritableBytes` at the very end, as it might | ||
// trigger a call to `send`. | ||
if (wasSent != 0) | ||
config.onWritableBytes(wasSent); | ||
}; | ||
connection.onopen = () => { | ||
config.onOpen({ | ||
type: 'single-stream', handshake: 'multistream-select-noise-yamux', | ||
initialWritableBytes: 1024 * 1024, writeClosable: false, | ||
}); | ||
}; | ||
connection.onclose = (event) => { | ||
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : ""); | ||
config.onConnectionReset(message); | ||
}; | ||
connection.onmessage = (msg) => { | ||
config.onMessage(new Uint8Array(msg.data)); | ||
}; | ||
return { | ||
reset: () => { | ||
connection.onopen = null; | ||
connection.onclose = null; | ||
connection.onmessage = null; | ||
connection.onerror = null; | ||
connection.close(); | ||
}, | ||
send: (data) => { | ||
connection.send(data); | ||
if (bufferedAmountCheck.quenedUnreportedBytes == 0) { | ||
bufferedAmountCheck.nextTimeout = 10; | ||
setTimeout(checkBufferedAmount, 10); | ||
} | ||
bufferedAmountCheck.quenedUnreportedBytes += data.length; | ||
}, | ||
closeSend: () => { throw new Error('Wrong connection type'); }, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else if (webRTCParsed != null) { | ||
const targetPort = webRTCParsed[3]; | ||
if (forbidWebRTC || targetPort === '0') { | ||
throw new ConnectionError('Connection type not allowed'); | ||
} | ||
const ipVersion = webRTCParsed[1] == 'ip4' ? '4' : '6'; | ||
const targetIp = webRTCParsed[2]; | ||
const remoteCertMultibase = webRTCParsed[4]; | ||
// The payload of `/certhash` is the hash of the self-generated certificate that the | ||
// server presents. | ||
// This function throws an exception if the certhash isn't correct. For this reason, this call | ||
// is performed as part of the parsing of the multiaddr. | ||
const remoteCertMultihash = multibaseBase64Decode(remoteCertMultibase); | ||
const remoteCertSha256Hash = multihashToSha256(remoteCertMultihash); | ||
// TODO: detect localhost for Firefox? https://bugzilla.mozilla.org/show_bug.cgi?id=1659672 | ||
// Note that `pc` can be the connection, but also null or undefined. | ||
// `undefined` means "certificate generation in progress", while `null` means "opening must | ||
// be cancelled". | ||
// While it would be better to use for example a string instead of `null`, using `null` lets | ||
// us use the `!` operator more easily and leads to more readable code. | ||
let pc = undefined; | ||
// Contains the data channels that are open and have been reported to smoldot. | ||
const dataChannels = new Map(); | ||
// For various reasons explained below, we open a data channel in advance without reporting it | ||
// to smoldot. This data channel is stored in this variable. Once it is reported to smoldot, | ||
// it is inserted in `dataChannels`. | ||
let handshakeDataChannel; | ||
// Multihash-encoded DTLS certificate of the local node. Unknown as long as it hasn't been | ||
// generated. | ||
// TODO: could be merged with `pc` in one variable, and maybe even the other fields as well | ||
let localTlsCertificateMultihash; | ||
// Kills all the JavaScript objects (the connection and all its substreams), ensuring that no | ||
// callback will be called again. Doesn't report anything to smoldot, as this should be done | ||
// by the caller. | ||
const killAllJs = () => { | ||
// The `RTCPeerConnection` is created pretty quickly. It is however still possible for | ||
// smoldot to cancel the opening, in which case `pc` will still be undefined. | ||
if (!pc) { | ||
console.assert(dataChannels.size === 0 && !handshakeDataChannel, "substreams exist while pc is undef"); | ||
pc = null; | ||
return; | ||
} | ||
pc.onconnectionstatechange = null; | ||
pc.onnegotiationneeded = null; | ||
pc.ondatachannel = null; | ||
for (const channel of Array.from(dataChannels.values())) { | ||
channel.channel.onopen = null; | ||
channel.channel.onerror = null; | ||
channel.channel.onclose = null; | ||
channel.channel.onbufferedamountlow = null; | ||
channel.channel.onmessage = null; | ||
} | ||
dataChannels.clear(); | ||
if (handshakeDataChannel) { | ||
handshakeDataChannel.onopen = null; | ||
handshakeDataChannel.onerror = null; | ||
handshakeDataChannel.onclose = null; | ||
handshakeDataChannel.onbufferedamountlow = null; | ||
handshakeDataChannel.onmessage = null; | ||
} | ||
handshakeDataChannel = undefined; | ||
pc.close(); // Not necessarily necessary, but it doesn't hurt to do so. | ||
}; | ||
// Function that configures a newly-opened channel and adds it to the map. Used for both | ||
// inbound and outbound substreams. | ||
const addChannel = (dataChannel, direction) => { | ||
const dataChannelId = dataChannel.id; | ||
dataChannel.binaryType = 'arraybuffer'; | ||
let isOpen = false; | ||
dataChannel.onopen = () => { | ||
console.assert(!isOpen, "substream opened twice"); | ||
isOpen = true; | ||
if (direction === 'first-outbound') { | ||
console.assert(dataChannels.size === 0, "dataChannels not empty when opening"); | ||
console.assert(handshakeDataChannel === dataChannel, "handshake substream mismatch"); | ||
config.onOpen({ | ||
type: 'multi-stream', | ||
handshake: 'webrtc', | ||
// `addChannel` can never be called before the local certificate is generated, so this | ||
// value is always defined. | ||
localTlsCertificateMultihash: localTlsCertificateMultihash, | ||
remoteTlsCertificateMultihash: remoteCertMultihash | ||
}); | ||
} | ||
else { | ||
console.assert(direction !== 'outbound' || !handshakeDataChannel, "handshakeDataChannel still defined"); | ||
config.onStreamOpened(dataChannelId, direction, 65536); | ||
} | ||
}; | ||
dataChannel.onerror = dataChannel.onclose = (_error) => { | ||
// A couple of different things could be happening here. | ||
if (handshakeDataChannel === dataChannel && !isOpen) { | ||
// The handshake data channel that we have opened ahead of time failed to open. As this | ||
// happens before we have reported the WebRTC connection as a whole as being open, we | ||
// need to report that the connection has failed to open. | ||
killAllJs(); | ||
// Note that the event doesn't give any additional reason for the failure. | ||
config.onConnectionReset("handshake data channel failed to open"); | ||
} | ||
else if (handshakeDataChannel === dataChannel) { | ||
// The handshake data channel has been closed before we reported it to smoldot. This | ||
// isn't really a problem. We just update the state and continue running. If smoldot | ||
// requests a substream, another one will be opened. It could be a valid implementation | ||
// to also just kill the entire connection, however doing so is a bit too intrusive and | ||
// punches through abstraction layers. | ||
handshakeDataChannel.onopen = null; | ||
handshakeDataChannel.onerror = null; | ||
handshakeDataChannel.onclose = null; | ||
handshakeDataChannel.onbufferedamountlow = null; | ||
handshakeDataChannel.onmessage = null; | ||
handshakeDataChannel = undefined; | ||
} | ||
else if (!isOpen) { | ||
// Substream wasn't opened yet and thus has failed to open. The API has no mechanism to | ||
// report substream openings failures. We could try opening it again, but given that | ||
// it's unlikely to succeed, we simply opt to kill the entire connection. | ||
killAllJs(); | ||
// Note that the event doesn't give any additional reason for the failure. | ||
config.onConnectionReset("data channel failed to open"); | ||
} | ||
else { | ||
// Substream was open and is now closed. Normal situation. | ||
config.onStreamReset(dataChannelId); | ||
} | ||
}; | ||
dataChannel.onbufferedamountlow = () => { | ||
const channel = dataChannels.get(dataChannelId); | ||
const val = channel.bufferedBytes; | ||
channel.bufferedBytes = 0; | ||
config.onWritableBytes(val, dataChannelId); | ||
}; | ||
dataChannel.onmessage = (m) => { | ||
// The `data` field is an `ArrayBuffer`. | ||
config.onMessage(new Uint8Array(m.data), dataChannelId); | ||
}; | ||
if (direction !== 'first-outbound') | ||
dataChannels.set(dataChannelId, { channel: dataChannel, bufferedBytes: 0 }); | ||
else | ||
handshakeDataChannel = dataChannel; | ||
}; | ||
// It is possible for the browser to use multiple different certificates. | ||
// In order for our local certificate to be deterministic, we need to generate it manually and | ||
// set it explicitly as part of the configuration. | ||
// According to <https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-generatecertificate>, | ||
// browsers are guaranteed to support `{ name: "ECDSA", namedCurve: "P-256" }`. | ||
RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }).then((localCertificate) => __awaiter(this, void 0, void 0, function* () { | ||
if (pc === null) | ||
return; | ||
// Create a new WebRTC connection. | ||
pc = new RTCPeerConnection({ certificates: [localCertificate] }); | ||
// We need to build the multihash corresponding to the local certificate. | ||
// While there exists a `RTCPeerConnection.getFingerprints` function, Firefox notably | ||
// doesn't support it. | ||
// See <https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate#browser_compatibility> | ||
// An alternative to `getFingerprints` is to ask the browser to generate an SDP offer and | ||
// extract from fingerprint from it. Because we explicitly provide a certificate, we have | ||
// the guarantee that the list of certificates will always be the same whenever an SDP offer | ||
// is generated by the browser. However, while this alternative does work on Firefox, it | ||
// doesn't on Chrome, as the SDP offer is for some reason missing the fingerprints. | ||
// Therefore, our strategy is to use `getFingerprints` when it is available (i.e. every | ||
// browser except Firefox), and parse the SDP offer when it is not (i.e. Firefox). In the | ||
// future, only `getFingerprints` would be used. | ||
let localTlsCertificateHex; | ||
if (localCertificate.getFingerprints) { | ||
for (const { algorithm, value } of localCertificate.getFingerprints()) { | ||
if (algorithm === 'sha-256') { | ||
localTlsCertificateHex = value; | ||
break; | ||
} | ||
} | ||
} | ||
else { | ||
const localSdpOffer = yield pc.createOffer(); | ||
// Note that this regex is not strict. The browser isn't a malicious actor, and the | ||
// objective of this regex is not to detect invalid input. | ||
const localSdpOfferFingerprintMatch = localSdpOffer.sdp.match(/a(\s*)=(\s*)fingerprint:(\s*)(sha|SHA)-256(\s*)(([a-fA-F0-9]{2}(:)*){32})/); | ||
if (localSdpOfferFingerprintMatch) { | ||
localTlsCertificateHex = localSdpOfferFingerprintMatch[6]; | ||
} | ||
} | ||
if (localTlsCertificateHex === undefined) { | ||
// Because we've already returned from the `connect` function at this point, we pretend | ||
// that the connection has failed to open. | ||
config.onConnectionReset('Failed to obtain the browser certificate fingerprint'); | ||
return; | ||
} | ||
localTlsCertificateMultihash = new Uint8Array(34); | ||
localTlsCertificateMultihash.set([0x12, 32], 0); | ||
localTlsCertificateMultihash.set(localTlsCertificateHex.split(':').map((s) => parseInt(s, 16)), 2); | ||
// `onconnectionstatechange` is used to detect when the connection has closed or has failed | ||
// to open. | ||
// Note that smoldot will think that the connection is open even when it is still opening. | ||
// Therefore we don't care about events concerning the fact that the connection is now fully | ||
// open. | ||
pc.onconnectionstatechange = (_event) => { | ||
if (pc.connectionState == "closed" || pc.connectionState == "disconnected" || pc.connectionState == "failed") { | ||
killAllJs(); | ||
config.onConnectionReset("WebRTC state transitioned to " + pc.connectionState); | ||
} | ||
}; | ||
pc.onnegotiationneeded = (_event) => __awaiter(this, void 0, void 0, function* () { | ||
var _a; | ||
// Create a new offer and set it as local description. | ||
let sdpOffer = (yield pc.createOffer()).sdp; | ||
// We check that the locally-generated SDP offer has a data channel with the UDP | ||
// protocol. If that isn't the case, the connection will likely fail. | ||
if (sdpOffer.match(/^m=application(\s+)(\d+)(\s+)UDP\/DTLS\/SCTP(\s+)webrtc-datachannel$/m) === null) { | ||
console.error("Local offer doesn't contain UDP data channel. WebRTC connections will likely fail. Please report this issue."); | ||
} | ||
// According to the libp2p WebRTC spec, the ufrag and pwd are the same | ||
// randomly-generated string on both sides, and must be prefixed with | ||
// `libp2p-webrtc-v1:`. We modify the local description to ensure that. | ||
// While we could randomly generate a new string, we just grab the one that the | ||
// browser has generated, in order to make sure that it respects the constraints | ||
// of the ICE protocol. | ||
const browserGeneratedPwd = (_a = sdpOffer.match(/^a=ice-pwd:(.+)$/m)) === null || _a === void 0 ? void 0 : _a.at(1); | ||
if (browserGeneratedPwd === undefined) { | ||
console.error("Failed to set ufrag to pwd. WebRTC connections will likely fail. Please report this issue."); | ||
} | ||
const ufragPwd = "libp2p+webrtc+v1/" + browserGeneratedPwd; | ||
sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:' + ufragPwd); | ||
sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:' + ufragPwd); | ||
yield pc.setLocalDescription({ type: 'offer', sdp: sdpOffer }); | ||
// Transform certificate hash into fingerprint (upper-hex; each byte separated by ":"). | ||
const fingerprint = Array.from(remoteCertSha256Hash).map((n) => ("0" + n.toString(16)).slice(-2).toUpperCase()).join(':'); | ||
// Note that the trailing line feed is important, as otherwise Chrome | ||
// fails to parse the payload. | ||
const remoteSdp = | ||
// Version of the SDP protocol. Always 0. (RFC8866) | ||
"v=0" + "\n" + | ||
// Identifies the creator of the SDP document. We are allowed to use dummy values | ||
// (`-` and `0.0.0.0`) to remain anonymous, which we do. Note that "IN" means | ||
// "Internet" (and not "input"). (RFC8866) | ||
"o=- 0 0 IN IP" + ipVersion + " " + targetIp + "\n" + | ||
// Name for the session. We are allowed to pass a dummy `-`. (RFC8866) | ||
"s=-" + "\n" + | ||
// Start and end of the validity of the session. `0 0` means that the session never | ||
// expires. (RFC8866) | ||
"t=0 0" + "\n" + | ||
// A lite implementation is only appropriate for devices that will | ||
// always be connected to the public Internet and have a public | ||
// IP address at which it can receive packets from any | ||
// correspondent. ICE will not function when a lite implementation | ||
// is placed behind a NAT (RFC8445). | ||
"a=ice-lite" + "\n" + | ||
// A `m=` line describes a request to establish a certain protocol. | ||
// The protocol in this line (i.e. `TCP/DTLS/SCTP` or `UDP/DTLS/SCTP`) must always be | ||
// the same as the one in the offer. We know that this is true because checked above. | ||
// The `<fmt>` component must always be `webrtc-datachannel` for WebRTC. | ||
// The rest of the SDP payload adds attributes to this specific media stream. | ||
// RFCs: 8839, 8866, 8841 | ||
"m=application " + targetPort + " " + "UDP/DTLS/SCTP webrtc-datachannel" + "\n" + | ||
// Indicates the IP address of the remote. | ||
// Note that "IN" means "Internet" (and not "input"). | ||
"c=IN IP" + ipVersion + " " + targetIp + "\n" + | ||
// Media ID - uniquely identifies this media stream (RFC9143). | ||
"a=mid:0" + "\n" + | ||
// Indicates that we are complying with RFC8839 (as oppposed to the legacy RFC5245). | ||
"a=ice-options:ice2" + "\n" + | ||
// ICE username and password, which are used for establishing and | ||
// maintaining the ICE connection. (RFC8839) | ||
// These values are set according to the libp2p WebRTC specification. | ||
"a=ice-ufrag:" + ufragPwd + "\n" + | ||
"a=ice-pwd:" + ufragPwd + "\n" + | ||
// Fingerprint of the certificate that the server will use during the TLS | ||
// handshake. (RFC8122) | ||
// MUST be derived from the certificate used by the answerer (server). | ||
"a=fingerprint:sha-256 " + fingerprint + "\n" + | ||
// Indicates that the remote DTLS server will only listen for incoming | ||
// connections. (RFC5763) | ||
// The answerer (server) MUST not be located behind a NAT (RFC6135). | ||
"a=setup:passive" + "\n" + | ||
// The SCTP port (RFC8841) | ||
// Note it's different from the "m=" line port value, which | ||
// indicates the port of the underlying transport-layer protocol | ||
// (UDP or TCP) | ||
"a=sctp-port:5000" + "\n" + | ||
// The maximum SCTP user message size (in bytes) (RFC8841) | ||
// Setting this field is part of the libp2p spec. | ||
"a=max-message-size:16384" + "\n" + | ||
// A transport address for a candidate that can be used for connectivity | ||
// checks (RFC8839). | ||
"a=candidate:1 1 UDP 1 " + targetIp + " " + targetPort + " typ host" + "\n"; | ||
yield pc.setRemoteDescription({ type: "answer", sdp: remoteSdp }); | ||
}); | ||
pc.ondatachannel = ({ channel }) => { | ||
// TODO: is the substream maybe already open? according to the Internet it seems that no but it's unclear | ||
addChannel(channel, 'inbound'); | ||
}; | ||
// Creating a `RTCPeerConnection` doesn't actually do anything before `createDataChannel` is | ||
// called. Smoldot's API, however, requires you to treat entire connections as open or | ||
// closed. We know, according to the libp2p WebRTC specification, that every connection | ||
// always starts with a substream where a handshake is performed. After we've reported that | ||
// the connection is open, smoldot will open a substream in order to perform the handshake. | ||
// Instead of following this API, we open this substream in advance, and will notify smoldot | ||
// that the connection is open when the substream is open. | ||
// Note that the label passed to `createDataChannel` is required to be empty as per the | ||
// libp2p WebRTC specification. | ||
addChannel(pc.createDataChannel("", { id: 0, negotiated: true }), 'first-outbound'); | ||
})); | ||
return { | ||
reset: (streamId) => { | ||
// If `streamId` is undefined, then the whole connection must be destroyed. | ||
if (streamId === undefined) { | ||
killAllJs(); | ||
} | ||
else { | ||
const channel = dataChannels.get(streamId); | ||
channel.channel.onopen = null; | ||
channel.channel.onerror = null; | ||
channel.channel.onclose = null; | ||
channel.channel.onbufferedamountlow = null; | ||
channel.channel.onmessage = null; | ||
channel.channel.close(); | ||
dataChannels.delete(streamId); | ||
} | ||
}, | ||
send: (data, streamId) => { | ||
const channel = dataChannels.get(streamId); | ||
channel.channel.send(data); | ||
channel.bufferedBytes += data.length; | ||
}, | ||
closeSend: () => { throw new Error('Wrong connection type'); }, | ||
openOutSubstream: () => { | ||
// `openOutSubstream` can only be called after we have called `config.onOpen`, therefore | ||
// `pc` is guaranteed to be non-null. | ||
// As explained above, we open a data channel ahead of time. If this data channel is still | ||
// there, we report it. | ||
if (handshakeDataChannel) { | ||
// Do this asynchronously because calling callbacks within callbacks is error-prone. | ||
(() => __awaiter(this, void 0, void 0, function* () { | ||
// We need to check again if `handshakeDataChannel` is still defined, as the | ||
// connection might have been closed. | ||
if (handshakeDataChannel) { | ||
config.onStreamOpened(handshakeDataChannel.id, 'outbound', 1024 * 1024); | ||
dataChannels.set(handshakeDataChannel.id, { channel: handshakeDataChannel, bufferedBytes: 0 }); | ||
handshakeDataChannel = undefined; | ||
} | ||
}))(); | ||
} | ||
else { | ||
// Note that the label passed to `createDataChannel` is required to be empty as per the | ||
// libp2p WebRTC specification. | ||
addChannel(pc.createDataChannel(""), 'outbound'); | ||
} | ||
} | ||
}; | ||
} | ||
else { | ||
throw new ConnectionError('Unrecognized multiaddr format'); | ||
} | ||
} | ||
/// Parses a multihash-multibase-encoded string into a SHA256 hash. | ||
/// | ||
/// Throws an exception if the multihash algorithm isn't SHA256. | ||
const multihashToSha256 = (certMultihash) => { | ||
if (certMultihash.length != 34 || certMultihash[0] != 0x12 || certMultihash[1] != 32) { | ||
throw new Error('Certificate multihash is not SHA-256'); | ||
} | ||
return new Uint8Array(certMultihash.slice(2)); | ||
}; |
@@ -1,3 +0,3 @@ | ||
import { Client, ClientOptions } from './client.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './client.js'; | ||
import { Client, ClientOptions } from './public-types.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './public-types.js'; | ||
/** | ||
@@ -4,0 +4,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. |
// Smoldot | ||
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. | ||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
import { start as innerStart } from './client.js'; | ||
import { ConnectionError } from './instance/instance.js'; | ||
import { default as wasmBase64 } from './instance/autogen/wasm.js'; | ||
export { AddChainError, AlreadyDestroyedError, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError } from './client.js'; | ||
import { startWithBytecode } from './no-auto-bytecode-deno.js'; | ||
import { compileBytecode } from './bytecode-deno.js'; | ||
export { AddChainError, AlreadyDestroyedError, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError } from './public-types.js'; | ||
/** | ||
@@ -36,256 +16,3 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. | ||
options = options || {}; | ||
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a | ||
// different file. | ||
// This is suboptimal compared to using `instantiateStreaming`, but it is the most | ||
// cross-platform cross-bundler approach. | ||
const wasmModule = zlibInflate(trustedBase64Decode(wasmBase64)).then(((bytecode) => WebAssembly.compile(bytecode))); | ||
return innerStart(options || {}, wasmModule, { | ||
registerShouldPeriodicallyYield: (_callback) => { | ||
return [true, () => { }]; | ||
}, | ||
performanceNow: () => { | ||
return performance.now(); | ||
}, | ||
getRandomValues: (buffer) => { | ||
const crypto = globalThis.crypto; | ||
if (!crypto) | ||
throw new Error('randomness not available'); | ||
crypto.getRandomValues(buffer); | ||
}, | ||
connect: (config) => { | ||
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidTcp) || false, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false); | ||
} | ||
}); | ||
return startWithBytecode(Object.assign({ bytecode: compileBytecode() }, options)); | ||
} | ||
/** | ||
* Applies the zlib inflate algorithm on the buffer. | ||
*/ | ||
function zlibInflate(buffer) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// This code has been copy-pasted from the official streams draft specification. | ||
// At the moment, it is found here: https://wicg.github.io/compression/#example-deflate-compress | ||
const ds = new DecompressionStream('deflate'); | ||
const writer = ds.writable.getWriter(); | ||
writer.write(buffer); | ||
writer.close(); | ||
const output = []; | ||
const reader = ds.readable.getReader(); | ||
let totalSize = 0; | ||
while (true) { | ||
const { value, done } = yield reader.read(); | ||
if (done) | ||
break; | ||
output.push(value); | ||
totalSize += value.byteLength; | ||
} | ||
const concatenated = new Uint8Array(totalSize); | ||
let offset = 0; | ||
for (const array of output) { | ||
concatenated.set(array, offset); | ||
offset += array.byteLength; | ||
} | ||
return concatenated; | ||
}); | ||
} | ||
/** | ||
* Decodes a base64 string. | ||
* | ||
* The input is assumed to be correct. | ||
*/ | ||
function trustedBase64Decode(base64) { | ||
// This code is a bit sketchy due to the fact that we decode into a string, but it seems to | ||
// work. | ||
const binaryString = atob(base64); | ||
const size = binaryString.length; | ||
const bytes = new Uint8Array(size); | ||
for (let i = 0; i < size; i++) { | ||
bytes[i] = binaryString.charCodeAt(i); | ||
} | ||
return bytes; | ||
} | ||
/** | ||
* Tries to open a new connection using the given configuration. | ||
* | ||
* @see Connection | ||
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol. | ||
*/ | ||
function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) { | ||
// Attempt to parse the multiaddress. | ||
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940) | ||
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/); | ||
const tcpParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)$/); | ||
if (wsParsed != null) { | ||
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss'; | ||
if ((proto == 'ws' && forbidWs) || | ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) || | ||
(proto == 'wss' && forbidWss)) { | ||
throw new ConnectionError('Connection type not allowed'); | ||
} | ||
const url = (wsParsed[1] == 'ip6') ? | ||
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) : | ||
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]); | ||
const socket = new WebSocket(url); | ||
socket.binaryType = 'arraybuffer'; | ||
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 }; | ||
const checkBufferedAmount = () => { | ||
if (socket.readyState != 1) | ||
return; | ||
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths | ||
// of all the data that has been sent, but that might not be the case. For this | ||
// reason, we use `bufferedAmount` as a hint rather than a correct value. | ||
const bufferedAmount = socket.bufferedAmount; | ||
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount; | ||
if (wasSent < 0) | ||
wasSent = 0; | ||
bufferedAmountCheck.quenedUnreportedBytes -= wasSent; | ||
if (bufferedAmountCheck.quenedUnreportedBytes != 0) { | ||
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout); | ||
bufferedAmountCheck.nextTimeout *= 2; | ||
if (bufferedAmountCheck.nextTimeout > 500) | ||
bufferedAmountCheck.nextTimeout = 500; | ||
} | ||
// Note: it is important to call `onWritableBytes` at the very end, as it might | ||
// trigger a call to `send`. | ||
if (wasSent != 0) | ||
config.onWritableBytes(wasSent); | ||
}; | ||
socket.onopen = () => { | ||
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false }); | ||
}; | ||
socket.onclose = (event) => { | ||
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : ""); | ||
config.onConnectionReset(message); | ||
}; | ||
socket.onmessage = (msg) => { | ||
config.onMessage(new Uint8Array(msg.data)); | ||
}; | ||
return { | ||
reset: () => { | ||
// We can't set these fields to null because the TypeScript definitions don't | ||
// allow it, but we can set them to dummy values. | ||
socket.onopen = () => { }; | ||
socket.onclose = () => { }; | ||
socket.onmessage = () => { }; | ||
socket.onerror = () => { }; | ||
socket.close(); | ||
}, | ||
send: (data) => { | ||
// The WebSocket library that we use seems to spontaneously transition connections | ||
// to the "closed" state but not call the `onclosed` callback immediately. Calling | ||
// `send` on that object throws an exception. In order to avoid panicking smoldot, | ||
// we thus absorb any exception thrown here. | ||
// See also <https://github.com/paritytech/smoldot/issues/2937>. | ||
try { | ||
socket.send(data); | ||
if (bufferedAmountCheck.quenedUnreportedBytes == 0) { | ||
bufferedAmountCheck.nextTimeout = 10; | ||
setTimeout(checkBufferedAmount, 10); | ||
} | ||
bufferedAmountCheck.quenedUnreportedBytes += data.length; | ||
} | ||
catch (_error) { } | ||
}, | ||
closeSend: () => { throw new Error('Wrong connection type'); }, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else if (tcpParsed != null) { | ||
if (forbidTcp) { | ||
throw new ConnectionError('TCP connections not available'); | ||
} | ||
const socket = { | ||
destroyed: false, | ||
inner: Deno.connect({ | ||
hostname: tcpParsed[2], | ||
port: parseInt(tcpParsed[3], 10), | ||
}).catch((error) => { | ||
socket.destroyed = true; | ||
config.onConnectionReset(error.toString()); | ||
return null; | ||
}) | ||
}; | ||
socket.inner = socket.inner.then((established) => { | ||
// TODO: at the time of writing of this comment, `setNoDelay` is still unstable | ||
//established.setNoDelay(); | ||
if (socket.destroyed) | ||
return established; | ||
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: true }); | ||
// Spawns an asynchronous task that continuously reads from the socket. | ||
// Every time data is read, the task re-executes itself in order to continue reading. | ||
// The task ends automatically if an EOF or error is detected, which should also happen | ||
// if the user calls `close()`. | ||
const read = (readBuffer) => __awaiter(this, void 0, void 0, function* () { | ||
if (socket.destroyed || established === null) | ||
return; | ||
let outcome = null; | ||
try { | ||
outcome = yield established.read(readBuffer); | ||
} | ||
catch (error) { | ||
// The type of `error` is unclear, but we assume that it implements `Error` | ||
outcome = error.toString(); | ||
} | ||
if (socket.destroyed) | ||
return; | ||
if (typeof outcome !== 'number' || outcome === null) { | ||
// The socket is reported closed, but `socket.destroyed` is still `false` (see | ||
// check above). As such, we must inform the inner layers. | ||
socket.destroyed = true; | ||
config.onConnectionReset(outcome === null ? "EOF when reading socket" : outcome); | ||
return; | ||
} | ||
console.assert(outcome !== 0); // `read` guarantees to return a non-zero value. | ||
config.onMessage(readBuffer.slice(0, outcome)); | ||
return read(readBuffer); | ||
}); | ||
read(new Uint8Array(32768)); | ||
return established; | ||
}); | ||
return { | ||
reset: () => { | ||
socket.destroyed = true; | ||
socket.inner.then((connec) => connec.close()); | ||
}, | ||
send: (data) => { | ||
let dataCopy = Uint8Array.from(data); // Deep copy of the data | ||
socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () { | ||
while (dataCopy.length > 0) { | ||
if (socket.destroyed || c === null) | ||
return c; | ||
let outcome; | ||
try { | ||
outcome = yield c.write(dataCopy); | ||
config.onWritableBytes(dataCopy.length); | ||
} | ||
catch (error) { | ||
// The type of `error` is unclear, but we assume that it implements `Error` | ||
outcome = error.toString(); | ||
} | ||
if (typeof outcome !== 'number') { | ||
// The socket is reported closed, but `socket.destroyed` is still | ||
// `false` (see check above). As such, we must inform the inner layers. | ||
socket.destroyed = true; | ||
config.onConnectionReset(outcome); | ||
return c; | ||
} | ||
// Note that, contrary to `read`, it is possible for `outcome` to be 0. | ||
// This happen if the write had to be interrupted, and the only thing | ||
// we have to do is try writing again. | ||
dataCopy = dataCopy.slice(outcome); | ||
} | ||
return c; | ||
})); | ||
}, | ||
closeSend: () => { | ||
socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () { | ||
yield (c === null || c === void 0 ? void 0 : c.closeWrite()); | ||
return c; | ||
})); | ||
}, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else { | ||
throw new ConnectionError('Unrecognized multiaddr format'); | ||
} | ||
} |
@@ -1,3 +0,3 @@ | ||
import { Client, ClientOptions } from './client.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './client.js'; | ||
import { Client, ClientOptions } from './public-types.js'; | ||
export { AddChainError, AddChainOptions, AlreadyDestroyedError, Chain, Client, ClientOptions, ClientOptionsWithBytecode, SmoldotBytecode, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError, LogCallback } from './public-types.js'; | ||
/** | ||
@@ -4,0 +4,0 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. |
// Smoldot | ||
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. | ||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
// Note: if you modify these imports, please test both the ModuleJS and CommonJS generated | ||
// bindings. JavaScript being JavaScript, some libraries (such as `websocket`) have issues working | ||
// with both at the same time. | ||
import { start as innerStart } from './client.js'; | ||
import { ConnectionError } from './instance/instance.js'; | ||
import { default as wasmBase64 } from './instance/autogen/wasm.js'; | ||
import { WebSocket } from 'ws'; | ||
import { inflate } from 'pako'; | ||
import { performance } from 'node:perf_hooks'; | ||
import { createConnection as nodeCreateConnection } from 'node:net'; | ||
import { randomFillSync } from 'node:crypto'; | ||
export { AddChainError, AlreadyDestroyedError, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError } from './client.js'; | ||
import { startWithBytecode } from './no-auto-bytecode-nodejs.js'; | ||
import { compileBytecode } from './bytecode-nodejs.js'; | ||
export { AddChainError, AlreadyDestroyedError, CrashError, MalformedJsonRpcError, QueueFullError, JsonRpcDisabledError } from './public-types.js'; | ||
/** | ||
@@ -35,185 +16,3 @@ * Initializes a new client. This is a pre-requisite to connecting to a blockchain. | ||
options = options || {}; | ||
// The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a | ||
// different file. | ||
// This is suboptimal compared to using `instantiateStreaming`, but it is the most | ||
// cross-platform cross-bundler approach. | ||
const wasmModule = WebAssembly.compile(inflate(Buffer.from(wasmBase64, 'base64'))); | ||
return innerStart(options || {}, wasmModule, { | ||
registerShouldPeriodicallyYield: (_callback) => { | ||
return [true, () => { }]; | ||
}, | ||
performanceNow: () => { | ||
return performance.now(); | ||
}, | ||
getRandomValues: (buffer) => { | ||
if (buffer.length >= 1024 * 1024) | ||
throw new Error('getRandomValues buffer too large'); | ||
randomFillSync(buffer); | ||
}, | ||
connect: (config) => { | ||
return connect(config, (options === null || options === void 0 ? void 0 : options.forbidTcp) || false, (options === null || options === void 0 ? void 0 : options.forbidWs) || false, (options === null || options === void 0 ? void 0 : options.forbidNonLocalWs) || false, (options === null || options === void 0 ? void 0 : options.forbidWss) || false); | ||
} | ||
}); | ||
return startWithBytecode(Object.assign({ bytecode: compileBytecode() }, options)); | ||
} | ||
/** | ||
* Tries to open a new connection using the given configuration. | ||
* | ||
* @see Connection | ||
* @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol. | ||
*/ | ||
function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) { | ||
// Attempt to parse the multiaddress. | ||
// TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940) | ||
const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/); | ||
const tcpParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)$/); | ||
if (wsParsed != null) { | ||
const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss'; | ||
if ((proto == 'ws' && forbidWs) || | ||
(proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) || | ||
(proto == 'wss' && forbidWss)) { | ||
throw new ConnectionError('Connection type not allowed'); | ||
} | ||
const url = (wsParsed[1] == 'ip6') ? | ||
(proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) : | ||
(proto + "://" + wsParsed[2] + ":" + wsParsed[3]); | ||
const socket = new WebSocket(url); | ||
socket.binaryType = 'arraybuffer'; | ||
const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 }; | ||
const checkBufferedAmount = () => { | ||
if (socket.readyState != 1) | ||
return; | ||
// Note that we might expect `bufferedAmount` to always be <= the sum of the lengths | ||
// of all the data that has been sent, but that seems to not be the case. It is | ||
// unclear whether this is intended or a bug, but is is likely that `bufferedAmount` | ||
// also includes WebSocket headers. For this reason, we use `bufferedAmount` as a hint | ||
// rather than a correct value. | ||
const bufferedAmount = socket.bufferedAmount; | ||
let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount; | ||
if (wasSent < 0) | ||
wasSent = 0; | ||
bufferedAmountCheck.quenedUnreportedBytes -= wasSent; | ||
if (bufferedAmountCheck.quenedUnreportedBytes != 0) { | ||
setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout); | ||
bufferedAmountCheck.nextTimeout *= 2; | ||
if (bufferedAmountCheck.nextTimeout > 500) | ||
bufferedAmountCheck.nextTimeout = 500; | ||
} | ||
// Note: it is important to call `onWritableBytes` at the very end, as it might | ||
// trigger a call to `send`. | ||
if (wasSent != 0) | ||
config.onWritableBytes(wasSent); | ||
}; | ||
socket.onopen = () => { | ||
config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false }); | ||
}; | ||
socket.onclose = (event) => { | ||
const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : ""); | ||
config.onConnectionReset(message); | ||
socket.onopen = () => { }; | ||
socket.onclose = () => { }; | ||
socket.onmessage = () => { }; | ||
socket.onerror = () => { }; | ||
}; | ||
socket.onerror = (event) => { | ||
config.onConnectionReset(event.message); | ||
socket.onopen = () => { }; | ||
socket.onclose = () => { }; | ||
socket.onmessage = () => { }; | ||
socket.onerror = () => { }; | ||
}; | ||
socket.onmessage = (msg) => { | ||
config.onMessage(new Uint8Array(msg.data)); | ||
}; | ||
return { | ||
reset: () => { | ||
// We can't set these fields to null because the TypeScript definitions don't | ||
// allow it, but we can set them to dummy values. | ||
socket.onopen = () => { }; | ||
socket.onclose = () => { }; | ||
socket.onmessage = () => { }; | ||
socket.onerror = () => { }; | ||
socket.close(); | ||
}, | ||
send: (data) => { | ||
socket.send(data); | ||
if (bufferedAmountCheck.quenedUnreportedBytes == 0) { | ||
bufferedAmountCheck.nextTimeout = 10; | ||
setTimeout(checkBufferedAmount, 10); | ||
} | ||
bufferedAmountCheck.quenedUnreportedBytes += data.length; | ||
}, | ||
closeSend: () => { throw new Error('Wrong connection type'); }, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else if (tcpParsed != null) { | ||
// `net` module will be missing when we're not in NodeJS. | ||
if (forbidTcp) { | ||
throw new ConnectionError('TCP connections not available'); | ||
} | ||
const socket = nodeCreateConnection({ | ||
host: tcpParsed[2], | ||
port: parseInt(tcpParsed[3], 10), | ||
}); | ||
// Number of bytes queued using `socket.write` and where `write` has returned false. | ||
const drainingBytes = { num: 0 }; | ||
socket.setNoDelay(); | ||
socket.on('connect', () => { | ||
if (socket.destroyed) | ||
return; | ||
config.onOpen({ | ||
type: 'single-stream', handshake: 'multistream-select-noise-yamux', | ||
initialWritableBytes: socket.writableHighWaterMark, writeClosable: true | ||
}); | ||
}); | ||
socket.on('close', (hasError) => { | ||
if (socket.destroyed) | ||
return; | ||
// NodeJS doesn't provide a reason why the closing happened, but only | ||
// whether it was caused by an error. | ||
const message = hasError ? "Error" : "Closed gracefully"; | ||
config.onConnectionReset(message); | ||
}); | ||
socket.on('error', () => { }); | ||
socket.on('data', (message) => { | ||
if (socket.destroyed) | ||
return; | ||
config.onMessage(new Uint8Array(message.buffer)); | ||
}); | ||
socket.on('drain', () => { | ||
// The bytes queued using `socket.write` and where `write` has returned false have now | ||
// been sent. Notify the API that it can write more data. | ||
if (socket.destroyed) | ||
return; | ||
const val = drainingBytes.num; | ||
drainingBytes.num = 0; | ||
config.onWritableBytes(val); | ||
}); | ||
return { | ||
reset: () => { | ||
socket.destroy(); | ||
}, | ||
send: (data) => { | ||
const dataLen = data.length; | ||
const allWritten = socket.write(data); | ||
if (allWritten) { | ||
setImmediate(() => { | ||
if (!socket.writable) | ||
return; | ||
config.onWritableBytes(dataLen); | ||
}); | ||
} | ||
else { | ||
drainingBytes.num += dataLen; | ||
} | ||
}, | ||
closeSend: () => { | ||
socket.end(); | ||
}, | ||
openOutSubstream: () => { throw new Error('Wrong connection type'); } | ||
}; | ||
} | ||
else { | ||
throw new ConnectionError('Unrecognized multiaddr format'); | ||
} | ||
} |
{ | ||
"name": "smoldot", | ||
"version": "1.0.4", | ||
"version": "1.0.5", | ||
"description": "Light client that connects to Polkadot and Substrate-based blockchains", | ||
@@ -22,9 +22,41 @@ "author": "Parity Technologies <admin@parity.io>", | ||
"exports": { | ||
"node": { | ||
"import": "./dist/mjs/index-nodejs.js", | ||
"require": "./dist/cjs/index-nodejs.js" | ||
".": { | ||
"node": { | ||
"import": "./dist/mjs/index-nodejs.js", | ||
"require": "./dist/cjs/index-nodejs.js" | ||
}, | ||
"default": { | ||
"import": "./dist/mjs/index-browser.js", | ||
"require": "./dist/cjs/index-browser.js" | ||
} | ||
}, | ||
"default": { | ||
"import": "./dist/mjs/index-browser.js", | ||
"require": "./dist/cjs/index-browser.js" | ||
"./no-auto-bytecode": { | ||
"node": { | ||
"import": "./dist/mjs/no-auto-bytecode-nodejs.js", | ||
"require": "./dist/cjs/no-auto-bytecode-nodejs.js" | ||
}, | ||
"default": { | ||
"import": "./dist/mjs/no-auto-bytecode-browser.js", | ||
"require": "./dist/cjs/no-auto-bytecode-browser.js" | ||
} | ||
}, | ||
"./worker": { | ||
"node": { | ||
"import": "./dist/mjs/worker-nodejs.js", | ||
"require": "./dist/cjs/worker-nodejs.js" | ||
}, | ||
"default": { | ||
"import": "./dist/mjs/worker-browser.js", | ||
"require": "./dist/cjs/worker-browser.js" | ||
} | ||
}, | ||
"./bytecode": { | ||
"node": { | ||
"import": "./dist/mjs/bytecode-nodejs.js", | ||
"require": "./dist/cjs/bytecode-nodejs.js" | ||
}, | ||
"default": { | ||
"import": "./dist/mjs/bytecode-browser.js", | ||
"require": "./dist/cjs/bytecode-browser.js" | ||
} | ||
} | ||
@@ -31,0 +63,0 @@ }, |
@@ -70,1 +70,59 @@ # Light client for Polkadot and Substrate-based chains | ||
sandbox". | ||
## Usage with a worker | ||
By default, calling `start()` will run smoldot entirely in the current thread. This can cause | ||
performance issues if other CPU-heavy operations are done in that thread. | ||
In order to help with this, it possible to use smoldot in conjunction with a worker. | ||
To do so, you must first create a worker. Since creating a worker has some subtle differences | ||
depending on the platform, this is outside of the responsibility of smoldot. | ||
Once the worker is created, create two `MessagePort`s using `new MessageChannel`, and send one | ||
of them to the worker. Then, pass one port to the `ClientOptions.portToWorker` field and the | ||
other port to the `run()` function of smoldot, which can be imported with | ||
`import { run } from 'smoldot/worker';` (on Deno, it is found in `worker-deno.ts`). | ||
Another optimization that is orthogonal to but is related to running smoldot in a worker consists | ||
in also loading the smoldot bytecode in that worker. The smoldot bytecode weights several | ||
megabytes, and loading it in a worker rather than the main thread makes it possible to load the | ||
UI while smoldot is still initializing. This is especially important when smoldot is included in | ||
an application served over the web. | ||
In order to load the smoldot bytecode in a worker, import `compileBytecode` with | ||
`import { compileBytecode } from 'smoldot/bytecode';` (on Deno: `bytecode-deno.ts`), then call the | ||
function and send the result to the main thread. From the main thread, rather than using the | ||
`start` function imported from `smoldot`, use the `startWithBytecode` function that can be imported | ||
using `import { startWithBytecode } from 'smoldot/no-auto-bytecode';` (on Deno: | ||
`no-auto-bytecode-deno.ts`). The options provided to `startWithBytecode` are the same as the ones | ||
passed to `start`, except for an additional `bytecode` field that must be set to the bytecode | ||
created in the worker. | ||
Here is an example of all this, assuming a browser environment: | ||
```ts | ||
import * as smoldot from 'smoldot/no-auto-bytecode'; | ||
const worker = new Worker(new URL('./worker.js', import.meta.url)); | ||
const bytecode = new Promise((resolve) => { | ||
worker.onmessage = (event) => resolve(event.data); | ||
}); | ||
const { port1, port2 } = new MessageChannel(); | ||
worker.postMessage(port1, [port1]); | ||
const client = smoldot.startWithBytecode({ | ||
bytecode, | ||
portToWorker: port2, | ||
}); | ||
// `worker.ts` | ||
import * as smoldot from '@substrate/smoldot-light/worker'; | ||
import { compileBytecode } from '@substrate/smoldot-light/bytecode'; | ||
compileBytecode().then((bytecode) => postMessage(bytecode)) | ||
onmessage = (msg) => smoldot.run(msg.data); | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
6090766
92
29691
128
1