diff --git a/js-simulation/package-lock.json b/js-simulation/package-lock.json index c49ec7d..ccdcfa7 100644 --- a/js-simulation/package-lock.json +++ b/js-simulation/package-lock.json @@ -23,12 +23,14 @@ "dev": true, "license": "Apache-2.0", "dependencies": { + "@jspm/core": "2.1.0", "archiver": "7.0.1", "axios": "1.7.7", "commander": "12.1.0", "decompress": "4.2.1", "esbuild": "0.24.0", "esbuild-plugin-tsc": "0.4.0", + "import-meta-resolve": "4.1.0", "readline-sync": "1.4.10" }, "bin": { diff --git a/js/cli/.npmignore b/js/cli/.npmignore index 58911bd..40b4068 100644 --- a/js/cli/.npmignore +++ b/js/cli/.npmignore @@ -1,3 +1,4 @@ * !/target/** +!/polyfills/** !/package.json diff --git a/js/cli/package.json b/js/cli/package.json index 4f7cd65..7691959 100644 --- a/js/cli/package.json +++ b/js/cli/package.json @@ -8,12 +8,14 @@ "main": "target/index.js", "types": "target/index.d.ts", "dependencies": { + "@jspm/core": "2.1.0", "archiver": "7.0.1", "axios": "1.7.7", "commander": "12.1.0", "decompress": "4.2.1", "esbuild": "0.24.0", "esbuild-plugin-tsc": "0.4.0", + "import-meta-resolve": "4.1.0", "readline-sync": "1.4.10" }, "devDependencies": { diff --git a/js/cli/polyfills/crypto.js b/js/cli/polyfills/crypto.js new file mode 100644 index 0000000..512482c --- /dev/null +++ b/js/cli/polyfills/crypto.js @@ -0,0 +1,87 @@ +import { Buffer } from "buffer" + +// limit of Crypto.getRandomValues() +// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues +const MAX_BYTES = 65536; + +// Node supports requesting up to this number of bytes +// https://github.com/nodejs/node/blob/master/lib/internal/crypto/random.js#L48 +const MAX_UINT32 = 4294967295; + +const JavaCrypto = Java.type("io.gatling.js.polyfills.Crypto"); + +export const randomBytes = (size, cb) => { + // Node supports requesting up to this number of bytes + // https://github.com/nodejs/node/blob/master/lib/internal/crypto/random.js#L48 + if (size > MAX_UINT32) { + throw new RangeError('requested too many random bytes'); + } + const bytes = Buffer.from(JavaCrypto.randomBytes(size)); + if (typeof cb === 'function') { + return process.nextTick(function () { + cb(null, bytes); + }) + } + return bytes; +}; +export const rng = randomBytes; +export const pseudoRandomBytes = randomBytes; +export const prng = randomBytes; +export const getRandomValues = (values) => { + const byteView = new Uint8Array(values.buffer, values.byteOffset, values.byteLength); + const bytes = randomBytes(byteView.length); + for (let i = 0; i < byteView.length; i++) { + // The range of Math.random() is [0, 1) and the ToUint8 abstract operation rounds down + byteView[i] = bytes[i]; + } + return values; +}; +export const randomUUID = () => JavaCrypto.randomUUID(); + +// export const Cipher = crypto.Cipher; +// export const Cipheriv = crypto.Cipheriv; +// export const Decipher = crypto.Decipher; +// export const Decipheriv = crypto.Decipheriv; +// export const DiffieHellman = crypto.DiffieHellman; +// export const DiffieHellmanGroup = crypto.DiffieHellmanGroup; +// export const Hash = crypto.Hash; +// export const Hmac = crypto.Hmac; +// export const Sign = crypto.Sign; +// export const Verify = crypto.Verify; +// export const constants = crypto.constants; +// export const createCipher = crypto.createCipher; +// export const createCipheriv = crypto.createCipheriv; +// export const createCredentials = crypto.createCredentials; +// export const createDecipher = crypto.createDecipher; +// export const createDecipheriv = crypto.createDecipheriv; +// export const createDiffieHellman = crypto.createDiffieHellman; +// export const createDiffieHellmanGroup = crypto.createDiffieHellmanGroup; +// export const createECDH = crypto.createECDH; +// export const createHash = crypto.createHash; +// export const createHmac = crypto.createHmac; +// export const createSign = crypto.createSign; +// export const createVerify = crypto.createVerify; +// export const getCiphers = crypto.getCiphers; +// export const getDiffieHellman = crypto.getDiffieHellman; +// export const getHashes = crypto.getHashes; +// export const listCiphers = crypto.listCiphers; +// export const pbkdf2 = crypto.pbkdf2; +// export const pbkdf2Sync = crypto.pbkdf2Sync; +// export const privateDecrypt = crypto.privateDecrypt; +// export const privateEncrypt = crypto.privateEncrypt; +// export const publicDecrypt = crypto.publicDecrypt; +// export const publicEncrypt = crypto.publicEncrypt; +// export const randomFill = crypto.randomFill; +// export const randomFillSync = crypto.randomFillSync; + +const crypto = { + randomBytes, + rng, + pseudoRandomBytes, + prng, + getRandomValues, + randomUUID, +}; +crypto.webcrypto = crypto; +globalThis.crypto = crypto; +export default crypto; diff --git a/js/cli/polyfills/global.js b/js/cli/polyfills/global.js new file mode 100644 index 0000000..13e2baf --- /dev/null +++ b/js/cli/polyfills/global.js @@ -0,0 +1,13 @@ +const global = globalThis; +export { global }; + +export { Buffer } from "buffer"; + +// These values are used by some of the JSPM polyfills +export const navigator = { + deviceMemory: 8, // Highest allowed value + hardwareConcurrency: 8, // Fairly common default + language: "en-US", // Most common default +}; + +export * as crypto from "crypto" diff --git a/js/cli/src/bundle.ts b/js/cli/src/bundle/index.ts similarity index 81% rename from js/cli/src/bundle.ts rename to js/cli/src/bundle/index.ts index 406145f..ebfbc3b 100644 --- a/js/cli/src/bundle.ts +++ b/js/cli/src/bundle/index.ts @@ -1,8 +1,9 @@ import * as esbuild from "esbuild"; import esbuildPluginTsc from "esbuild-plugin-tsc"; -import { SimulationFile } from "./simulations"; -import { logger } from "./log"; +import { polyfill } from "./polyfill"; +import { SimulationFile } from "../simulations"; +import { logger } from "../log"; export interface BundleOptions { sourcesFolder: string; @@ -20,12 +21,15 @@ export const bundle = async (options: BundleOptions): Promise => { const contents = options.simulations.map((s) => `export { default as "${s.name}" } from "./${s.path}";`).join("\n"); const plugins = options.typescript ? [esbuildPluginTsc({ force: true })] : []; + plugins.push(polyfill()); await esbuild.build({ stdin: { contents, resolveDir: options.sourcesFolder }, outfile: options.bundleFile, + platform: "neutral", + mainFields: ["main", "module"], bundle: true, minify: false, sourcemap: true, diff --git a/js/cli/src/bundle/polyfill.ts b/js/cli/src/bundle/polyfill.ts new file mode 100644 index 0000000..7005b77 --- /dev/null +++ b/js/cli/src/bundle/polyfill.ts @@ -0,0 +1,91 @@ +import type { Plugin } from "esbuild"; +import { fileURLToPath, pathToFileURL } from "url"; +import { resolve, dirname } from "path"; + +// This is largely inspired by https://github.com/cyco130/esbuild-plugin-polyfill-node + +export const polyfill = (): Plugin => ({ + name: "gatling-js-polyfill", + setup: async (build) => { + // modules + const jspmResolved = await resolveImport(`@jspm/core/nodelibs/fs`); + build.onResolve({ filter: polyfillsFilter }, async ({ path }) => { + const [, , moduleName] = path.match(polyfillsFilter)!; + const resolved = customPolyfills.find((name) => name === moduleName) + ? resolve(dirname(__filename), `../../polyfills/${moduleName}.js`) + : resolve(jspmResolved, `../../browser/${moduleName}.js`); + return { path: resolved }; + }); + + // Globals + build.initialOptions.inject = build.initialOptions.inject || []; + const injectGlobal = (name: string) => + (build.initialOptions.inject as string[]).push(resolve(dirname(__filename), `../../polyfills/${name}.js`)); + injectGlobal("global"); + } +}); + +const customPolyfills = ["crypto"]; + +const jspmPolyfills = ["buffer", "path", "string_decoder"]; + +// Other available jspm-core modules: +// "_stream_duplex" +// "_stream_passthrough" +// "_stream_readable" +// "_stream_transform" +// "_stream_writable" +// "assert" +// "assert/strict" +// "async_hooks" +// "child_process" +// "cluster" +// "console" +// "constants" +// "crypto" +// "dgram" +// "diagnostics_channel" +// "dns" +// "domain" +// "events" +// "fs" +// "fs/promises" +// "http" +// "http2" +// "https" +// "module" +// "net" +// "os" +// "perf_hooks" +// "process" +// "punycode" +// "querystring" +// "readline" +// "repl" +// "stream" +// "sys" +// "timers" +// "timers/promises" +// "tls" +// "tty" +// "url" +// "util" +// "v8" +// "vm" +// "wasi" +// "worker_threads" +// "zlib" + +const polyfillsFilter = new RegExp(`^(node:)?(${jspmPolyfills.concat(customPolyfills).join("|")})$`); + +let importMetaResolve: (specifier: string, parent: string) => string; + +const importMetaUrl = pathToFileURL(__filename).href; + +const resolveImport = async (specifier: string) => { + if (!importMetaResolve) { + importMetaResolve = (await import("import-meta-resolve")).resolve; + } + const resolved = importMetaResolve(specifier, importMetaUrl); + return fileURLToPath(resolved); +}; diff --git a/js/package-lock.json b/js/package-lock.json index dbc35d5..990633e 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -22,12 +22,14 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { + "@jspm/core": "2.1.0", "archiver": "7.0.1", "axios": "1.7.7", "commander": "12.1.0", "decompress": "4.2.1", "esbuild": "0.24.0", "esbuild-plugin-tsc": "0.4.0", + "import-meta-resolve": "4.1.0", "readline-sync": "1.4.10" }, "bin": { @@ -633,6 +635,7 @@ "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "aix" @@ -648,6 +651,7 @@ "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -663,6 +667,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -678,6 +683,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -693,6 +699,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -708,6 +715,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -723,6 +731,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -738,6 +747,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -753,6 +763,7 @@ "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -768,6 +779,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -783,6 +795,7 @@ "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -798,6 +811,7 @@ "cpu": [ "loong64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -813,6 +827,7 @@ "cpu": [ "mips64el" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -828,6 +843,7 @@ "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -843,6 +859,7 @@ "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -858,6 +875,7 @@ "cpu": [ "s390x" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -873,6 +891,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -888,6 +907,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -903,6 +923,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -918,6 +939,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -933,6 +955,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "sunos" @@ -948,6 +971,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -963,6 +987,7 @@ "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -978,6 +1003,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1414,6 +1440,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jspm/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.1.0.tgz", + "integrity": "sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==", + "license": "Apache-2.0" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "license": "MIT", @@ -1610,7 +1642,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", "bin": { @@ -2855,6 +2889,7 @@ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -3321,6 +3356,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "dev": true, diff --git a/jvm/adapter/src/main/scala/io/gatling/js/polyfills/Crypto.scala b/jvm/adapter/src/main/scala/io/gatling/js/polyfills/Crypto.scala new file mode 100644 index 0000000..8481da2 --- /dev/null +++ b/jvm/adapter/src/main/scala/io/gatling/js/polyfills/Crypto.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2011-2024 GatlingCorp (https://gatling.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.gatling.js.polyfills + +import java.security.SecureRandom +import java.util.UUID + +object Crypto { + def randomBytes(size: Int): Array[Byte] = { + val bytes = new Array[Byte](size) + SecureRandom.getInstanceStrong.nextBytes(bytes) + bytes + } + + def randomUUID(): String = + UUID.randomUUID().toString +} diff --git a/ts-simulation/package-lock.json b/ts-simulation/package-lock.json index 6ed5587..efcb6ce 100644 --- a/ts-simulation/package-lock.json +++ b/ts-simulation/package-lock.json @@ -24,12 +24,14 @@ "dev": true, "license": "Apache-2.0", "dependencies": { + "@jspm/core": "2.1.0", "archiver": "7.0.1", "axios": "1.7.7", "commander": "12.1.0", "decompress": "4.2.1", "esbuild": "0.24.0", "esbuild-plugin-tsc": "0.4.0", + "import-meta-resolve": "4.1.0", "readline-sync": "1.4.10" }, "bin": {