From b38dd9483b598c77fa1b16501c3acb883f7256e0 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 23 May 2024 14:52:36 -0700 Subject: [PATCH] Port remaining StarlingMonkey builtins (#790) --- .../js-compute/fixtures/app/src/cache-core.js | 40 +- .../fixtures/app/src/config-store.js | 4 +- .../js-compute/fixtures/app/src/device.js | 2 +- .../js-compute/fixtures/app/src/dictionary.js | 2 +- .../fixtures/app/src/edge-rate-limiter.js | 12 +- .../js-compute/fixtures/app/src/fanout.js | 2 +- .../js-compute/fixtures/app/src/logger.js | 4 +- .../js-compute/fixtures/app/src/react-byob.js | 9 +- .../fixtures/app/src/secret-store.js | 4 +- .../fixtures/app/tests-starlingmonkey.json | 288 +++- runtime/StarlingMonkey | 2 +- runtime/fastly/CMakeLists.txt | 8 + runtime/fastly/builtins/backend.cpp | 2 +- runtime/fastly/builtins/backend.h | 4 +- runtime/fastly/builtins/body.cpp | 317 +++++ runtime/fastly/builtins/body.h | 35 + runtime/fastly/builtins/cache-core.cpp | 1224 +++++++++++++++++ runtime/fastly/builtins/cache-core.h | 167 +++ runtime/fastly/builtins/cache-override.cpp | 1 + runtime/fastly/builtins/cache-override.h | 4 +- runtime/fastly/builtins/cache-simple.cpp | 2 + runtime/fastly/builtins/cache-simple.h | 11 +- runtime/fastly/builtins/config-store.cpp | 148 ++ runtime/fastly/builtins/config-store.h | 32 + runtime/fastly/builtins/device.cpp | 582 ++++++++ runtime/fastly/builtins/device.h | 60 + runtime/fastly/builtins/dictionary.cpp | 161 +++ runtime/fastly/builtins/dictionary.h | 29 + runtime/fastly/builtins/edge-rate-limiter.cpp | 499 +++++++ runtime/fastly/builtins/edge-rate-limiter.h | 66 + runtime/fastly/builtins/fastly.cpp | 382 +++-- runtime/fastly/builtins/fastly.h | 20 +- runtime/fastly/builtins/fetch-event.h | 6 +- runtime/fastly/builtins/fetch/fetch.cpp | 6 +- runtime/fastly/builtins/fetch/headers.h | 4 +- .../fastly/builtins/fetch/request-response.h | 4 +- runtime/fastly/builtins/kv-store.cpp | 3 +- runtime/fastly/builtins/kv-store.h | 15 +- runtime/fastly/builtins/logger.cpp | 94 ++ runtime/fastly/builtins/logger.h | 29 + runtime/fastly/builtins/secret-store.cpp | 238 ++++ runtime/fastly/builtins/secret-store.h | 47 + src/compileApplicationToWasm.js | 48 +- 43 files changed, 4284 insertions(+), 333 deletions(-) create mode 100644 runtime/fastly/builtins/body.cpp create mode 100644 runtime/fastly/builtins/body.h create mode 100644 runtime/fastly/builtins/cache-core.cpp create mode 100644 runtime/fastly/builtins/cache-core.h create mode 100644 runtime/fastly/builtins/config-store.cpp create mode 100644 runtime/fastly/builtins/config-store.h create mode 100644 runtime/fastly/builtins/device.cpp create mode 100644 runtime/fastly/builtins/device.h create mode 100644 runtime/fastly/builtins/dictionary.cpp create mode 100644 runtime/fastly/builtins/dictionary.h create mode 100644 runtime/fastly/builtins/edge-rate-limiter.cpp create mode 100644 runtime/fastly/builtins/edge-rate-limiter.h create mode 100644 runtime/fastly/builtins/logger.cpp create mode 100644 runtime/fastly/builtins/logger.h create mode 100644 runtime/fastly/builtins/secret-store.cpp create mode 100644 runtime/fastly/builtins/secret-store.h diff --git a/integration-tests/js-compute/fixtures/app/src/cache-core.js b/integration-tests/js-compute/fixtures/app/src/cache-core.js index 1a5e3141d7..4a7793b172 100644 --- a/integration-tests/js-compute/fixtures/app/src/cache-core.js +++ b/integration-tests/js-compute/fixtures/app/src/cache-core.js @@ -3,12 +3,8 @@ import { pass, assert, assertDoesNotThrow, assertThrows, sleep, streamToString, assertResolves } from "./assertions.js"; import { routes } from "./routes.js"; -import { CoreCache, CacheEntry } from 'fastly:cache'; -import * as fastlyCache from 'fastly:cache'; -import * as fastlyBody from "fastly:body"; - -const { CacheState, TransactionCacheEntry } = fastlyCache; -const { FastlyBody } = fastlyBody; +import { CoreCache, CacheEntry, CacheState, TransactionCacheEntry } from 'fastly:cache'; +import { FastlyBody } from "fastly:body"; let error; @@ -485,7 +481,7 @@ let error; routes.set("/core-cache/lookup/called-as-constructor", () => { let error = assertThrows(() => { new CoreCache.lookup('1') - }, TypeError, `CoreCache.lookup is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -631,7 +627,7 @@ let error; routes.set("/core-cache/insert/called-as-constructor", () => { let error = assertThrows(() => { new CoreCache.insert('1', { maxAge: 1 }) - }, TypeError, `CoreCache.insert is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1033,7 +1029,7 @@ let error; routes.set("/core-cache/transactionLookup/called-as-constructor", () => { let error = assertThrows(() => { new CoreCache.transactionLookup('1') - }, TypeError, `CoreCache.transactionLookup is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1579,7 +1575,7 @@ let error; routes.set("/cache-entry/close/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.close() - }, TypeError, `CacheEntry.prototype.close is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1609,7 +1605,7 @@ let error; routes.set("/cache-entry/state/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.state() - }, TypeError, `CacheEntry.prototype.state is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1641,7 +1637,7 @@ let error; routes.set("/cache-entry/userMetadata/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.userMetadata() - }, TypeError, `CacheEntry.prototype.userMetadata is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1694,7 +1690,7 @@ let error; routes.set("/cache-entry/body/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.body() - }, TypeError, `CacheEntry.prototype.body is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1872,7 +1868,7 @@ let error; routes.set("/cache-entry/length/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.length() - }, TypeError, `CacheEntry.prototype.length is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1907,7 +1903,7 @@ let error; routes.set("/cache-entry/maxAge/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.maxAge() - }, TypeError, `CacheEntry.prototype.maxAge is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1937,7 +1933,7 @@ let error; routes.set("/cache-entry/staleWhileRevalidate/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.staleWhileRevalidate() - }, TypeError, `CacheEntry.prototype.staleWhileRevalidate is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -1969,7 +1965,7 @@ let error; routes.set("/cache-entry/age/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.age() - }, TypeError, `CacheEntry.prototype.age is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -2005,7 +2001,7 @@ let error; routes.set("/cache-entry/hits/called-as-constructor", () => { let error = assertThrows(() => { new CacheEntry.prototype.hits() - }, TypeError, `CacheEntry.prototype.hits is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -2264,7 +2260,7 @@ let error; let error = assertThrows(() => { let entry = CoreCache.transactionLookup('1') new entry.insert({maxAge: 1}) - }, TypeError, `entry.insert is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -2526,7 +2522,7 @@ let error; let error = assertThrows(() => { let entry = CoreCache.transactionLookup(path) new entry.insertAndStreamBack({maxAge: 1}) - }, TypeError, `entry.insertAndStreamBack is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -2834,7 +2830,7 @@ let error; let error = assertThrows(() => { let entry = CoreCache.transactionLookup(path) new entry.update({maxAge: 1}) - }, TypeError, `entry.update is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); @@ -3161,7 +3157,7 @@ let error; let error = assertThrows(() => { let entry = CoreCache.transactionLookup(path) new entry.cancel() - }, TypeError, `entry.cancel is not a constructor`) + }, TypeError) if (error) { return error } return pass("ok") }); diff --git a/integration-tests/js-compute/fixtures/app/src/config-store.js b/integration-tests/js-compute/fixtures/app/src/config-store.js index 1d3364477f..11cc088384 100644 --- a/integration-tests/js-compute/fixtures/app/src/config-store.js +++ b/integration-tests/js-compute/fixtures/app/src/config-store.js @@ -1,10 +1,8 @@ /* eslint-env serviceworker */ import { pass, assert } from "./assertions.js"; -import * as configStore from 'fastly:config-store' +import { ConfigStore } from 'fastly:config-store' import { routes } from "./routes.js"; -const { ConfigStore } = configStore; - routes.set("/config-store", () => { let config = new ConfigStore("testconfig"); let twitterValue = config.get("twitter"); diff --git a/integration-tests/js-compute/fixtures/app/src/device.js b/integration-tests/js-compute/fixtures/app/src/device.js index 6cf88667d6..7580d565e4 100644 --- a/integration-tests/js-compute/fixtures/app/src/device.js +++ b/integration-tests/js-compute/fixtures/app/src/device.js @@ -169,7 +169,7 @@ routes.set("/device/interface", () => { routes.set("/device/lookup/called-as-constructor", () => { let error = assertThrows(() => { new Device.lookup('1') - }, TypeError, `Device.lookup is not a constructor`) + }, TypeError) if (error) { return error } return pass('ok') }); diff --git a/integration-tests/js-compute/fixtures/app/src/dictionary.js b/integration-tests/js-compute/fixtures/app/src/dictionary.js index fd18fd7d46..a433fd813b 100644 --- a/integration-tests/js-compute/fixtures/app/src/dictionary.js +++ b/integration-tests/js-compute/fixtures/app/src/dictionary.js @@ -93,7 +93,7 @@ import { routes } from "./routes.js"; routes.set("/dictionary/get/called-as-constructor", () => { let error = assertThrows(() => { new Dictionary.prototype.get('1') - }, TypeError, `Dictionary.prototype.get is not a constructor`) + }, TypeError) if (error) { return error } return pass() }); diff --git a/integration-tests/js-compute/fixtures/app/src/edge-rate-limiter.js b/integration-tests/js-compute/fixtures/app/src/edge-rate-limiter.js index 38dc1f70e4..55f33e6556 100644 --- a/integration-tests/js-compute/fixtures/app/src/edge-rate-limiter.js +++ b/integration-tests/js-compute/fixtures/app/src/edge-rate-limiter.js @@ -256,7 +256,7 @@ let error; routes.set("/rate-counter/increment/called-as-constructor", () => { error = assertThrows(() => { new RateCounter.prototype.increment('entry', 1) - }, Error, `RateCounter.prototype.increment is not a constructor`) + }, Error) if (error) { return error } return pass('ok') }); @@ -344,7 +344,7 @@ let error; routes.set("/rate-counter/lookupRate/called-as-constructor", () => { error = assertThrows(() => { new RateCounter.prototype.lookupRate('entry', 1) - }, Error, `RateCounter.prototype.lookupRate is not a constructor`) + }, Error) if (error) { return error } return pass('ok') }); @@ -438,7 +438,7 @@ let error; routes.set("/rate-counter/lookupCount/called-as-constructor", () => { error = assertThrows(() => { new RateCounter.prototype.lookupCount('entry', 1) - }, Error, `RateCounter.prototype.lookupCount is not a constructor`) + }, Error) if (error) { return error } return pass('ok') }); @@ -750,7 +750,7 @@ let error; routes.set("/penalty-box/has/called-as-constructor", () => { error = assertThrows(() => { new PenaltyBox.prototype.has('entry') - }, Error, `PenaltyBox.prototype.has is not a constructor`) + }, Error) if (error) { return error } return pass('ok') }); @@ -806,7 +806,7 @@ let error; routes.set("/penalty-box/add/called-as-constructor", () => { error = assertThrows(() => { new PenaltyBox.prototype.add('entry', 1) - }, Error, `PenaltyBox.prototype.add is not a constructor`) + }, Error) if (error) { return error } return pass('ok') }); @@ -1064,7 +1064,7 @@ let error; routes.set("/edge-rate-limiter/checkRate/called-as-constructor", () => { error = assertThrows(() => { new EdgeRateLimiter.prototype.checkRate('entry') - }, Error, `EdgeRateLimiter.prototype.checkRate is not a constructor`) + }, Error) if (error) { return error } return pass('ok') }); diff --git a/integration-tests/js-compute/fixtures/app/src/fanout.js b/integration-tests/js-compute/fixtures/app/src/fanout.js index 403f21448f..fd8c9caee8 100644 --- a/integration-tests/js-compute/fixtures/app/src/fanout.js +++ b/integration-tests/js-compute/fixtures/app/src/fanout.js @@ -26,7 +26,7 @@ routes.set("/createFanoutHandoff", () => { error = assert(result instanceof Response, true, 'result instanceof Response'); if (error) { return error; } - error = assertThrows(() => new createFanoutHandoff(new Request('.'), 'hello'), TypeError, `createFanoutHandoff is not a constructor`) + error = assertThrows(() => new createFanoutHandoff(new Request('.'), 'hello'), TypeError) if (error) { return error } error = assertDoesNotThrow(() => { diff --git a/integration-tests/js-compute/fixtures/app/src/logger.js b/integration-tests/js-compute/fixtures/app/src/logger.js index c6caa46e87..f27d3882ed 100644 --- a/integration-tests/js-compute/fixtures/app/src/logger.js +++ b/integration-tests/js-compute/fixtures/app/src/logger.js @@ -1,8 +1,6 @@ -import * as logger from "fastly:logger"; +import { Logger } from "fastly:logger"; import { routes, isRunningLocally } from "./routes"; -const { Logger } = logger; - routes.set("/logger", () => { if (isRunningLocally()) { let logger = new Logger("ComputeLog"); diff --git a/integration-tests/js-compute/fixtures/app/src/react-byob.js b/integration-tests/js-compute/fixtures/app/src/react-byob.js index 3927c7d0c0..9db5f20de1 100644 --- a/integration-tests/js-compute/fixtures/app/src/react-byob.js +++ b/integration-tests/js-compute/fixtures/app/src/react-byob.js @@ -2106,11 +2106,12 @@ routes.set("/react-byob", async () => { }); function App() { - return q("html", { - children: [q("title", { + const qq = typeof jsx !== 'undefined' ? jsx : q; + return qq("html", { + children: [qq("title", { children: "My app" - }), q("body", { - children: q("h1", { + }), qq("body", { + children: qq("h1", { children: "App" }) })] diff --git a/integration-tests/js-compute/fixtures/app/src/secret-store.js b/integration-tests/js-compute/fixtures/app/src/secret-store.js index 86f93e583a..340a52468d 100644 --- a/integration-tests/js-compute/fixtures/app/src/secret-store.js +++ b/integration-tests/js-compute/fixtures/app/src/secret-store.js @@ -1,11 +1,9 @@ /* eslint-env serviceworker */ -import * as secretStore from 'fastly:secret-store' +import { SecretStore, SecretStoreEntry } from 'fastly:secret-store' import { pass, assert, assertThrows, assertRejects } from "./assertions.js"; import { routes } from "./routes.js"; import fc from './fast-check.js'; -const { SecretStore, SecretStoreEntry } = secretStore; - // SecretStore { routes.set("/secret-store/exposed-as-global", () => { diff --git a/integration-tests/js-compute/fixtures/app/tests-starlingmonkey.json b/integration-tests/js-compute/fixtures/app/tests-starlingmonkey.json index 80ec151990..b80445a52e 100644 --- a/integration-tests/js-compute/fixtures/app/tests-starlingmonkey.json +++ b/integration-tests/js-compute/fixtures/app/tests-starlingmonkey.json @@ -8,6 +8,7 @@ "GET /cache-override/constructor/invalid-mode", "GET /cache-override/constructor/valid-mode", "GET /cache-override/fetch/mode-none", + "GET /cache-override/fetch/mode-pass", "GET /simple-cache/interface", "GET /simple-store/constructor/called-as-regular-function", "GET /simple-cache/constructor/throws", @@ -97,6 +98,7 @@ "GET /client/tlsClientCertificate", "GET /client/tlsCipherOpensslName", "GET /client/tlsProtocol", + "GET /config-store", "GET /console", "GET /crypto", "GET /crypto.subtle", @@ -190,6 +192,7 @@ "GET /crypto.subtle.verify/fourth-parameter-invalid-format", "GET /crypto.subtle.verify/incorrect-signature-jwk", "GET /crypto.subtle.verify/incorrect-signature-hmac", + "GET /crypto.subtle.verify/correct-signature-jwk-ecdsa", "GET /crypto.subtle.verify/correct-signature-jwk-rsa", "GET /crypto.subtle.verify/correct-signature-hmac", "GET /backend/timeout", @@ -259,7 +262,24 @@ "GET /backend/health/parameter-invalid", "GET /backend/health/happy-path-backend-exists", "GET /backend/health/happy-path-backend-does-not-exist", + "GET /dictionary/exposed-as-global", + "GET /dictionary/interface", + "GET /dictionary/constructor/called-as-regular-function", + "GET /dictionary/constructor/parameter-calls-7.1.17-ToString", + "GET /dictionary/constructor/empty-parameter", + "GET /dictionary/constructor/found", + "GET /dictionary/constructor/invalid-name", + "GET /dictionary/get/called-as-constructor", + "GET /dictionary/get/called-unbound", + "GET /dictionary/get/key-parameter-calls-7.1.17-ToString", + "GET /dictionary/get/key-parameter-not-supplied", + "GET /dictionary/get/key-parameter-empty-string", + "GET /dictionary/get/key-parameter-255-character-string", + "GET /dictionary/get/key-parameter-256-character-string", + "GET /dictionary/get/key-does-not-exist-returns-null", + "GET /dictionary/get/key-exists", "GET /env", + "GET /createFanoutHandoff", "GET /fastly/now", "GET /fastly/version", "GET /fastly/getgeolocationforipaddress/interface", @@ -273,6 +293,7 @@ "GET /fastly/getgeolocationforipaddress/parameter-expanded-ipv6-string", "GET /fastly/getgeolocationforipaddress/called-unbound", "GET /fastly:geolocation", + "GET /includeBytes", "GET /kv-store/exposed-as-global", "GET /kv-store/interface", "GET /kv-store/constructor/called-as-regular-function", @@ -348,11 +369,15 @@ "GET /kv-store-entry/json/invalid", "GET /kv-store-entry/arrayBuffer/valid", "GET /kv-store-entry/bodyUsed", + "GET /logger", "GET /missing-backend", "GET /multiple-set-cookie/response-init", "GET /multiple-set-cookie/response-direct", "GET /multiple-set-cookie/downstream", "GET /Performance/interface", + "GET /globalThis.performance", + "GET /globalThis.performance/now", + "GET /globalThis.performance/timeOrigin", "GET /request/constructor/fastly/decompressGzip/true", "GET /request/constructor/fastly/decompressGzip/false", "GET /fetch/requestinit/fastly/decompressGzip/true", @@ -427,5 +452,266 @@ "GET /override-content-length/response/init/response-instance/false", "GET /override-content-length/response/method/false", "GET /override-content-length/response/method/true", - "GET /headers/non-ascii-latin1-field-value" + "GET /headers/non-ascii-latin1-field-value", + "GET /FastlyBody/interface", + "GET /core-cache/interface", + "GET /core-cache/constructor/called-as-regular-function", + "GET /core-cache/constructor/throws", + "GET /core-cache/lookup/called-as-constructor", + "GET /core-cache/lookup/key-parameter-calls-7.1.17-ToString", + "GET /core-cache/lookup/key-parameter-not-supplied", + "GET /core-cache/lookup/key-parameter-empty-string", + "GET /core-cache/lookup/key-parameter-8135-character-string", + "GET /core-cache/lookup/key-parameter-8136-character-string", + "GET /core-cache/lookup/key-does-not-exist-returns-null", + "GET /core-cache/lookup/key-exists", + "GET /core-cache/lookup/options-parameter-wrong-type", + "GET /core-cache/lookup/options-parameter-headers-field-wrong-type", + "GET /core-cache/lookup/options-parameter-headers-field-undefined", + "GET /core-cache/lookup/options-parameter-headers-field-valid-sequence", + "GET /core-cache/lookup/options-parameter-headers-field-valid-record", + "GET /core-cache/lookup/options-parameter-headers-field-valid-Headers-instance", + "GET /core-cache/insert/called-as-constructor", + "GET /core-cache/insert/key-parameter-calls-7.1.17-ToString", + "GET /core-cache/insert/key-parameter-not-supplied", + "GET /core-cache/insert/key-parameter-empty-string", + "GET /core-cache/insert/key-parameter-8135-character-string", + "GET /core-cache/insert/key-parameter-8136-character-string", + "GET /core-cache/insert/options-parameter-wrong-type", + "GET /core-cache/insert/options-parameter-headers-field-wrong-type", + "GET /core-cache/insert/options-parameter-headers-field-undefined", + "GET /core-cache/insert/options-parameter-headers-field-valid-sequence", + "GET /core-cache/insert/options-parameter-headers-field-valid-record", + "GET /core-cache/insert/options-parameter-headers-field-valid-Headers-instance", + "GET /core-cache/insert/options-parameter-maxAge-field-valid-record", + "GET /core-cache/insert/options-parameter-maxAge-field-NaN", + "GET /core-cache/insert/options-parameter-maxAge-field-postitive-infinity", + "GET /core-cache/insert/options-parameter-maxAge-field-negative-infinity", + "GET /core-cache/insert/options-parameter-maxAge-field-negative-number", + "GET /core-cache/insert/options-parameter-initialAge-field-valid-record", + "GET /core-cache/insert/options-parameter-initialAge-field-NaN", + "GET /core-cache/insert/options-parameter-initialAge-field-postitive-infinity", + "GET /core-cache/insert/options-parameter-initialAge-field-negative-infinity", + "GET /core-cache/insert/options-parameter-initialAge-field-negative-number", + "GET /core-cache/insert/options-parameter-staleWhileRevalidate-field-valid-record", + "GET /core-cache/insert/options-parameter-staleWhileRevalidate-field-NaN", + "GET /core-cache/insert/options-parameter-staleWhileRevalidate-field-postitive-infinity", + "GET /core-cache/insert/options-parameter-staleWhileRevalidate-field-negative-infinity", + "GET /core-cache/insert/options-parameter-staleWhileRevalidate-field-negative-number", + "GET /core-cache/insert/options-parameter-length-field-valid-record", + "GET /core-cache/insert/options-parameter-length-field-NaN", + "GET /core-cache/insert/options-parameter-length-field-postitive-infinity", + "GET /core-cache/insert/options-parameter-length-field-negative-infinity", + "GET /core-cache/insert/options-parameter-length-field-negative-number", + "GET /core-cache/insert/options-parameter-sensitive-field", + "GET /core-cache/insert/options-parameter-vary-field", + "GET /core-cache/insert/options-parameter-userMetadata-field/arraybuffer/empty", + "GET /core-cache/insert/options-parameter-userMetadata-field/arraybuffer/not-empty", + "GET /core-cache/insert/options-parameter-userMetadata-field/URLSearchParams", + "GET /core-cache/insert/options-parameter-userMetadata-field/string", + "GET /core-cache/transactionLookup/called-as-constructor", + "GET /core-cache/transactionLookup/key-parameter-calls-7.1.17-ToString", + "GET /core-cache/transactionLookup/key-parameter-not-supplied", + "GET /core-cache/transactionLookup/key-parameter-empty-string", + "GET /core-cache/transactionLookup/key-parameter-8135-character-string", + "GET /core-cache/transactionLookup/key-parameter-8136-character-string", + "GET /core-cache/transactionLookup/key-does-not-exist", + "GET /core-cache/transactionLookup/key-exists", + "GET /core-cache/transactionLookup/options-parameter-wrong-type", + "GET /core-cache/transactionLookup/options-parameter-headers-field-wrong-type", + "GET /core-cache/transactionLookup/options-parameter-headers-field-undefined", + "GET /core-cache/transactionLookup/options-parameter-headers-field-valid-sequence", + "GET /core-cache/transactionLookup/options-parameter-headers-field-valid-record", + "GET /core-cache/transactionLookup/options-parameter-headers-field-valid-Headers-instance", + "GET /cache-entry/interface", + "GET /cache-entry/constructor/called-as-regular-function", + "GET /cache-entry/constructor/throws", + "GET /cache-entry/close/called-as-constructor", + "GET /cache-entry/close/called-unbound", + "GET /cache-entry/close/called-on-instance", + "GET /cache-entry/state/called-as-constructor", + "GET /cache-entry/state/called-unbound", + "GET /cache-entry/state/called-on-instance", + "GET /cache-entry/userMetadata/called-as-constructor", + "GET /cache-entry/userMetadata/called-unbound", + "GET /cache-entry/userMetadata/called-on-instance", + "GET /cache-entry/userMetadata/basic", + "GET /cache-entry/body/called-as-constructor", + "GET /cache-entry/body/called-unbound", + "GET /cache-entry/body/options-start-negative", + "GET /cache-entry/body/options-start-NaN", + "GET /cache-entry/body/options-start-Infinity", + "GET /cache-entry/body/options-end-negative", + "GET /cache-entry/body/options-end-NaN", + "GET /cache-entry/body/options-end-Infinity", + "GET /cache-entry/length/called-as-constructor", + "GET /cache-entry/length/called-unbound", + "GET /cache-entry/length/called-on-instance", + "GET /cache-entry/maxAge/called-as-constructor", + "GET /cache-entry/maxAge/called-unbound", + "GET /cache-entry/maxAge/called-on-instance", + "GET /cache-entry/staleWhileRevalidate/called-as-constructor", + "GET /cache-entry/staleWhileRevalidate/called-unbound", + "GET /cache-entry/staleWhileRevalidate/called-on-instance", + "GET /cache-entry/age/called-as-constructor", + "GET /cache-entry/age/called-unbound", + "GET /cache-entry/hits/called-as-constructor", + "GET /cache-entry/hits/called-unbound", + "GET /cache-entry/hits/called-on-instance", + "GET /transaction-cache-entry/interface", + "GET /transaction-cache-entry/insert/called-as-constructor", + "GET /transaction-cache-entry/insert/entry-parameter-not-supplied", + "GET /transaction-cache-entry/insert/options-parameter-maxAge-field-valid-record", + "GET /transaction-cache-entry/insert/options-parameter-maxAge-field-NaN", + "GET /transaction-cache-entry/insert/options-parameter-maxAge-field-postitive-infinity", + "GET /transaction-cache-entry/insert/options-parameter-maxAge-field-negative-infinity", + "GET /transaction-cache-entry/insert/options-parameter-maxAge-field-negative-number", + "GET /transaction-cache-entry/insert/options-parameter-initialAge-field-valid-record", + "GET /transaction-cache-entry/insert/options-parameter-initialAge-field-NaN", + "GET /transaction-cache-entry/insert/options-parameter-initialAge-field-postitive-infinity", + "GET /transaction-cache-entry/insert/options-parameter-initialAge-field-negative-infinity", + "GET /transaction-cache-entry/insert/options-parameter-initialAge-field-negative-number", + "GET /transaction-cache-entry/insert/options-parameter-staleWhileRevalidate-field-valid-record", + "GET /transaction-cache-entry/insert/options-parameter-staleWhileRevalidate-field-NaN", + "GET /transaction-cache-entry/insert/options-parameter-staleWhileRevalidate-field-postitive-infinity", + "GET /transaction-cache-entry/insert/options-parameter-staleWhileRevalidate-field-negative-infinity", + "GET /transaction-cache-entry/insert/options-parameter-staleWhileRevalidate-field-negative-number", + "GET /transaction-cache-entry/insert/options-parameter-length-field-valid-record", + "GET /transaction-cache-entry/insert/options-parameter-length-field-NaN", + "GET /transaction-cache-entry/insert/options-parameter-length-field-postitive-infinity", + "GET /transaction-cache-entry/insert/options-parameter-length-field-negative-infinity", + "GET /transaction-cache-entry/insert/options-parameter-length-field-negative-number", + "GET /transaction-cache-entry/insert/options-parameter-sensitive-field", + "GET /transaction-cache-entry/insert/options-parameter-vary-field", + "GET /transaction-cache-entry/insertAndStreamBack/called-as-constructor", + "GET /transaction-cache-entry/insertAndStreamBack/entry-parameter-not-supplied", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-maxAge-field-valid-record", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-maxAge-field-NaN", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-maxAge-field-postitive-infinity", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-maxAge-field-negative-infinity", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-maxAge-field-negative-number", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-initialAge-field-valid-record", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-initialAge-field-NaN", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-initialAge-field-postitive-infinity", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-initialAge-field-negative-infinity", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-initialAge-field-negative-number", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-staleWhileRevalidate-field-valid-record", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-staleWhileRevalidate-field-NaN", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-staleWhileRevalidate-field-postitive-infinity", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-staleWhileRevalidate-field-negative-infinity", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-staleWhileRevalidate-field-negative-number", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-length-field-valid-record", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-length-field-NaN", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-length-field-postitive-infinity", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-length-field-negative-infinity", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-length-field-negative-number", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-sensitive-field", + "GET /transaction-cache-entry/insertAndStreamBack/options-parameter-vary-field", + "GET /transaction-cache-entry/update/called-as-constructor", + "GET /transaction-cache-entry/update/entry-parameter-not-supplied", + "GET /transaction-cache-entry/update/options-parameter-maxAge-field-valid-record", + "GET /transaction-cache-entry/update/options-parameter-maxAge-field-NaN", + "GET /transaction-cache-entry/update/options-parameter-maxAge-field-postitive-infinity", + "GET /transaction-cache-entry/update/options-parameter-maxAge-field-negative-infinity", + "GET /transaction-cache-entry/update/options-parameter-maxAge-field-negative-number", + "GET /transaction-cache-entry/update/options-parameter-initialAge-field-valid-record", + "GET /transaction-cache-entry/update/options-parameter-initialAge-field-NaN", + "GET /transaction-cache-entry/update/options-parameter-initialAge-field-postitive-infinity", + "GET /transaction-cache-entry/update/options-parameter-initialAge-field-negative-infinity", + "GET /transaction-cache-entry/update/options-parameter-initialAge-field-negative-number", + "GET /transaction-cache-entry/update/options-parameter-staleWhileRevalidate-field-valid-record", + "GET /transaction-cache-entry/update/options-parameter-staleWhileRevalidate-field-NaN", + "GET /transaction-cache-entry/update/options-parameter-staleWhileRevalidate-field-postitive-infinity", + "GET /transaction-cache-entry/update/options-parameter-staleWhileRevalidate-field-negative-infinity", + "GET /transaction-cache-entry/update/options-parameter-staleWhileRevalidate-field-negative-number", + "GET /transaction-cache-entry/update/options-parameter-length-field-valid-record", + "GET /transaction-cache-entry/update/options-parameter-length-field-NaN", + "GET /transaction-cache-entry/update/options-parameter-length-field-postitive-infinity", + "GET /transaction-cache-entry/update/options-parameter-length-field-negative-infinity", + "GET /transaction-cache-entry/update/options-parameter-length-field-negative-number", + "GET /transaction-cache-entry/update/write-to-writer-and-read-from-reader", + "GET /transaction-cache-entry/update/options-parameter-vary-field", + "GET /transaction-cache-entry/update/options-parameter-userMetadata-field", + "GET /transaction-cache-entry/cancel/called-as-constructor", + "GET /transaction-cache-entry/cancel/called-once", + "GET /transaction-cache-entry/cancel/makes-entry-cancelled", + "GET /transaction-cache-entry/cancel/called-twice-throws", + "GET /rate-counter/interface", + "GET /rate-counter/constructor/called-as-regular-function", + "GET /rate-counter/constructor/called-as-constructor-no-arguments", + "GET /rate-counter/constructor/name-parameter-calls-7.1.17-ToString", + "GET /rate-counter/constructor/happy-path", + "GET /rate-counter/increment/called-as-constructor", + "GET /rate-counter/increment/entry-parameter-calls-7.1.17-ToString", + "GET /rate-counter/increment/entry-parameter-not-supplied", + "GET /rate-counter/increment/delta-parameter-not-supplied", + "GET /rate-counter/increment/delta-parameter-negative", + "GET /rate-counter/increment/delta-parameter-infinity", + "GET /rate-counter/increment/delta-parameter-NaN", + "GET /rate-counter/increment/returns-undefined", + "GET /rate-counter/lookupRate/called-as-constructor", + "GET /rate-counter/lookupRate/entry-parameter-calls-7.1.17-ToString", + "GET /rate-counter/lookupRate/entry-parameter-not-supplied", + "GET /rate-counter/lookupRate/window-parameter-not-supplied", + "GET /rate-counter/lookupRate/window-parameter-negative", + "GET /rate-counter/lookupRate/window-parameter-infinity", + "GET /rate-counter/lookupRate/window-parameter-NaN", + "GET /rate-counter/lookupRate/returns-number", + "GET /rate-counter/lookupCount/called-as-constructor", + "GET /rate-counter/lookupCount/entry-parameter-calls-7.1.17-ToString", + "GET /rate-counter/lookupCount/entry-parameter-not-supplied", + "GET /rate-counter/lookupCount/duration-parameter-not-supplied", + "GET /rate-counter/lookupCount/duration-parameter-negative", + "GET /rate-counter/lookupCount/duration-parameter-infinity", + "GET /rate-counter/lookupCount/duration-parameter-NaN", + "GET /rate-counter/lookupCount/returns-number", + "GET /penalty-box/interface", + "GET /penalty-box/constructor/called-as-regular-function", + "GET /penalty-box/constructor/called-as-constructor-no-arguments", + "GET /penalty-box/constructor/name-parameter-calls-7.1.17-ToString", + "GET /penalty-box/constructor/happy-path", + "GET /penalty-box/has/called-as-constructor", + "GET /penalty-box/has/entry-parameter-calls-7.1.17-ToString", + "GET /penalty-box/has/entry-parameter-not-supplied", + "GET /penalty-box/has/returns-boolean", + "GET /penalty-box/add/called-as-constructor", + "GET /penalty-box/add/entry-parameter-calls-7.1.17-ToString", + "GET /penalty-box/add/entry-parameter-not-supplied", + "GET /penalty-box/add/timeToLive-parameter-not-supplied", + "GET /penalty-box/add/timeToLive-parameter-negative", + "GET /penalty-box/add/timeToLive-parameter-infinity", + "GET /penalty-box/add/timeToLive-parameter-NaN", + "GET /penalty-box/add/returns-undefined", + "GET /edge-rate-limiter/interface", + "GET /edge-rate-limiter/constructor/called-as-regular-function", + "GET /edge-rate-limiter/constructor/called-as-constructor-no-arguments", + "GET /edge-rate-limiter/constructor/rate-counter-not-instance-of-rateCounter", + "GET /edge-rate-limiter/constructor/penalty-box-not-instance-of-penaltyBox", + "GET /edge-rate-limiter/constructor/happy-path", + "GET /edge-rate-limiter/checkRate/called-as-constructor", + "GET /edge-rate-limiter/checkRate/entry-parameter-calls-7.1.17-ToString", + "GET /edge-rate-limiter/checkRate/entry-parameter-not-supplied", + "GET /edge-rate-limiter/checkRate/delta-parameter-negative", + "GET /edge-rate-limiter/checkRate/delta-parameter-infinity", + "GET /edge-rate-limiter/checkRate/delta-parameter-NaN", + "GET /edge-rate-limiter/checkRate/window-parameter-negative", + "GET /edge-rate-limiter/checkRate/window-parameter-infinity", + "GET /edge-rate-limiter/checkRate/window-parameter-NaN", + "GET /edge-rate-limiter/checkRate/limit-parameter-negative", + "GET /edge-rate-limiter/checkRate/limit-parameter-infinity", + "GET /edge-rate-limiter/checkRate/limit-parameter-NaN", + "GET /edge-rate-limiter/checkRate/timeToLive-parameter-negative", + "GET /edge-rate-limiter/checkRate/timeToLive-parameter-infinity", + "GET /edge-rate-limiter/checkRate/timeToLive-parameter-NaN", + "GET /edge-rate-limiter/checkRate/returns-boolean", + "GET /device/interface", + "GET /device/constructor/called-as-regular-function", + "GET /device/constructor/throws", + "GET /device/lookup/called-as-constructor", + "GET /device/lookup/useragent-parameter-calls-7.1.17-ToString", + "GET /device/lookup/useragent-parameter-not-supplied", + "GET /device/lookup/useragent-parameter-empty-string", + "GET /device/lookup/useragent-does-not-exist-returns-null", + "GET /device/lookup/useragent-exists-all-fields-identified", + "GET /device/lookup/useragent-exists-some-fields-identified" ] diff --git a/runtime/StarlingMonkey b/runtime/StarlingMonkey index 14aaa0df0c..3eb6a1a20f 160000 --- a/runtime/StarlingMonkey +++ b/runtime/StarlingMonkey @@ -1 +1 @@ -Subproject commit 14aaa0df0c319a26864e8b7d097caff1e6d9dae7 +Subproject commit 3eb6a1a20f0f2da5510b38afc5369c3f8460dec6 diff --git a/runtime/fastly/CMakeLists.txt b/runtime/fastly/CMakeLists.txt index b0071272a4..b266dc50c5 100644 --- a/runtime/fastly/CMakeLists.txt +++ b/runtime/fastly/CMakeLists.txt @@ -6,7 +6,15 @@ add_builtin(fastly::runtime SRC handler.cpp host-api/component/fastly_world_adap add_builtin(fastly::cache_simple SRC builtins/cache-simple.cpp DEPENDENCIES OpenSSL) add_builtin(fastly::fastly SRC builtins/fastly.cpp) add_builtin(fastly::backend SRC builtins/backend.cpp) +add_builtin(fastly::body SRC builtins/body.cpp) +add_builtin(fastly::cache_core SRC builtins/cache-core.cpp) add_builtin(fastly::kv_store SRC builtins/kv-store.cpp) +add_builtin(fastly::logger SRC builtins/logger.cpp) +add_builtin(fastly::device SRC builtins/device.cpp) +add_builtin(fastly::dictionary SRC builtins/dictionary.cpp) +add_builtin(fastly::edge_rate_limiter SRC builtins/edge-rate-limiter.cpp) +add_builtin(fastly::config_store SRC builtins/config-store.cpp) +add_builtin(fastly::secret_store SRC builtins/secret-store.cpp) add_builtin(fastly::fetch SRC builtins/fetch/fetch.cpp builtins/fetch/request-response.cpp builtins/fetch/headers.cpp) add_builtin(fastly::cache_override SRC builtins/cache-override.cpp) add_builtin(fastly::fetch_event SRC builtins/fetch-event.cpp DEPENDENCIES OpenSSL) diff --git a/runtime/fastly/builtins/backend.cpp b/runtime/fastly/builtins/backend.cpp index c0a1523745..a3dceda11e 100644 --- a/runtime/fastly/builtins/backend.cpp +++ b/runtime/fastly/builtins/backend.cpp @@ -24,8 +24,8 @@ #include "encode.h" #include "fastly.h" +using builtins::BuiltinImpl; using fastly::fastly::FastlyGetErrorMessage; - using fastly::fetch::RequestOrResponse; namespace fastly::backend { diff --git a/runtime/fastly/builtins/backend.h b/runtime/fastly/builtins/backend.h index eef3e8e971..a5a6e41c6e 100644 --- a/runtime/fastly/builtins/backend.h +++ b/runtime/fastly/builtins/backend.h @@ -6,9 +6,7 @@ namespace fastly::backend { -using builtins::BuiltinImpl; - -class Backend : public BuiltinImpl { +class Backend : public builtins::BuiltinImpl { private: public: static constexpr const char *class_name = "Backend"; diff --git a/runtime/fastly/builtins/body.cpp b/runtime/fastly/builtins/body.cpp new file mode 100644 index 0000000000..7f213440ad --- /dev/null +++ b/runtime/fastly/builtins/body.cpp @@ -0,0 +1,317 @@ +#include +#include +#include +#include +#include + +#include "../../../StarlingMonkey/builtins/web/streams/native-stream-source.h" +#include "../../../StarlingMonkey/builtins/web/url.h" +#include "../../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" +#include "./fetch/request-response.h" +#include "body.h" +#include "builtin.h" +#include "fastly.h" +#include "js/Stream.h" + +using builtins::web::streams::NativeStreamSource; +using fastly::fastly::convertBodyInit; +using fastly::fastly::FastlyGetErrorMessage; +using fastly::fetch::RequestOrResponse; + +namespace fastly::body { + +host_api::HttpBody host_body(JSObject *obj) { + JS::Value val = JS::GetReservedSlot(obj, static_cast(FastlyBody::Slots::Body)); + return host_api::HttpBody(static_cast(val.toInt32())); +} + +// concat(dest: FastlyBody): void; +bool FastlyBody::concat(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1); + auto destination_val = args.get(0); + if (!FastlyBody::is_instance(destination_val)) { + JS_ReportErrorUTF8( + cx, "FastlyBody.concat: The `destination` argument is not an instance of FastlyBody."); + return false; + } + JS::RootedObject destination(cx, &destination_val.toObject()); + + auto body = host_body(self); + auto result = body.append(host_body(destination)); + if (auto *err = result.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + args.rval().setUndefined(); + return true; +} + +// read(chunkSize: number): ArrayBuffer; +bool FastlyBody::read(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1); + double chunkSize_val; + if (!JS::ToNumber(cx, args.get(0), &chunkSize_val)) { + return false; + } + uint32_t chunkSize = std::round(chunkSize_val); + if (chunkSize < 0) { + JS_ReportErrorUTF8(cx, + "FastlyBody.read: The `chunkSize` argument has to be a positive integer."); + return false; + } + + auto body = host_body(self); + auto result = body.read(chunkSize); + if (auto *err = result.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto &chunk = result.unwrap(); + JS::UniqueChars buffer = std::move(chunk.ptr); + JS::RootedObject array_buffer(cx); + array_buffer.set(JS::NewArrayBufferWithContents( + cx, chunk.len, buffer.get(), JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory)); + if (!array_buffer) { + JS_ReportOutOfMemory(cx); + return false; + } + + // `array_buffer` now owns `buf` + static_cast(buffer.release()); + + args.rval().setObject(*array_buffer); + return true; +} + +// append(data: BodyInit): void; +bool FastlyBody::append(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1); + + auto body = host_body(self); + if (!body.valid()) { + return false; + } + + auto data_val = args.get(0); + host_api::HttpBody source_body; + JS::UniqueChars data; + JS::RootedObject data_obj(cx, data_val.isObject() ? &data_val.toObject() : nullptr); + // If the data parameter is a Host-backed ReadableStream we optimise our implementation + // by using the ReadableStream's handle directly. + if (data_obj && JS::IsReadableStream(data_obj)) { + if (RequestOrResponse::body_unusable(cx, data_obj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLE_STREAM_LOCKED_OR_DISTRUBED); + return false; + } + + // If the stream is backed by a C@E body handle, we can use that handle directly. + if (NativeStreamSource::stream_is_body(cx, data_obj)) { + JS::RootedObject stream_source(cx, NativeStreamSource::get_stream_source(cx, data_obj)); + JS::RootedObject source_owner(cx, NativeStreamSource::owner(stream_source)); + + source_body = RequestOrResponse::body_handle(source_owner); + if (!source_body.valid()) { + return false; + } + auto res = body.append(source_body); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + args.rval().setUndefined(); + return true; + } else { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_SIMPLE_CACHE_SET_CONTENT_STREAM); + return false; + } + } else { + auto result = convertBodyInit(cx, data_val); + if (result.isErr()) { + return false; + } + size_t length; + std::tie(data, length) = result.unwrap(); + auto write_res = body.write_all_back(reinterpret_cast(data.get()), length); + if (auto *err = write_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + args.rval().setUndefined(); + return true; + } +} + +// prepend(data: BodyInit): void; +bool FastlyBody::prepend(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1); + + auto body = host_body(self); + if (!body.valid()) { + return false; + } + + auto data_val = args.get(0); + host_api::HttpBody source_body; + JS::RootedObject data_obj(cx, data_val.isObject() ? &data_val.toObject() : nullptr); + // If the data parameter is a Host-backed ReadableStream we optimise our implementation + // by using the ReadableStream's handle directly. + if (data_obj && JS::IsReadableStream(data_obj)) { + if (RequestOrResponse::body_unusable(cx, data_obj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_READABLE_STREAM_LOCKED_OR_DISTRUBED); + return false; + } + + // If the stream is backed by a C@E body handle, we can use that handle directly. + if (NativeStreamSource::stream_is_body(cx, data_obj)) { + JS::RootedObject stream_source(cx, NativeStreamSource::get_stream_source(cx, data_obj)); + JS::RootedObject source_owner(cx, NativeStreamSource::owner(stream_source)); + + source_body = RequestOrResponse::body_handle(source_owner); + if (!source_body.valid()) { + return false; + } + + auto make_res = host_api::HttpBody::make(); + if (auto *err = make_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto new_body = make_res.unwrap(); + if (!new_body.valid()) { + return false; + } + + auto res = new_body.append(source_body); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + res = new_body.append(body); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + JS::SetReservedSlot(self, static_cast(Slots::Body), + JS::Int32Value(new_body.handle)); + + args.rval().setUndefined(); + return true; + } else { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SIMPLE_CACHE_SET_CONTENT_STREAM); + return false; + } + } else { + auto result = convertBodyInit(cx, data_val); + if (result.isErr()) { + return false; + } + size_t length; + JS::UniqueChars data; + std::tie(data, length) = result.unwrap(); + auto write_res = body.write_all_front(reinterpret_cast(data.get()), length); + if (auto *err = write_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + args.rval().setUndefined(); + return true; + } +} + +// close(): void; +bool FastlyBody::close(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + auto body = host_body(self); + auto result = body.close(); + + if (auto *err = result.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + args.rval().setUndefined(); + return true; +} + +const JSFunctionSpec FastlyBody::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec FastlyBody::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec FastlyBody::methods[] = { + JS_FN("concat", concat, 1, JSPROP_ENUMERATE), JS_FN("read", read, 1, JSPROP_ENUMERATE), + JS_FN("append", append, 1, JSPROP_ENUMERATE), JS_FN("prepend", prepend, 1, JSPROP_ENUMERATE), + JS_FN("close", close, 0, JSPROP_ENUMERATE), JS_FS_END, +}; + +const JSPropertySpec FastlyBody::properties[] = { + JS_STRING_SYM_PS(toStringTag, "FastlyBody", JSPROP_READONLY), + JS_PS_END, +}; + +bool FastlyBody::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The FastlyBody builtin"); + CTOR_HEADER("FastlyBody", 0); + + auto make_res = host_api::HttpBody::make(); + if (auto *err = make_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto body = make_res.unwrap(); + if (!body.valid()) { + return false; + } + + JS::RootedObject instance(cx, JS_NewObjectForConstructor(cx, &class_, args)); + if (!instance) { + return false; + } + JS::SetReservedSlot(instance, static_cast(Slots::Body), JS::Int32Value(body.handle)); + + args.rval().setObject(*instance); + return true; +} + +JSObject *FastlyBody::create(JSContext *cx, uint32_t handle) { + host_api::HttpBody body{handle}; + + JS::RootedObject instance( + cx, JS_NewObjectWithGivenProto(cx, &FastlyBody::class_, FastlyBody::proto_obj)); + if (!instance) { + return nullptr; + } + JS::SetReservedSlot(instance, static_cast(Slots::Body), JS::Int32Value(handle)); + return instance; +} + +bool install(api::Engine *engine) { + if (!FastlyBody::init_class_impl(engine->cx(), engine->global())) { + return false; + } + RootedObject body_obj(engine->cx(), JS_GetConstructor(engine->cx(), FastlyBody::proto_obj)); + RootedValue body_val(engine->cx(), ObjectValue(*body_obj)); + RootedObject body_ns(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + if (!JS_SetProperty(engine->cx(), body_ns, "FastlyBody", body_val)) { + return false; + } + RootedValue body_ns_val(engine->cx(), JS::ObjectValue(*body_ns)); + if (!engine->define_builtin_module("fastly:body", body_ns_val)) { + return false; + } + return true; +} + +} // namespace fastly::body diff --git a/runtime/fastly/builtins/body.h b/runtime/fastly/builtins/body.h new file mode 100644 index 0000000000..fe6613eefa --- /dev/null +++ b/runtime/fastly/builtins/body.h @@ -0,0 +1,35 @@ +#ifndef JS_COMPUTE_RUNTIME_BODY_H +#define JS_COMPUTE_RUNTIME_BODY_H + +#include "builtin.h" +#include "extension-api.h" + +namespace fastly::body { + +class FastlyBody final : public builtins::BuiltinImpl { + static bool concat(JSContext *cx, unsigned argc, JS::Value *vp); + static bool read(JSContext *cx, unsigned argc, JS::Value *vp); + static bool append(JSContext *cx, unsigned argc, JS::Value *vp); + static bool prepend(JSContext *cx, unsigned argc, JS::Value *vp); + static bool close(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "FastlyBody"; + enum class Slots { + Body, + Count, + }; + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static const unsigned ctor_length = 0; + + static JSObject *create(JSContext *cx, uint32_t handle); + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); +}; + +} // namespace fastly::body + +#endif diff --git a/runtime/fastly/builtins/cache-core.cpp b/runtime/fastly/builtins/cache-core.cpp new file mode 100644 index 0000000000..6583fdf85c --- /dev/null +++ b/runtime/fastly/builtins/cache-core.cpp @@ -0,0 +1,1224 @@ +#include "cache-core.h" +#include "../../../StarlingMonkey/builtins/web/streams/native-stream-source.h" +#include "../../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" +#include "./fetch/headers.h" +#include "body.h" +#include "builtin.h" +#include "fastly.h" +#include "host_api.h" +#include "js/Stream.h" +#include + +using builtins::BuiltinImpl; +using builtins::web::streams::NativeStreamSource; +using fastly::body::FastlyBody; +using fastly::fastly::convertBodyInit; +using fastly::fetch::Headers; +using fastly::fetch::Request; +using fastly::fetch::RequestOrResponse; + +namespace fastly::cache_core { + +namespace { + +api::Engine *ENGINE; + +// The JavaScript LookupOptions parameter we are parsing should have the below interface: +// interface LookupOptions { +// headers?: HeadersInit; +// } +JS::Result parseLookupOptions(JSContext *cx, + JS::HandleValue options_val) { + host_api::CacheLookupOptions options; + if (!options_val.isUndefined()) { + if (!options_val.isObject()) { + JS_ReportErrorASCII(cx, "options argument must be an object"); + return JS::Result(JS::Error()); + } + JS::RootedObject options_obj(cx, &options_val.toObject()); + JS::RootedValue headers_val(cx); + if (!JS_GetProperty(cx, options_obj, "headers", &headers_val)) { + return JS::Result(JS::Error()); + } + // headers property is optional + if (!headers_val.isUndefined()) { + JS::RootedObject headersInstance( + cx, JS_NewObjectWithGivenProto(cx, &Headers::class_, Headers::proto_obj)); + if (!headersInstance) { + return JS::Result(JS::Error()); + } + auto headers = Headers::create(cx, headersInstance, Headers::Mode::Standalone, nullptr, + headers_val, true); + if (!headers) { + return JS::Result(JS::Error()); + } + JS::RootedValue headers_val(cx, JS::ObjectValue(*headers)); + JS::RootedObject requestInstance(cx, Request::create_instance(cx)); + if (!requestInstance) { + return JS::Result(JS::Error()); + } + + // We need to convert the supplied HeadersInit in the `headers` property into a host-backed + // Request which contains the same headers Request::create does exactly that + // however, it also expects a fully valid URL for the Request. We don't ever use the Request + // URL, so we hard-code a valid URL + JS::RootedValue input(cx, JS::StringValue(JS_NewStringCopyZ(cx, "http://example.com"))); + JS::RootedObject request(cx, Request::create(cx, requestInstance, input, headers_val)); + options.request_headers = host_api::HttpReq(Request::request_handle(request)); + } + } + return options; +} + +// The JavaScript TransactionUpdateOptions parameter we are parsing should have the below interface: +// interface TransactionUpdateOptions { +// maxAge: number, +// vary?: Array, +// initialAge?: number, +// staleWhileRevalidate?: number, +// surrogateKeys?: Array, +// length?: number, +// userMetadata?: ArrayBufferView | ArrayBuffer | URLSearchParams | string, +// } +JS::Result parseTransactionUpdateOptions(JSContext *cx, + JS::HandleValue options_val) { + host_api::CacheWriteOptions options; + if (!options_val.isObject()) { + JS_ReportErrorASCII(cx, "options argument must be an object"); + return JS::Result(JS::Error()); + } + JS::RootedObject options_obj(cx, &options_val.toObject()); + + JS::RootedValue maxAge_val(cx); + if (!JS_GetProperty(cx, options_obj, "maxAge", &maxAge_val) || maxAge_val.isUndefined()) { + JS_ReportErrorASCII(cx, "maxAge is required"); + return JS::Result(JS::Error()); + } + + // Convert maxAge field into a number and check the value adheres to our + // validation rules. + double maxAge; + if (!JS::ToNumber(cx, maxAge_val, &maxAge)) { + return JS::Result(JS::Error()); + } + if (maxAge < 0 || std::isnan(maxAge) || std::isinf(maxAge)) { + JS_ReportErrorASCII( + cx, + "maxAge field is an invalid value, only positive numbers can be used for maxAge values."); + return JS::Result(JS::Error()); + } + // turn millisecond representation into nanosecond representation + options.max_age_ns = JS::ToUint64(maxAge) * 1'000'000; + + if (options.max_age_ns > pow(2, 63)) { + JS_ReportErrorASCII(cx, "maxAge can not be greater than 2^63."); + return JS::Result(JS::Error()); + } + + JS::RootedValue initialAge_val(cx); + if (!JS_GetProperty(cx, options_obj, "initialAge", &initialAge_val)) { + return JS::Result(JS::Error()); + } + if (!initialAge_val.isUndefined()) { + // Convert initialAge field into a number and check the value adheres to our + // validation rules. + double initialAge; + if (!JS::ToNumber(cx, initialAge_val, &initialAge)) { + return JS::Result(JS::Error()); + } + if (initialAge < 0 || std::isnan(initialAge) || std::isinf(initialAge)) { + JS_ReportErrorASCII(cx, "initialAge field is an invalid value, only positive numbers can be " + "used for initialAge values."); + return JS::Result(JS::Error()); + } + // turn millisecond representation into nanosecond representation + options.initial_age_ns = JS::ToUint64(initialAge) * 1'000'000; + + if (options.initial_age_ns > pow(2, 63)) { + JS_ReportErrorASCII(cx, "initialAge can not be greater than 2^63."); + return JS::Result(JS::Error()); + } + } + + JS::RootedValue staleWhileRevalidate_val(cx); + if (!JS_GetProperty(cx, options_obj, "staleWhileRevalidate", &staleWhileRevalidate_val)) { + return JS::Result(JS::Error()); + } + if (!staleWhileRevalidate_val.isUndefined()) { + // Convert staleWhileRevalidate field into a number and check the value adheres to our + // validation rules. + double staleWhileRevalidate; + if (!JS::ToNumber(cx, staleWhileRevalidate_val, &staleWhileRevalidate)) { + return JS::Result(JS::Error()); + } + if (staleWhileRevalidate < 0 || std::isnan(staleWhileRevalidate) || + std::isinf(staleWhileRevalidate)) { + JS_ReportErrorASCII(cx, "staleWhileRevalidate field is an invalid value, only positive " + "numbers can be used for staleWhileRevalidate values."); + return JS::Result(JS::Error()); + } + // turn millisecond representation into nanosecond representation + options.stale_while_revalidate_ns = JS::ToUint64(staleWhileRevalidate) * 1'000'000; + + if (options.initial_age_ns > pow(2, 63)) { + JS_ReportErrorASCII(cx, "staleWhileRevalidate can not be greater than 2^63."); + return JS::Result(JS::Error()); + } + } + + JS::RootedValue length_val(cx); + if (!JS_GetProperty(cx, options_obj, "length", &length_val)) { + return JS::Result(JS::Error()); + } + if (!length_val.isUndefined()) { + // Convert length field into a number and check the value adheres to our + // validation rules. + double length; + if (!JS::ToNumber(cx, length_val, &length)) { + return JS::Result(JS::Error()); + } + if (length < 0 || std::isnan(length) || std::isinf(length)) { + JS_ReportErrorASCII( + cx, + "length field is an invalid value, only positive numbers can be used for length values."); + return JS::Result(JS::Error()); + } + options.length = JS::ToUint64(length); + } + + JS::RootedValue vary_val(cx); + if (!JS_GetProperty(cx, options_obj, "vary", &vary_val)) { + return JS::Result(JS::Error()); + } + if (!vary_val.isUndefined()) { + JS::ForOfIterator it(cx); + if (!it.init(vary_val)) { + return JS::Result(JS::Error()); + } + + JS::RootedValue entry_val(cx); + std::string varies; + bool first = true; + while (true) { + bool done; + if (!it.next(&entry_val, &done)) { + return JS::Result(JS::Error()); + } + + if (done) { + break; + } + auto vary = core::encode(cx, entry_val); + if (!vary) { + return JS::Result(JS::Error()); + } + if (first) { + first = false; + } else { + varies += " "; + } + varies += vary; + } + + options.vary_rule = varies; + } + + JS::RootedValue surrogateKeys_val(cx); + if (!JS_GetProperty(cx, options_obj, "surrogateKeys", &surrogateKeys_val)) { + return JS::Result(JS::Error()); + } + if (!surrogateKeys_val.isUndefined()) { + JS::ForOfIterator it(cx); + if (!it.init(vary_val)) { + return JS::Result(JS::Error()); + } + + JS::RootedValue entry_val(cx); + std::string surrogateKeys; + bool first = true; + while (true) { + bool done; + if (!it.next(&entry_val, &done)) { + return JS::Result(JS::Error()); + } + + if (done) { + break; + } + auto skey = core::encode(cx, entry_val); + if (!skey) { + return JS::Result(JS::Error()); + } + if (first) { + first = false; + } else { + surrogateKeys += " "; + } + surrogateKeys += skey; + } + + options.surrogate_keys = surrogateKeys; + } + + JS::RootedValue userMetadata_val(cx); + if (!JS_GetProperty(cx, options_obj, "userMetadata", &userMetadata_val)) { + return JS::Result(JS::Error()); + } + if (!userMetadata_val.isUndefined()) { + auto result = convertBodyInit(cx, userMetadata_val); + if (result.isErr()) { + return JS::Result(JS::Error()); + } + size_t length; + JS::UniqueChars data; + std::tie(data, length) = result.unwrap(); + options.metadata = host_api::HostBytes( + std::unique_ptr(reinterpret_cast(std::move(data).release())), length); + } + + return options; +} + +// The JavaScript TransactionInsertOptions parameter we are parsing should have the below interface: +// interface TransactionInsertOptions { +// maxAge: number, +// vary?: Array, +// initialAge?: number, +// staleWhileRevalidate?: number, +// surrogateKeys?: Array, +// length?: number, +// userMetadata?: ArrayBufferView | ArrayBuffer | URLSearchParams | string, +// sensitive?: boolean, +// } +JS::Result parseTransactionInsertOptions(JSContext *cx, + JS::HandleValue options_val) { + auto options_res = parseTransactionUpdateOptions(cx, options_val); + if (options_res.isErr()) { + return JS::Result(JS::Error()); + } + auto options = options_res.unwrap(); + JS::RootedObject options_obj(cx, &options_val.toObject()); + + JS::RootedValue sensitive_val(cx); + if (!JS_GetProperty(cx, options_obj, "sensitive", &sensitive_val)) { + return JS::Result(JS::Error()); + } + options.sensitive = JS::ToBoolean(sensitive_val); + + return options; +} + +// The JavaScript TransactionInsertOptions parameter we are parsing should have the below interface: +// interface InsertOptions extends TransactionInsertOptions { +// headers?: HeadersInit, +// } +JS::Result parseInsertOptions(JSContext *cx, + JS::HandleValue options_val) { + auto options_res = parseTransactionInsertOptions(cx, options_val); + if (options_res.isErr()) { + return JS::Result(JS::Error()); + } + auto options = options_res.unwrap(); + JS::RootedObject options_obj(cx, &options_val.toObject()); + JS::RootedValue headers_val(cx); + if (!JS_GetProperty(cx, options_obj, "headers", &headers_val)) { + return JS::Result(JS::Error()); + } + // headers property is optional + if (!headers_val.isUndefined()) { + JS::RootedObject headersInstance( + cx, JS_NewObjectWithGivenProto(cx, &Headers::class_, Headers::proto_obj)); + if (!headersInstance) { + return JS::Result(JS::Error()); + } + auto headers = + Headers::create(cx, headersInstance, Headers::Mode::Standalone, nullptr, headers_val, true); + if (!headers) { + return JS::Result(JS::Error()); + } + JS::RootedValue headers_val(cx, JS::ObjectValue(*headers)); + JS::RootedObject requestInstance(cx, Request::create_instance(cx)); + if (!requestInstance) { + return JS::Result(JS::Error()); + } + + // We need to convert the supplied HeadersInit in the `headers` property into a host-backed + // Request which contains the same headers Request::create does exactly that however, + // it also expects a fully valid URL for the Request. We don't ever use the Request URL, so we + // hard-code a valid URL + JS::RootedValue input(cx, JS::StringValue(JS_NewStringCopyZ(cx, "http://example.com"))); + JS::RootedObject request(cx, Request::create(cx, requestInstance, input, headers_val)); + options.request_headers = host_api::HttpReq(Request::request_handle(request)); + } + return options; +} +} // namespace + +// Below is the implementation of the JavaScript CacheState Class which has this definition: +// class CacheState { +// found(): boolean; +// usable(): boolean; +// stale(): boolean; +// mustInsertOrUpdate(): boolean; +// } + +// found(): boolean; +bool CacheState::found(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + auto state = static_cast( + JS::GetReservedSlot(self, static_cast(Slots::State)).toInt32()); + + args.rval().setBoolean(state & CacheState::found_flag); + return true; +} + +// usable(): boolean; +bool CacheState::usable(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + auto state = static_cast( + JS::GetReservedSlot(self, static_cast(Slots::State)).toInt32()); + + args.rval().setBoolean(state & CacheState::usable_flag); + return true; +} + +// stale(): boolean; +bool CacheState::stale(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + auto state = static_cast( + JS::GetReservedSlot(self, static_cast(Slots::State)).toInt32()); + + args.rval().setBoolean(state & CacheState::stale_flag); + return true; +} + +// mustInsertOrUpdate(): boolean; +bool CacheState::mustInsertOrUpdate(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + auto state = static_cast( + JS::GetReservedSlot(self, static_cast(Slots::State)).toInt32()); + + args.rval().setBoolean(state & CacheState::must_insert_or_update_flag); + return true; +} + +const JSFunctionSpec CacheState::static_methods[] = {JS_FS_END}; + +const JSPropertySpec CacheState::static_properties[] = {JS_PS_END}; + +const JSFunctionSpec CacheState::methods[] = { + JS_FN("found", found, 0, JSPROP_ENUMERATE), + JS_FN("usable", usable, 0, JSPROP_ENUMERATE), + JS_FN("stale", stale, 0, JSPROP_ENUMERATE), + JS_FN("mustInsertOrUpdate", mustInsertOrUpdate, 0, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec CacheState::properties[] = { + JS_STRING_SYM_PS(toStringTag, "CacheState", JSPROP_READONLY), JS_PS_END}; + +// We don't expose the ability for JavaScript programs to instantiate a CacheState instance directly +// using the CacheState Constructor +bool CacheState::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ILLEGAL_CTOR); + return false; +} + +JSObject *CacheState::create(JSContext *cx, uint32_t state) { + JS::RootedObject instance(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + if (!instance) { + return nullptr; + } + JS::SetReservedSlot(instance, static_cast(Slots::State), JS::Int32Value(state)); + return instance; +} + +// Below is the implementation of the JavaScript CacheEntry Class which has this definition: +// class CacheEntry { +// close(): void; +// state(): CacheState; +// userMetadata(): ArrayBuffer; +// body(options?: CacheBodyOptions): ReadableStream; +// length(): number | null; +// maxAge(): number; +// staleWhileRevalidate(): number; +// age(): number; +// hits(): number; +// } + +bool CacheEntry::is_instance(JSObject *obj) { + return BuiltinImpl::is_instance(obj) || TransactionCacheEntry::is_instance(obj); +} + +bool CacheEntry::is_instance(JS::Value val) { + return val.isObject() && is_instance(&val.toObject()); +} + +host_api::CacheHandle CacheEntry::get_cache_handle(JSObject *self) { + MOZ_ASSERT(CacheEntry::is_instance(self)); + host_api::CacheHandle handle{static_cast( + JS::GetReservedSlot(self, static_cast(Slots::Handle)).toInt32())}; + return handle; +} + +// close(): void; +bool CacheEntry::close(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "close")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "close", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.close(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + args.rval().setUndefined(); + return true; +} + +// state(): CacheState; +bool CacheEntry::state(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "state")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "state", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.get_state(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + JS::RootedObject state(cx, CacheState::create(cx, res.unwrap().state)); + + args.rval().setObjectOrNull(state); + return true; +} + +// userMetadata(): ArrayBuffer; +bool CacheEntry::userMetadata(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "userMetadata")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "userMetadata", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.get_user_metadata(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto metadata = std::move(res.unwrap()); + JS::RootedObject array_buffer(cx); + array_buffer.set(JS::NewArrayBufferWithContents( + cx, metadata.len, metadata.ptr.get(), JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory)); + if (!array_buffer) { + JS_ReportOutOfMemory(cx); + return false; + } + + // `array_buffer` now owns `metadata` + static_cast(metadata.ptr.release()); + + args.rval().setObject(*array_buffer); + return true; +} + +// body(options?: CacheBodyOptions): ReadableStream; +bool CacheEntry::body(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "body")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "body", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + + host_api::CacheGetBodyOptions options; + auto options_val = args.get(0); + // options parameter is optional + // options is meant to be an object with an optional `start` and `end` fields, both which can be + // Numbers. + ENGINE->dump_value(options_val, stdout); + if (!options_val.isUndefined()) { + if (!options_val.isObject()) { + JS_ReportErrorASCII(cx, "options argument must be an object"); + return false; + } + JS::RootedObject options_obj(cx, &options_val.toObject()); + + JS::RootedValue start_val(cx); + if (!JS_GetProperty(cx, options_obj, "start", &start_val)) { + return false; + } + // start property is optional + if (!start_val.isUndefined()) { + // Convert start field into a number and check the value adheres to our + // validation rules. + double start; + if (!JS::ToNumber(cx, start_val, &start)) { + return false; + } + if (start < 0 || std::isnan(start) || std::isinf(start)) { + JS_ReportErrorASCII( + cx, + "start field is an invalid value, only positive numbers can be used for start values."); + return false; + } + options.start = JS::ToUint64(start); + } + + JS::RootedValue end_val(cx); + if (!JS_GetProperty(cx, options_obj, "end", &end_val)) { + return false; + } + // end property is optional + if (!end_val.isUndefined()) { + // Convert start field into a number and check the value adheres to our + // validation rules. + double end; + if (!JS::ToNumber(cx, end_val, &end)) { + return false; + } + if (end < 0 || std::isnan(end) || std::isinf(end)) { + JS_ReportErrorASCII( + cx, "end field is an invalid value, only positive numbers can be used for end values."); + return false; + } + options.end = JS::ToUint64(end); + } + } + + auto res = handle.get_body(options); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto body = res.unwrap(); + JS::SetReservedSlot(self, static_cast(Slots::Body), JS::Int32Value(body.handle)); + JS::SetReservedSlot(self, static_cast(Slots::BodyStream), JS::NullValue()); + JS::SetReservedSlot(self, static_cast(Slots::HasBody), JS::BooleanValue(true)); + JS::SetReservedSlot(self, static_cast(Slots::BodyUsed), JS::FalseValue()); + + JS::RootedObject source( + cx, NativeStreamSource::create(cx, self, JS::UndefinedHandleValue, + RequestOrResponse::body_source_pull_algorithm, + RequestOrResponse::body_source_cancel_algorithm)); + if (!source) { + return false; + } + + // Create a readable stream with a highwater mark of 0.0 to prevent an eager + // pull. With the default HWM of 1.0, the streams implementation causes a + // pull, which means we enqueue a read from the host handle, which we quite + // often have no interest in at all. + JS::RootedObject body_stream(cx, JS::NewReadableDefaultStreamObject(cx, source, nullptr, 0.0)); + if (!body_stream) { + return false; + } + + args.rval().setObject(*body_stream); + + return true; +} + +// length(): number | null; +bool CacheEntry::length(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "length")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "length", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.get_length(); + if (auto *err = res.to_err()) { + if (host_api::error_is_optional_none(*err)) { + args.rval().setNull(); + return true; + } + HANDLE_ERROR(cx, *err); + return false; + } + auto length = res.unwrap(); + JS::RootedValue result(cx, JS::NumberValue(length)); + args.rval().set(result); + return true; +} + +// maxAge(): number; +bool CacheEntry::maxAge(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "maxAge")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "maxAge", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.get_max_age_ns(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto age = res.unwrap(); + JS::RootedValue result(cx, JS::NumberValue(age / 1'000'000)); + args.rval().set(result); + return true; +} + +// staleWhileRevalidate(): number; +bool CacheEntry::staleWhileRevalidate(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "staleWhileRevalidate")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "staleWhileRevalidate", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.get_stale_while_revalidate_ns(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto staleWhileRevalidateNs = res.unwrap(); + JS::RootedValue result(cx, JS::NumberValue(staleWhileRevalidateNs / 1'000'000)); + args.rval().set(result); + return true; +} + +// age(): number; +bool CacheEntry::age(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "age")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "age", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.get_age_ns(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto age = res.unwrap(); + JS::RootedValue result(cx, JS::NumberValue(age / 1'000'000)); + args.rval().set(result); + return true; +} + +// hits(): number; +bool CacheEntry::hits(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!check_receiver(cx, args.thisv(), "hits")) { + return false; + } + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "hits", 0)) { + return false; + } + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.get_hits(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto hits = res.unwrap(); + JS::RootedValue result(cx, JS::NumberValue(hits)); + args.rval().set(result); + return true; +} + +const JSFunctionSpec CacheEntry::static_methods[] = {JS_FS_END}; + +const JSPropertySpec CacheEntry::static_properties[] = {JS_PS_END}; + +const JSFunctionSpec CacheEntry::methods[] = { + JS_FN("close", close, 0, JSPROP_ENUMERATE), + JS_FN("state", state, 0, JSPROP_ENUMERATE), + JS_FN("userMetadata", userMetadata, 0, JSPROP_ENUMERATE), + JS_FN("body", body, 0, JSPROP_ENUMERATE), + JS_FN("length", length, 0, JSPROP_ENUMERATE), + JS_FN("maxAge", maxAge, 0, JSPROP_ENUMERATE), + JS_FN("staleWhileRevalidate", staleWhileRevalidate, 0, JSPROP_ENUMERATE), + JS_FN("age", age, 0, JSPROP_ENUMERATE), + JS_FN("hits", hits, 0, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec CacheEntry::properties[] = { + JS_STRING_SYM_PS(toStringTag, "CacheEntry", JSPROP_READONLY), JS_PS_END}; + +// We don't expose the ability for JavaScript programs to instantiate a CacheEntry instance directly +// using the CacheEntry Constructor +bool CacheEntry::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ILLEGAL_CTOR); + return false; +} + +JSObject *CacheEntry::create(JSContext *cx, uint32_t handle) { + JS::RootedObject instance(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + if (!instance) { + return nullptr; + } + JS::SetReservedSlot(instance, static_cast(Slots::Handle), JS::Int32Value(handle)); + return instance; +} + +// Below is the implementation of the JavaScript TransactionCacheEntry Class which has this +// definition: class TransactionCacheEntry extends CacheEntry { +// insert(options: TransactionInsertOptions): import("fastly:body").FastlyBody; +// insertAndStreamBack(options: TransactionInsertOptions): [import("fastly:body").FastlyBody, +// CacheEntry]; update(options: TransactionUpdateOptions): void; cancel(): void; +// } + +// insert(options: TransactionInsertOptions): FastlyBody; +bool TransactionCacheEntry::insert(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1); + + auto handle = CacheEntry::get_cache_handle(self); + auto options_res = parseTransactionInsertOptions(cx, args.get(0)); + if (options_res.isErr()) { + return false; + } + auto options = options_res.unwrap(); + auto res = handle.transaction_insert(options); + + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto body = res.unwrap(); + + auto instance = FastlyBody::create(cx, body.handle); + if (!instance) { + return false; + } + + args.rval().setObjectOrNull(instance); + return true; +} + +// insertAndStreamBack(options: TransactionInsertOptions): [FastlyBody, CacheEntry]; +bool TransactionCacheEntry::insertAndStreamBack(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1); + + auto options_res = parseTransactionInsertOptions(cx, args.get(0)); + if (options_res.isErr()) { + return false; + } + auto options = options_res.unwrap(); + + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.transaction_insert_and_stream_back(options); + + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + host_api::HttpBody body; + host_api::CacheHandle cache_handle; + std::tie(body, cache_handle) = res.unwrap(); + + JS::RootedValue writer(cx, JS::ObjectOrNullValue(FastlyBody::create(cx, body.handle))); + + JS::RootedValue reader(cx, JS::ObjectOrNullValue(CacheEntry::create(cx, cache_handle.handle))); + + JS::RootedValueVector result(cx); + if (!result.append(writer)) { + js::ReportOutOfMemory(cx); + return false; + } + if (!result.append(reader)) { + js::ReportOutOfMemory(cx); + return false; + } + + JS::Rooted writer_and_reader(cx, JS::NewArrayObject(cx, result)); + if (!writer_and_reader) { + return false; + } + + args.rval().setObjectOrNull(writer_and_reader); + return true; +} + +// update(options: TransactionUpdateOptions): void; +bool TransactionCacheEntry::update(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1); + + auto options_res = parseTransactionUpdateOptions(cx, args.get(0)); + if (options_res.isErr()) { + return false; + } + auto options = options_res.unwrap(); + + auto handle = CacheEntry::get_cache_handle(self); + + auto state_res = handle.get_state(); + if (auto *err = state_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto state = state_res.unwrap(); + if (!state.is_found() || !state.must_insert_or_update()) { + JS_ReportErrorASCII(cx, + "TransactionCacheEntry.update: entry does not exist or is not updatable"); + return false; + } + auto res = handle.transaction_update(options); + + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + args.rval().setUndefined(); + return true; +} + +// cancel(): void; +bool TransactionCacheEntry::cancel(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0); + + auto handle = CacheEntry::get_cache_handle(self); + auto res = handle.transaction_cancel(); + + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + args.rval().setUndefined(); + return true; +} + +const JSFunctionSpec TransactionCacheEntry::static_methods[] = {JS_FS_END}; + +const JSPropertySpec TransactionCacheEntry::static_properties[] = {JS_PS_END}; + +const JSFunctionSpec TransactionCacheEntry::methods[] = { + JS_FN("insert", insert, 1, JSPROP_ENUMERATE), + JS_FN("insertAndStreamBack", insertAndStreamBack, 1, JSPROP_ENUMERATE), + JS_FN("update", update, 1, JSPROP_ENUMERATE), + JS_FN("cancel", cancel, 0, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec TransactionCacheEntry::properties[] = { + JS_STRING_SYM_PS(toStringTag, "TransactionCacheEntry", JSPROP_READONLY), JS_PS_END}; + +// We don't expose the ability for JavaScript programs to instantiate a TransactionCacheEntry +// instance directly using the TransactionCacheEntry Constructor +bool TransactionCacheEntry::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ILLEGAL_CTOR); + return false; +} + +JSObject *TransactionCacheEntry::create(JSContext *cx, uint32_t handle) { + JS::RootedObject instance(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + if (!instance) { + return nullptr; + } + JS::SetReservedSlot(instance, static_cast(Slots::Handle), JS::Int32Value(handle)); + return instance; +} + +// Below is the implementation of the JavaScript CoreCache Class which has this definition: +// class CoreCache { +// static lookup(key: string, options?: LookupOptions): CacheEntry | null; +// static insert(key: string, options: InsertOptions): import("fastly:body").FastlyBody; +// static transactionLookup(key: string, options?: LookupOptions): TransactionCacheEntry; +// } + +// static lookup(key: string, options?: LookupOptions): CacheEntry | null; +bool CoreCache::lookup(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The CoreCache builtin"); + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "CoreCache.lookup", 1)) { + return false; + } + + // Convert key parameter into a string and check the value adheres to our validation rules. + auto key = core::encode(cx, args.get(0)); + if (!key) { + return false; + } + + if (key.len == 0) { + JS_ReportErrorASCII(cx, "CoreCache.lookup: key can not be an empty string"); + return false; + } + if (key.len > 8135) { + JS_ReportErrorASCII(cx, + "CoreCache.lookup: key is too long, the maximum allowed length is 8135."); + return false; + } + + auto options_result = parseLookupOptions(cx, args.get(1)); + if (options_result.isErr()) { + return false; + } + auto options = options_result.unwrap(); + + auto res = host_api::CacheHandle::lookup(key, options); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto cache_handle = res.unwrap(); + + auto cache_state_res = cache_handle.get_state(); + if (auto *err = cache_state_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto cache_state = cache_state_res.unwrap(); + + if (cache_state.is_found()) { + JS::RootedObject entry(cx, CacheEntry::create(cx, cache_handle.handle)); + args.rval().setObject(*entry); + } else { + args.rval().setNull(); + } + return true; +} + +// static insert(key: string, options: InsertOptions): FastlyBody; +bool CoreCache::insert(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The CoreCache builtin"); + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "CoreCache.insert", 2)) { + return false; + } + + // Convert key parameter into a string and check the value adheres to our validation rules. + auto key = core::encode(cx, args.get(0)); + if (!key) { + return false; + } + + if (key.len == 0) { + JS_ReportErrorASCII(cx, "CoreCache.insert: key can not be an empty string"); + return false; + } + if (key.len > 8135) { + JS_ReportErrorASCII(cx, + "CoreCache.insert: key is too long, the maximum allowed length is 8135."); + return false; + } + + auto options_res = parseInsertOptions(cx, args.get(1)); + if (options_res.isErr()) { + return false; + } + auto options = options_res.unwrap(); + + auto res = host_api::CacheHandle::insert(key, options); + + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto body = res.unwrap(); + + JS::RootedObject instance(cx, FastlyBody::create(cx, body.handle)); + if (!instance) { + return false; + } + + args.rval().setObjectOrNull(instance); + return true; +} + +// static transactionLookup(key: string, options?: LookupOptions): TransactionCacheEntry; +bool CoreCache::transactionLookup(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The CoreCache builtin"); + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "CoreCache.transactionLookup", 1)) { + return false; + } + + // Convert key parameter into a string and check the value adheres to our validation rules. + auto key = core::encode(cx, args.get(0)); + if (!key) { + return false; + } + + if (key.len == 0) { + JS_ReportErrorASCII(cx, "CoreCache.transactionLookup: key can not be an empty string"); + return false; + } + if (key.len > 8135) { + JS_ReportErrorASCII( + cx, "CoreCache.transactionLookup: key is too long, the maximum allowed length is 8135."); + return false; + } + + host_api::CacheLookupOptions options; + auto options_val = args.get(1); + // options parameter is optional + // options is meant to be an object with an optional headers field, + // the headers field can be: + // Headers | string[][] | Record; + if (!options_val.isUndefined()) { + if (!options_val.isObject()) { + JS_ReportErrorASCII(cx, "options argument must be an object"); + return false; + } + JS::RootedObject options_obj(cx, &options_val.toObject()); + JS::RootedValue headers_val(cx); + if (!JS_GetProperty(cx, options_obj, "headers", &headers_val)) { + return false; + } + // headers property is optional + if (!headers_val.isUndefined()) { + JS::RootedObject headersInstance( + cx, JS_NewObjectWithGivenProto(cx, &Headers::class_, Headers::proto_obj)); + if (!headersInstance) { + return false; + } + auto headers = Headers::create(cx, headersInstance, Headers::Mode::Standalone, nullptr, + headers_val, true); + if (!headers) { + return false; + } + JS::RootedValue headers_val(cx, JS::ObjectValue(*headers)); + JS::RootedObject requestInstance(cx, Request::create_instance(cx)); + if (!requestInstance) { + return false; + } + + // We need to convert the supplied HeadersInit in the `headers` property into a host-backed + // Request which contains the same headers Request::create does exactly that + // however, it also expects a fully valid URL for the Request. We don't ever use the Request + // URL, so we hard-code a valid URL + JS::RootedValue input(cx, JS::StringValue(JS_NewStringCopyZ(cx, "http://example.com"))); + JS::RootedObject request(cx, Request::create(cx, requestInstance, input, headers_val)); + options.request_headers = host_api::HttpReq(Request::request_handle(request)); + } + } + + auto res = host_api::CacheHandle::transaction_lookup(key, options); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto cache_handle = res.unwrap(); + + JS::RootedObject entry(cx, TransactionCacheEntry::create(cx, cache_handle.handle)); + args.rval().setObject(*entry); + return true; +} + +const JSFunctionSpec CoreCache::static_methods[] = { + JS_FN("lookup", lookup, 1, JSPROP_ENUMERATE), + JS_FN("insert", insert, 2, JSPROP_ENUMERATE), + JS_FN("transactionLookup", transactionLookup, 1, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec CoreCache::static_properties[] = {JS_PS_END}; + +const JSFunctionSpec CoreCache::methods[] = {JS_FS_END}; + +const JSPropertySpec CoreCache::properties[] = { + JS_STRING_SYM_PS(toStringTag, "CoreCache", JSPROP_READONLY), JS_PS_END}; + +// We don't expose the ability for JavaScript programs to instantiate a CoreCache instance directly +// using the CoreCache Constructor +bool CoreCache::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ILLEGAL_CTOR); + return false; +} + +bool install(api::Engine *engine) { + ENGINE = engine; + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + JS::RootedObject proto(engine->cx(), CacheEntry::proto_obj); + if (!proto) { + return false; + } + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global(), proto)) { + return false; + } + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + + // fastly:cache + RootedObject cache(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + RootedValue cache_val(engine->cx(), JS::ObjectValue(*cache)); + RootedObject core_cache_obj(engine->cx(), JS_GetConstructor(engine->cx(), CoreCache::proto_obj)); + RootedValue core_cache_val(engine->cx(), ObjectValue(*core_cache_obj)); + if (!JS_SetProperty(engine->cx(), cache, "CoreCache", core_cache_val)) { + return false; + } + RootedObject cache_entry_obj(engine->cx(), + JS_GetConstructor(engine->cx(), CacheEntry::proto_obj)); + RootedValue cache_entry_val(engine->cx(), ObjectValue(*cache_entry_obj)); + if (!JS_SetProperty(engine->cx(), cache, "CacheEntry", cache_entry_val)) { + return false; + } + RootedObject cache_state_obj(engine->cx(), + JS_GetConstructor(engine->cx(), CacheState::proto_obj)); + RootedValue cache_state_val(engine->cx(), ObjectValue(*cache_state_obj)); + if (!JS_SetProperty(engine->cx(), cache, "CacheState", cache_state_val)) { + return false; + } + RootedObject transaction_cache_entry_obj( + engine->cx(), JS_GetConstructor(engine->cx(), TransactionCacheEntry::proto_obj)); + RootedValue transaction_cache_entry_val(engine->cx(), ObjectValue(*transaction_cache_entry_obj)); + if (!JS_SetProperty(engine->cx(), cache, "TransactionCacheEntry", transaction_cache_entry_val)) { + return false; + } + RootedValue simple_cache_val(engine->cx()); + if (!JS_GetProperty(engine->cx(), engine->global(), "SimpleCache", &simple_cache_val)) { + return false; + } + if (!JS_SetProperty(engine->cx(), cache, "SimpleCache", simple_cache_val)) { + return false; + } + RootedValue simple_cache_entry_val(engine->cx()); + if (!JS_GetProperty(engine->cx(), engine->global(), "SimpleCacheEntry", + &simple_cache_entry_val)) { + return false; + } + if (!JS_SetProperty(engine->cx(), cache, "SimpleCacheEntry", simple_cache_entry_val)) { + return false; + } + if (!engine->define_builtin_module("fastly:cache", cache_val)) { + return false; + } + return true; +} + +} // namespace fastly::cache_core diff --git a/runtime/fastly/builtins/cache-core.h b/runtime/fastly/builtins/cache-core.h new file mode 100644 index 0000000000..1cca1ebd47 --- /dev/null +++ b/runtime/fastly/builtins/cache-core.h @@ -0,0 +1,167 @@ +#ifndef FASTLY_CACHE_CORE_H +#define FASTLY_CACHE_CORE_H + +#include "./fetch/request-response.h" +#include "builtin.h" +#include "extension-api.h" + +namespace fastly::cache_core { + +// export class CacheState { +// found(): boolean; +// usable(): boolean; +// stale(): boolean; +// mustInsertOrUpdate(): boolean; +// } +class CacheState : public builtins::BuiltinImpl { + static constexpr const uint8_t found_flag = 1 << 0; + static constexpr const uint8_t usable_flag = 1 << 1; + static constexpr const uint8_t stale_flag = 1 << 2; + static constexpr const uint8_t must_insert_or_update_flag = 1 << 3; + + // found(): boolean; + static bool found(JSContext *cx, unsigned argc, JS::Value *vp); + + // usable(): boolean; + static bool usable(JSContext *cx, unsigned argc, JS::Value *vp); + + // stale(): boolean; + static bool stale(JSContext *cx, unsigned argc, JS::Value *vp); + + // mustInsertOrUpdate(): boolean; + static bool mustInsertOrUpdate(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "CacheState"; + static const int ctor_length = 0; + enum Slots { State, Count }; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + static JSObject *create(JSContext *cx, uint32_t handle); +}; + +class CacheEntry : public builtins::BuiltinImpl { + // cache-close: func(handle: cache-handle) -> result<_, error> + // close(): void; + static bool close(JSContext *cx, unsigned argc, JS::Value *vp); + + // cache-get-state: func(handle: cache-handle) -> result + // state(): CacheState; + static bool state(JSContext *cx, unsigned argc, JS::Value *vp); + + /// cache-get-user-metadata: func(handle: cache-handle) -> result, error> + // userMetadata(): ArrayBuffer; + static bool userMetadata(JSContext *cx, unsigned argc, JS::Value *vp); + + // cache-get-body: func(handle: cache-handle, options: cache-get-body-options) -> + // result body(options?: CacheBodyOptions): ReadableStream; + static bool body(JSContext *cx, unsigned argc, JS::Value *vp); + + // cache-get-length: func(handle: cache-handle) -> result + // length(): number; + static bool length(JSContext *cx, unsigned argc, JS::Value *vp); + + // cache-get-max-age-ns: func(handle: cache-handle) -> result + // maxAge(): number; + static bool maxAge(JSContext *cx, unsigned argc, JS::Value *vp); + + // cache-get-stale-while-revalidate-ns: func(handle: cache-handle) -> result + // staleWhileRevalidate(): number; + static bool staleWhileRevalidate(JSContext *cx, unsigned argc, JS::Value *vp); + + // cache-get-age-ns: func(handle: cache-handle) -> result + // age(): number; + static bool age(JSContext *cx, unsigned argc, JS::Value *vp); + + // cache-get-hits: func(handle: cache-handle) -> result + // hits(): number; + static bool hits(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "CacheEntry"; + static const int ctor_length = 0; + enum Slots { + Body = static_cast(fetch::RequestOrResponse::Slots::Body), + BodyStream = static_cast(fetch::RequestOrResponse::Slots::BodyStream), + HasBody = static_cast(fetch::RequestOrResponse::Slots::HasBody), + BodyUsed = static_cast(fetch::RequestOrResponse::Slots::BodyUsed), + Handle = static_cast(fetch::RequestOrResponse::Slots::Count), + Count + }; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool is_instance(JSObject *obj); + static bool is_instance(JS::Value val); + static host_api::CacheHandle get_cache_handle(JSObject *self); + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + static JSObject *create(JSContext *cx, uint32_t handle); +}; + +class TransactionCacheEntry : public builtins::BuiltinImpl { + + static bool insert(JSContext *cx, unsigned argc, JS::Value *vp); + static bool insertAndStreamBack(JSContext *cx, unsigned argc, JS::Value *vp); + static bool update(JSContext *cx, unsigned argc, JS::Value *vp); + static bool cancel(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "TransactionCacheEntry"; + static const int ctor_length = 0; + enum Slots { + Body = static_cast(CacheEntry::Slots::Body), + BodyStream = static_cast(CacheEntry::Slots::BodyStream), + HasBody = static_cast(CacheEntry::Slots::HasBody), + BodyUsed = static_cast(CacheEntry::Slots::BodyUsed), + Handle = static_cast(CacheEntry::Slots::Handle), + Count + }; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + static JSObject *create(JSContext *cx, uint32_t handle); +}; + +class CoreCache : public builtins::BuiltinImpl { + // cache-lookup: func(cache-key: string, options: cache-lookup-options) -> result static lookup(key: string, options?: LookupOptions): CacheEntry | null; + static bool lookup(JSContext *cx, unsigned argc, JS::Value *vp); + + // cache-insert: func(cache-key: string, options: cache-write-options) -> result static insert(key: string, options: InsertOptions): FastlyBody; + static bool insert(JSContext *cx, unsigned argc, JS::Value *vp); + + // transaction-lookup: func(cache-key: string, options: cache-lookup-options) -> + // result static transactionLookup(key: string, optoptions?: LookupOptions): + // CacheEntry | null; + static bool transactionLookup(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "CoreCache"; + static const int ctor_length = 0; + enum Slots { Count }; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); +}; + +} // namespace fastly::cache_core + +#endif diff --git a/runtime/fastly/builtins/cache-override.cpp b/runtime/fastly/builtins/cache-override.cpp index 99f2960734..900ce6b7fe 100644 --- a/runtime/fastly/builtins/cache-override.cpp +++ b/runtime/fastly/builtins/cache-override.cpp @@ -12,6 +12,7 @@ #include "fastly.h" #include "host_api.h" +using builtins::BuiltinImpl; using fastly::fastly::FastlyGetErrorMessage; namespace fastly::cache_override { diff --git a/runtime/fastly/builtins/cache-override.h b/runtime/fastly/builtins/cache-override.h index 0fbebfadea..f519c4cf2d 100644 --- a/runtime/fastly/builtins/cache-override.h +++ b/runtime/fastly/builtins/cache-override.h @@ -4,11 +4,9 @@ #include "../host-api/host_api_fastly.h" #include "builtin.h" -using builtins::BuiltinImpl; - namespace fastly::cache_override { -class CacheOverride : public BuiltinImpl { +class CacheOverride : public builtins::BuiltinImpl { private: public: static constexpr const char *class_name = "CacheOverride"; diff --git a/runtime/fastly/builtins/cache-simple.cpp b/runtime/fastly/builtins/cache-simple.cpp index c74af35167..bcaf5ef152 100644 --- a/runtime/fastly/builtins/cache-simple.cpp +++ b/runtime/fastly/builtins/cache-simple.cpp @@ -11,9 +11,11 @@ #include "openssl/evp.h" #include +using builtins::BuiltinImpl; using builtins::web::streams::NativeStreamSource; using fastly::fastly::convertBodyInit; using fastly::fastly::FastlyGetErrorMessage; +using fastly::fetch::RequestOrResponse; namespace fastly::cache_simple { diff --git a/runtime/fastly/builtins/cache-simple.h b/runtime/fastly/builtins/cache-simple.h index 2f9d5aaaed..6abbd4510d 100644 --- a/runtime/fastly/builtins/cache-simple.h +++ b/runtime/fastly/builtins/cache-simple.h @@ -5,12 +5,10 @@ #include "./fetch/request-response.h" #include "builtin.h" -using fastly::fetch::RequestOrResponse; - namespace fastly::cache_simple { -class SimpleCacheEntry final : public BuiltinImpl { - template +class SimpleCacheEntry final : public builtins::BuiltinImpl { + template static bool bodyAll(JSContext *cx, unsigned argc, JS::Value *vp); static bool body_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool bodyUsed_get(JSContext *cx, unsigned argc, JS::Value *vp); @@ -18,7 +16,7 @@ class SimpleCacheEntry final : public BuiltinImpl { public: static constexpr const char *class_name = "SimpleCacheEntry"; - using Slots = RequestOrResponse::Slots; + using Slots = fetch::RequestOrResponse::Slots; static const JSFunctionSpec static_methods[]; static const JSPropertySpec static_properties[]; static const JSFunctionSpec methods[]; @@ -26,12 +24,11 @@ class SimpleCacheEntry final : public BuiltinImpl { static const unsigned ctor_length = 0; - static bool init_class(JSContext *cx, JS::HandleObject global); static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); static JSObject *create(JSContext *cx, host_api::HttpBody body_handle); }; -class SimpleCache : public BuiltinImpl { +class SimpleCache : public builtins::BuiltinImpl { private: public: static constexpr const char *class_name = "SimpleCache"; diff --git a/runtime/fastly/builtins/config-store.cpp b/runtime/fastly/builtins/config-store.cpp new file mode 100644 index 0000000000..9c077110b2 --- /dev/null +++ b/runtime/fastly/builtins/config-store.cpp @@ -0,0 +1,148 @@ +#include "config-store.h" +#include "../../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" +#include "fastly.h" + +using builtins::BuiltinImpl; +using fastly::fastly::FastlyGetErrorMessage; + +namespace fastly::config_store { + +host_api::Dict ConfigStore::config_store_handle(JSObject *obj) { + JS::Value val = JS::GetReservedSlot(obj, ConfigStore::Slots::Handle); + return host_api::Dict(val.toInt32()); +} + +bool ConfigStore::get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + auto key = core::encode(cx, args[0]); + // If the converted string has a length of 0 then we throw an Error + // because Dictionary keys have to be at-least 1 character. + if (!key || key.len == 0) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_CONFIG_STORE_KEY_EMPTY); + return false; + } + + // key has to be less than 256 + if (key.len > 255) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_CONFIG_STORE_KEY_TOO_LONG); + return false; + } + + std::string_view key_str = key; + // Ensure that we throw an exception for all unexpected host errors. + auto get_res = ConfigStore::config_store_handle(self).get(key_str); + if (auto *err = get_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + // None indicates the key wasn't found, so we return null. + auto ret = std::move(get_res.unwrap()); + if (!ret.has_value()) { + args.rval().setNull(); + return true; + } + + JS::RootedString text(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(ret->begin(), ret->size()))); + if (!text) { + return false; + } + + args.rval().setString(text); + return true; +} + +const JSFunctionSpec ConfigStore::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec ConfigStore::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec ConfigStore::methods[] = {JS_FN("get", get, 1, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec ConfigStore::properties[] = {JS_PS_END}; + +bool ConfigStore::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The ConfigStore builtin"); + CTOR_HEADER("ConfigStore", 1); + + auto name = core::encode(cx, args[0]); + + // If the converted string has a length of 0 then we throw an Error + // because Dictionary names have to be at-least 1 character. + if (!name) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_CONFIG_STORE_NAME_EMPTY); + return false; + } + + // If the converted string has a length of more than 255 then we throw an Error + // because Dictionary names have to be less than 255 characters. + if (name.size() > 255) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_CONFIG_STORE_NAME_TOO_LONG); + return false; + } + + // Name must start with ascii alphabetical and contain only ascii alphanumeric, underscore, and + // whitespace + if (!std::isalpha(*name.begin())) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_CONFIG_STORE_NAME_START_WITH_ASCII_ALPHA); + return false; + } + + auto is_valid_name = std::all_of(std::next(name.begin(), 1), name.end(), [&](auto character) { + return std::isalnum(character) || character == '_' || character == ' '; + }); + + if (!is_valid_name) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_CONFIG_STORE_NAME_CONTAINS_INVALID_CHARACTER); + return false; + } + + JS::RootedObject config_store(cx, JS_NewObjectForConstructor(cx, &class_, args)); + auto open_res = host_api::Dict::open(name); + if (auto *err = open_res.to_err()) { + if (host_api::error_is_bad_handle(*err)) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_CONFIG_STORE_DOES_NOT_EXIST, name.begin()); + return false; + } else { + HANDLE_ERROR(cx, *err); + return false; + } + } + + JS::SetReservedSlot(config_store, ConfigStore::Slots::Handle, + JS::Int32Value(open_res.unwrap().handle)); + if (!config_store) + return false; + args.rval().setObject(*config_store); + return true; +} + +bool install(api::Engine *engine) { + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + + RootedObject config_store_ns_obj(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + RootedValue config_store_ns_val(engine->cx(), JS::ObjectValue(*config_store_ns_obj)); + RootedObject config_store_obj(engine->cx(), + JS_GetConstructor(engine->cx(), ConfigStore::proto_obj)); + RootedValue config_store_val(engine->cx(), ObjectValue(*config_store_obj)); + if (!JS_SetProperty(engine->cx(), config_store_ns_obj, "ConfigStore", config_store_val)) { + return false; + } + if (!engine->define_builtin_module("fastly:config-store", config_store_ns_val)) { + return false; + } + + return true; +} + +} // namespace fastly::config_store diff --git a/runtime/fastly/builtins/config-store.h b/runtime/fastly/builtins/config-store.h new file mode 100644 index 0000000000..7d14b57f7b --- /dev/null +++ b/runtime/fastly/builtins/config-store.h @@ -0,0 +1,32 @@ +#ifndef FASTLY_CONFIG_STORE_H +#define FASTLY_CONFIG_STORE_H + +#include "../host-api/host_api_fastly.h" +#include "builtin.h" +#include "extension-api.h" + +namespace fastly::config_store { + +class ConfigStore : public builtins::BuiltinImpl { +private: +public: + static constexpr const char *class_name = "ConfigStore"; + static const int ctor_length = 1; + enum Slots { Handle, Count }; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool get(JSContext *cx, unsigned argc, JS::Value *vp); + + static host_api::Dict config_store_handle(JSObject *obj); + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + + static bool init_class(JSContext *cx, JS::HandleObject global); +}; + +} // namespace fastly::config_store + +#endif diff --git a/runtime/fastly/builtins/device.cpp b/runtime/fastly/builtins/device.cpp new file mode 100644 index 0000000000..605c6ebbc0 --- /dev/null +++ b/runtime/fastly/builtins/device.cpp @@ -0,0 +1,582 @@ +#include "device.h" +#include "../../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" +#include "builtin.h" +#include "js/JSON.h" + +using builtins::BuiltinImpl; + +namespace fastly::device { + +namespace { +bool callbackCalled; +bool write_json_to_buf(const char16_t *str, uint32_t strlen, void *out) { + callbackCalled = true; + auto outstr = static_cast(out); + outstr->append(str, strlen); + + return true; +} +JSObject *deviceToJSON(JSContext *cx, JS::HandleObject self) { + MOZ_ASSERT(Device::is_instance(self)); + + JS::RootedValue device_info( + cx, JS::GetReservedSlot(self, static_cast(Device::Slots::DeviceInfo))); + JS::RootedValue value(cx); + if (!device_info.isObject()) { + return nullptr; + } + + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedObject result(cx, JS_NewPlainObject(cx)); + + if (!JS_GetProperty(cx, device_info_obj, "name", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isString() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "name", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "brand", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isString() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "brand", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "model", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isString() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "model", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "hardwareType", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isString() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "hardwareType", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "isDesktop", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isBoolean() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "isDesktop", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "isGameConsole", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isBoolean() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "isGameConsole", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "isMediaPlayer", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isBoolean() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "isMediaPlayer", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "isMobile", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isBoolean() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "isMobile", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "isSmartTV", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isBoolean() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "isSmartTV", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "isTablet", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isBoolean() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "isTablet", value)) { + return nullptr; + } + + if (!JS_GetProperty(cx, device_info_obj, "isTouchscreen", &value)) { + return nullptr; + } + MOZ_ASSERT(value.isBoolean() || value.isNullOrUndefined()); + if (value.isUndefined()) { + value.setNull(); + } + if (!JS_SetProperty(cx, result, "isTouchscreen", value)) { + return nullptr; + } + + return result; +} + +} // namespace + +/* + * This is used by our `Console` implementation and logs all the approproiate properties for a + * Device instance + */ +JSString *Device::ToSource(JSContext *cx, JS::HandleObject self) { + MOZ_ASSERT(Device::is_instance(self)); + JS::RootedValue data(cx); + data.setObjectOrNull(deviceToJSON(cx, self)); + JS::RootedObject replacer(cx); + JS::RootedValue space(cx); + + std::u16string out; + // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. + callbackCalled = false; + if (!JS::ToJSON(cx, data, replacer, space, &write_json_to_buf, &out)) { + return nullptr; + } + if (!callbackCalled) { + JS_ReportErrorASCII(cx, "The data is not JSON serializable"); + return nullptr; + } + + return JS_NewUCStringCopyN(cx, out.c_str(), out.length()); +} + +bool Device::toJSON(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + args.rval().setObjectOrNull(deviceToJSON(cx, self)); + + return true; +} + +// get name(): string | null; +bool Device::device_name_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setNull(); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_name(cx); + if (!JS_GetProperty(cx, device_info_obj, "name", &device_name)) { + return false; + } + MOZ_ASSERT(device_name.isString() || device_name.isNullOrUndefined()); + if (device_name.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_name); + } + return true; +} + +// get brand(): string | null; +bool Device::brand_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setNull(); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_brand(cx); + if (!JS_GetProperty(cx, device_info_obj, "brand", &device_brand)) { + return false; + } + MOZ_ASSERT(device_brand.isString() || device_brand.isNullOrUndefined()); + if (device_brand.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_brand); + } + return true; +} + +// get model(): string | null; +bool Device::model_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setNull(); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_model(cx); + if (!JS_GetProperty(cx, device_info_obj, "model", &device_model)) { + return false; + } + MOZ_ASSERT(device_model.isString() || device_model.isNullOrUndefined()); + if (device_model.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_model); + } + return true; +} + +// get hardwareType(): string | null; +bool Device::hardware_type_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setNull(); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_hwtype(cx); + if (!JS_GetProperty(cx, device_info_obj, "hwtype", &device_hwtype)) { + return false; + } + MOZ_ASSERT(device_hwtype.isString() || device_hwtype.isNullOrUndefined()); + if (device_hwtype.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_hwtype); + } + return true; +} + +// get isDesktop(): boolean | null; +bool Device::is_desktop_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setBoolean(false); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_is_desktop(cx); + if (!JS_GetProperty(cx, device_info_obj, "is_desktop", &device_is_desktop)) { + return false; + } + MOZ_ASSERT(device_is_desktop.isBoolean() || device_is_desktop.isNullOrUndefined()); + if (device_is_desktop.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_is_desktop); + } + return true; +} + +// get isGameConsole(): boolean | null; +bool Device::is_gameconsole_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setBoolean(false); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_is_gameconsole(cx); + if (!JS_GetProperty(cx, device_info_obj, "is_gameconsole", &device_is_gameconsole)) { + return false; + } + MOZ_ASSERT(device_is_gameconsole.isBoolean() || device_is_gameconsole.isNullOrUndefined()); + if (device_is_gameconsole.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_is_gameconsole); + } + return true; +} + +// get isMediaPlayer(): boolean | null; +bool Device::is_mediaplayer_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setBoolean(false); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_is_mediaplayer(cx); + if (!JS_GetProperty(cx, device_info_obj, "is_mediaplayer", &device_is_mediaplayer)) { + return false; + } + MOZ_ASSERT(device_is_mediaplayer.isBoolean() || device_is_mediaplayer.isNullOrUndefined()); + if (device_is_mediaplayer.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_is_mediaplayer); + } + return true; +} + +// get isMobile(): boolean | null; +bool Device::is_mobile_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setBoolean(false); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_is_mobile(cx); + if (!JS_GetProperty(cx, device_info_obj, "is_mobile", &device_is_mobile)) { + return false; + } + MOZ_ASSERT(device_is_mobile.isBoolean() || device_is_mobile.isNullOrUndefined()); + if (device_is_mobile.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_is_mobile); + } + return true; +} + +// get isSmartTV(): boolean | null; +bool Device::is_smarttv_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setBoolean(false); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_is_smarttv(cx); + if (!JS_GetProperty(cx, device_info_obj, "is_smarttv", &device_is_smarttv)) { + return false; + } + MOZ_ASSERT(device_is_smarttv.isBoolean() || device_is_smarttv.isNullOrUndefined()); + if (device_is_smarttv.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_is_smarttv); + } + return true; +} + +// get isTablet(): boolean | null; +bool Device::is_tablet_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setBoolean(false); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_is_tablet(cx); + if (!JS_GetProperty(cx, device_info_obj, "is_tablet", &device_is_tablet)) { + return false; + } + MOZ_ASSERT(device_is_tablet.isBoolean() || device_is_tablet.isNullOrUndefined()); + if (device_is_tablet.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_is_tablet); + } + return true; +} + +// get isTouchscreen(): boolean | null; +bool Device::is_touchscreen_get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + JS::RootedValue device_info(cx, + JS::GetReservedSlot(self, static_cast(Slots::DeviceInfo))); + if (!device_info.isObject()) { + args.rval().setBoolean(false); + return true; + } + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + JS::RootedValue device_is_touchscreen(cx); + if (!JS_GetProperty(cx, device_info_obj, "is_touchscreen", &device_is_touchscreen)) { + return false; + } + MOZ_ASSERT(device_is_touchscreen.isBoolean() || device_is_touchscreen.isNullOrUndefined()); + if (device_is_touchscreen.isUndefined()) { + args.rval().setNull(); + } else { + args.rval().set(device_is_touchscreen); + } + return true; +} + +// static lookup(useragent: string): Device; +bool Device::lookup(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The Device builtin"); + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "Device.lookup", 1)) { + return false; + } + + // Convert key parameter into a string and check the value adheres to our validation rules. + auto key = core::encode(cx, args[0]); + if (!key) { + return false; + } + + if (key.len == 0) { + JS_ReportErrorASCII(cx, "Device.lookup: useragent parameter can not be an empty string"); + return false; + } + + auto lookup_res = host_api::DeviceDetection::lookup(key); + if (auto *err = lookup_res.to_err()) { + if (host_api::error_is_optional_none(*err)) { + args.rval().setNull(); + return true; + } + HANDLE_ERROR(cx, *err); + return false; + } + auto result = std::move(lookup_res.unwrap()); + + JS::RootedString device_info_str(cx, JS_NewStringCopyN(cx, result.ptr.release(), result.len)); + if (!device_info_str) { + return false; + } + + JS::RootedValue device_info(cx); + + if (!JS_ParseJSON(cx, device_info_str, &device_info)) { + return false; + } + + MOZ_ASSERT(device_info.isObject()); + + JS::RootedObject device_info_obj(cx, device_info.toObjectOrNull()); + + args.rval().setObjectOrNull(Device::create(cx, device_info_obj)); + + return true; +} + +const JSFunctionSpec Device::static_methods[] = { + JS_FN("lookup", lookup, 1, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec Device::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec Device::methods[] = {JS_FN("toJSON", toJSON, 0, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec Device::properties[] = { + JS_PSG("name", Device::device_name_get, JSPROP_ENUMERATE), + JS_PSG("brand", Device::brand_get, JSPROP_ENUMERATE), + JS_PSG("model", Device::model_get, JSPROP_ENUMERATE), + JS_PSG("hardwareType", Device::hardware_type_get, JSPROP_ENUMERATE), + JS_PSG("isDesktop", Device::is_desktop_get, JSPROP_ENUMERATE), + JS_PSG("isGameConsole", Device::is_gameconsole_get, JSPROP_ENUMERATE), + JS_PSG("isMediaPlayer", Device::is_mediaplayer_get, JSPROP_ENUMERATE), + JS_PSG("isMobile", Device::is_mobile_get, JSPROP_ENUMERATE), + JS_PSG("isSmartTV", Device::is_smarttv_get, JSPROP_ENUMERATE), + JS_PSG("isTablet", Device::is_tablet_get, JSPROP_ENUMERATE), + JS_PSG("isTouchscreen", Device::is_touchscreen_get, JSPROP_ENUMERATE), + JS_STRING_SYM_PS(toStringTag, "Device", JSPROP_READONLY), + JS_PS_END}; + +bool Device::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_ILLEGAL_CTOR); + return false; +} + +JSObject *Device::create(JSContext *cx, JS::HandleObject device_info) { + JS::RootedObject instance(cx, JS_NewObjectWithGivenProto(cx, &Device::class_, Device::proto_obj)); + + JS::RootedValue device(cx); + if (!JS_GetProperty(cx, device_info, "device", &device)) { + return nullptr; + } + MOZ_ASSERT(device.isObject()); + + JS::SetReservedSlot(instance, static_cast(Slots::DeviceInfo), device); + + return instance; +} + +bool install(api::Engine *engine) { + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + + RootedObject device_ns_obj(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + RootedValue device_ns_val(engine->cx(), JS::ObjectValue(*device_ns_obj)); + RootedObject device_obj(engine->cx(), JS_GetConstructor(engine->cx(), Device::proto_obj)); + RootedValue device_val(engine->cx(), ObjectValue(*device_obj)); + if (!JS_SetProperty(engine->cx(), device_ns_obj, "Device", device_val)) { + return false; + } + if (!engine->define_builtin_module("fastly:device", device_ns_val)) { + return false; + } + + return true; +} + +} // namespace fastly::device diff --git a/runtime/fastly/builtins/device.h b/runtime/fastly/builtins/device.h new file mode 100644 index 0000000000..2bf8320981 --- /dev/null +++ b/runtime/fastly/builtins/device.h @@ -0,0 +1,60 @@ +#ifndef FASTLY_DEVICE_H +#define FASTLY_DEVICE_H + +#include "builtin.h" +#include "extension-api.h" + +namespace fastly::device { + +class Device : public builtins::BuiltinImpl { +private: +public: + static constexpr const char *class_name = "Device"; + static const int ctor_length = 0; + // { + // "brand": null, + // "hwtype": null, + // "is_desktop": false, + // "is_ereader": false, + // "is_gameconsole": false, + // "is_mediaplayer": false, + // "is_mobile": false, + // "is_smarttv": false, + // "is_tablet": false, + // "is_touchscreen": false, + // "is_tvplayer": false, + // "model": null, + // "name": null + // } + enum Slots { DeviceInfo, Count }; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool device_name_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool brand_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool model_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool hardware_type_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool is_gameconsole_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool is_mediaplayer_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool is_mobile_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool is_smarttv_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool is_tablet_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool is_desktop_get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool is_touchscreen_get(JSContext *cx, unsigned argc, JS::Value *vp); + + static bool toJSON(JSContext *cx, unsigned argc, JS::Value *vp); + + static bool lookup(JSContext *cx, unsigned argc, JS::Value *vp); + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + static JSObject *create(JSContext *cx, JS::HandleObject deviceInfo); + + static JSString *ToSource(JSContext *cx, JS::HandleObject self); +}; + +} // namespace fastly::device + +#endif diff --git a/runtime/fastly/builtins/dictionary.cpp b/runtime/fastly/builtins/dictionary.cpp new file mode 100644 index 0000000000..cabc8b97bc --- /dev/null +++ b/runtime/fastly/builtins/dictionary.cpp @@ -0,0 +1,161 @@ +#include "dictionary.h" +#include "../../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" +#include "fastly.h" + +using builtins::BuiltinImpl; +using fastly::fastly::FastlyGetErrorMessage; + +namespace fastly::dictionary { + +host_api::Dict Dictionary::dictionary_handle(JSObject *obj) { + JS::Value val = JS::GetReservedSlot(obj, Dictionary::Slots::Handle); + return host_api::Dict(val.toInt32()); +} + +bool Dictionary::get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + JS::HandleValue name_arg = args.get(0); + + // Convert into a String following https://tc39.es/ecma262/#sec-tostring + auto name = core::encode(cx, name_arg); + if (!name) { + return false; + } + + // If the converted string has a length of 0 then we throw an Error + // because Dictionary keys have to be at-least 1 character. + if (name.len == 0) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_DICTIONARY_KEY_EMPTY); + return false; + } + // key has to be less than 256 + if (name.len > 255) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_DICTIONARY_KEY_TOO_LONG); + return false; + } + + // Ensure that we throw an exception for all unexpected host errors. + auto res = Dictionary::dictionary_handle(self).get(name); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto ret = std::move(res.unwrap()); + if (!ret.has_value()) { + args.rval().setNull(); + return true; + } + + JS::RootedString text(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(ret->ptr.get(), ret->len))); + if (!text) + return false; + + args.rval().setString(text); + return true; +} + +const JSFunctionSpec Dictionary::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec Dictionary::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec Dictionary::methods[] = {JS_FN("get", get, 1, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec Dictionary::properties[] = {JS_PS_END}; + +bool Dictionary::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The Dictionary builtin"); + CTOR_HEADER("Dictionary", 1); + + JS::HandleValue name_arg = args.get(0); + + // Convert into a String following https://tc39.es/ecma262/#sec-tostring + auto name = core::encode(cx, name_arg); + if (!name) { + return false; + } + + // If the converted string has a length of 0 then we throw an Error + // because Dictionary names have to be at-least 1 character. + if (name.len == 0) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_DICTIONARY_NAME_EMPTY); + return false; + } + + // If the converted string has a length of more than 255 then we throw an Error + // because Dictionary names have to be less than 255 characters. + if (name.len > 255) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_DICTIONARY_NAME_TOO_LONG); + return false; + } + + // Name must start with ascii alphabetical and contain only ascii alphanumeric, underscore, and + // whitespace + std::string_view name_view = name; + if (!std::isalpha(name_view.front())) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_DICTIONARY_NAME_START_WITH_ASCII_ALPHA); + return false; + } + + auto is_valid_name = + std::all_of(std::next(name_view.begin(), 1), name_view.end(), [&](auto character) { + return std::isalnum(character) || character == '_' || character == ' '; + }); + + if (!is_valid_name) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_DICTIONARY_NAME_CONTAINS_INVALID_CHARACTER); + return false; + } + + JS::RootedObject dictionary(cx, JS_NewObjectForConstructor(cx, &class_, args)); + + auto res = host_api::Dict::open(name_view); + if (auto *err = res.to_err()) { + if (host_api::error_is_bad_handle(*err)) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_DICTIONARY_DOES_NOT_EXIST, + name_view.data()); + return false; + } else { + HANDLE_ERROR(cx, *err); + return false; + } + } + + auto dict = res.unwrap(); + JS::SetReservedSlot(dictionary, Dictionary::Slots::Handle, JS::Int32Value(dict.handle)); + if (!dictionary) { + return false; + } + + args.rval().setObject(*dictionary); + return true; +} + +bool install(api::Engine *engine) { + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + + RootedObject dictionary_ns_obj(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + RootedValue dictionary_ns_val(engine->cx(), JS::ObjectValue(*dictionary_ns_obj)); + RootedObject dictionary_obj(engine->cx(), JS_GetConstructor(engine->cx(), Dictionary::proto_obj)); + RootedValue dictionary_val(engine->cx(), ObjectValue(*dictionary_obj)); + if (!JS_SetProperty(engine->cx(), dictionary_ns_obj, "Dictionary", dictionary_val)) { + return false; + } + if (!engine->define_builtin_module("fastly:dictionary", dictionary_ns_val)) { + return false; + } + + return true; +} + +} // namespace fastly::dictionary diff --git a/runtime/fastly/builtins/dictionary.h b/runtime/fastly/builtins/dictionary.h new file mode 100644 index 0000000000..4857d68575 --- /dev/null +++ b/runtime/fastly/builtins/dictionary.h @@ -0,0 +1,29 @@ +#ifndef FASTLYE_DICTIONARY_H +#define FASTLYE_DICTIONARY_H + +#include "../host-api/host_api_fastly.h" +#include "builtin.h" +#include "extension-api.h" + +namespace fastly::dictionary { + +class Dictionary : public builtins::BuiltinImpl { +private: +public: + static constexpr const char *class_name = "Dictionary"; + static const int ctor_length = 1; + enum Slots { Handle, Count }; + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool get(JSContext *cx, unsigned argc, JS::Value *vp); + + static host_api::Dict dictionary_handle(JSObject *obj); + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); +}; + +} // namespace fastly::dictionary + +#endif diff --git a/runtime/fastly/builtins/edge-rate-limiter.cpp b/runtime/fastly/builtins/edge-rate-limiter.cpp new file mode 100644 index 0000000000..9f74b2fd70 --- /dev/null +++ b/runtime/fastly/builtins/edge-rate-limiter.cpp @@ -0,0 +1,499 @@ +#include "edge-rate-limiter.h" +#include "../../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" +#include "builtin.h" +#include "js/Result.h" +#include + +using builtins::BuiltinImpl; + +namespace fastly::edge_rate_limiter { + +JSString *PenaltyBox::get_name(JSObject *self) { + MOZ_ASSERT(is_instance(self)); + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString()); + + return JS::GetReservedSlot(self, Slots::Name).toString(); +} + +// add(entry: string, timeToLive: number): void; +bool PenaltyBox::add(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The PenaltyBox builtin"); + METHOD_HEADER(2); + + // Convert entry parameter into a string + auto entry = core::encode(cx, args.get(0)); + if (!entry) { + return false; + } + + // Convert timeToLive parameter into a number + double timeToLive; + if (!JS::ToNumber(cx, args.get(1), &timeToLive)) { + return false; + } + + // This needs to happen on the happy-path as these all end up being valid uint32_t values that the + // host-call accepts + if (std::isnan(timeToLive) || std::isinf(timeToLive) || timeToLive < 1 || timeToLive > 60) { + JS_ReportErrorASCII(cx, "add: timeToLive parameter is an invalid value, only numbers from 1 to " + "60 can be used for timeToLive values."); + return false; + } + + // We expose it in minutes as the value gets truncated to minutes however the host expects it in + // seconds + timeToLive = timeToLive * 60; + + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString()); + JS::RootedString name_val(cx, JS::GetReservedSlot(self, Slots::Name).toString()); + auto name = core::encode(cx, name_val); + if (!name) { + return false; + } + + auto res = host_api::PenaltyBox::add(name, entry, timeToLive); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + args.rval().setUndefined(); + return true; +} + +// has(entry: string): boolean; +bool PenaltyBox::has(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The PenaltyBox builtin"); + METHOD_HEADER(1); + + // Convert entry parameter into a string + auto entry = core::encode(cx, args.get(0)); + if (!entry) { + return false; + } + + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString()); + JS::RootedString name_val(cx, JS::GetReservedSlot(self, Slots::Name).toString()); + auto name = core::encode(cx, name_val); + if (!name) { + return false; + } + + auto res = host_api::PenaltyBox::has(name, entry); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + args.rval().setBoolean(res.unwrap()); + return true; +} + +const JSFunctionSpec PenaltyBox::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec PenaltyBox::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec PenaltyBox::methods[] = { + JS_FN("add", add, 2, JSPROP_ENUMERATE), + JS_FN("has", has, 1, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec PenaltyBox::properties[] = { + JS_STRING_SYM_PS(toStringTag, "PenaltyBox", JSPROP_READONLY), JS_PS_END}; + +// Open a penalty-box identified by the given name +// constructor(name: string); +bool PenaltyBox::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The PenaltyBox builtin"); + CTOR_HEADER("PenaltyBox", 1); + auto name = core::encode(cx, args.get(0)); + if (!name) { + return false; + } + + if (name.len == 0) { + JS_ReportErrorASCII(cx, "PenaltyBox constructor: name parameter can not be an empty string."); + } + + JS::RootedObject instance(cx, JS_NewObjectForConstructor(cx, &class_, args)); + if (!instance) { + return false; + } + JS::SetReservedSlot(instance, static_cast(Slots::Name), + JS::StringValue(JS_NewStringCopyZ(cx, name.begin()))); + args.rval().setObject(*instance); + return true; +} + +JSString *RateCounter::get_name(JSObject *self) { + MOZ_ASSERT(is_instance(self)); + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString()); + + return JS::GetReservedSlot(self, Slots::Name).toString(); +} + +// increment(entry: string, delta: number): void; +bool RateCounter::increment(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The RateCounter builtin"); + METHOD_HEADER(2); + + // Convert entry parameter into a string + auto entry = core::encode(cx, args.get(0)); + if (!entry) { + return false; + } + + // Convert delta parameter into a number + double delta; + if (!JS::ToNumber(cx, args.get(1), &delta)) { + return false; + } + + // This needs to happen on the happy-path as these all end up being valid uint32_t values that the + // host-call accepts + if (delta < 0 || std::isnan(delta) || std::isinf(delta)) { + JS_ReportErrorASCII(cx, + "increment: delta parameter is an invalid value, only positive numbers can " + "be used for delta values."); + return false; + } + + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString()); + JS::RootedString name_val(cx, JS::GetReservedSlot(self, Slots::Name).toString()); + auto name = core::encode(cx, name_val); + if (!name) { + return false; + } + + auto res = host_api::RateCounter::increment(name, entry, delta); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + args.rval().setUndefined(); + return true; +} + +// lookupRate(entry: string, window: [1, 10, 60]): number; +bool RateCounter::lookupRate(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The RateCounter builtin"); + METHOD_HEADER(2); + + // Convert entry parameter into a string + auto entry = core::encode(cx, args.get(0)); + if (!entry) { + return false; + } + + // Convert window parameter into a number + double window; + if (!JS::ToNumber(cx, args.get(1), &window)) { + return false; + } + + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString()); + JS::RootedString name_val(cx, JS::GetReservedSlot(self, Slots::Name).toString()); + auto name = core::encode(cx, name_val); + if (!name) { + return false; + } + + auto res = host_api::RateCounter::lookup_rate(name, entry, window); + if (auto *err = res.to_err()) { + if (host_api::error_is_generic(*err) || host_api::error_is_invalid_argument(*err)) { + if (window != 1 && window != 10 && window != 60) { + JS_ReportErrorASCII(cx, "lookupRate: window parameter must be either: 1, 10, or 60"); + return false; + } + } + HANDLE_ERROR(cx, *err); + return false; + } + + JS::RootedValue rate(cx, JS::NumberValue(res.unwrap())); + args.rval().set(rate); + return true; +} + +// lookupCount(entry: string, duration: [10, 20, 30, 40, 50, 60]): number; +bool RateCounter::lookupCount(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The RateCounter builtin"); + METHOD_HEADER(2); + + // Convert entry parameter into a string + auto entry = core::encode(cx, args.get(0)); + if (!entry) { + return false; + } + + // Convert duration parameter into a number + double duration; + if (!JS::ToNumber(cx, args.get(1), &duration)) { + return false; + } + + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::Name).isString()); + JS::RootedString name_val(cx, JS::GetReservedSlot(self, Slots::Name).toString()); + auto name = core::encode(cx, name_val); + if (!name) { + return false; + } + + auto res = host_api::RateCounter::lookup_count(name, entry, duration); + if (auto *err = res.to_err()) { + if (host_api::error_is_generic(*err) || host_api::error_is_invalid_argument(*err)) { + if (duration != 10 && duration != 20 && duration != 30 && duration != 40 && duration != 50 && + duration != 60) { + JS_ReportErrorASCII( + cx, "lookupCount: duration parameter must be either: 10, 20, 30, 40, 50, or 60"); + return false; + } + } + HANDLE_ERROR(cx, *err); + return false; + } + + JS::RootedValue rate(cx, JS::NumberValue(res.unwrap())); + args.rval().set(rate); + return true; +} + +const JSFunctionSpec RateCounter::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec RateCounter::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec RateCounter::methods[] = { + JS_FN("increment", increment, 2, JSPROP_ENUMERATE), + JS_FN("lookupRate", lookupRate, 2, JSPROP_ENUMERATE), + JS_FN("lookupCount", lookupCount, 2, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec RateCounter::properties[] = { + JS_STRING_SYM_PS(toStringTag, "RateCounter", JSPROP_READONLY), JS_PS_END}; + +// Open a RateCounter instance with the given name +// constructor(name: string); +bool RateCounter::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The RateCounter builtin"); + CTOR_HEADER("RateCounter", 1); + auto name = core::encode(cx, args.get(0)); + if (!name) { + return false; + } + + if (name.len == 0) { + JS_ReportErrorASCII(cx, "RateCounter constructor: name parameter can not be an empty string."); + } + + JS::RootedObject instance(cx, JS_NewObjectForConstructor(cx, &class_, args)); + if (!instance) { + return false; + } + JS::SetReservedSlot(instance, static_cast(Slots::Name), + JS::StringValue(JS_NewStringCopyZ(cx, name.begin()))); + args.rval().setObject(*instance); + return true; +} + +// checkRate(entry: string, delta: number, window: [1, 10, 60], limit: number, timeToLive: number): +// boolean; +bool EdgeRateLimiter::checkRate(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The EdgeRateLimiter builtin"); + METHOD_HEADER(5); + + // Convert entry parameter into a string + auto entry = core::encode(cx, args.get(0)); + if (!entry) { + return false; + } + + // Convert delta parameter into a number + double delta; + if (!JS::ToNumber(cx, args.get(1), &delta)) { + return false; + } + + if (delta < 0 || std::isnan(delta) || std::isinf(delta)) { + JS_ReportErrorASCII(cx, + "checkRate: delta parameter is an invalid value, only positive numbers can " + "be used for delta values."); + return false; + } + + // Convert window parameter into a number + double window; + if (!JS::ToNumber(cx, args.get(2), &window)) { + return false; + } + + if (window != 1 && window != 10 && window != 60) { + JS_ReportErrorASCII(cx, "checkRate: window parameter must be either: 1, 10, or 60"); + return false; + } + + // Convert limit parameter into a number + double limit; + if (!JS::ToNumber(cx, args.get(3), &limit)) { + return false; + } + + // This needs to happen on the happy-path as these all end up being valid uint32_t values that the + // host-call accepts + if (limit < 0 || std::isnan(limit) || std::isinf(limit)) { + JS_ReportErrorASCII(cx, + "checkRate: limit parameter is an invalid value, only positive numbers can " + "be used for limit values."); + return false; + } + + // Convert timeToLive parameter into a number + double timeToLive; + if (!JS::ToNumber(cx, args.get(4), &timeToLive)) { + return false; + } + + // This needs to happen on the happy-path as these all end up being valid uint32_t values that the + // host-call accepts + if (std::isnan(timeToLive) || std::isinf(timeToLive) || timeToLive < 1 || timeToLive > 60) { + JS_ReportErrorASCII( + cx, "checkRate: timeToLive parameter is an invalid value, only numbers from 1 to " + "60 can be used for timeToLive values."); + return false; + } + + // We expose it in minutes as the value gets truncated to minutes however the host expects it in + // seconds + timeToLive = timeToLive * 60; + + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::RateCounterName).isString()); + JS::RootedString rc_name_val(cx, JS::GetReservedSlot(self, Slots::RateCounterName).toString()); + auto rc_name = core::encode(cx, rc_name_val); + if (!rc_name) { + return false; + } + MOZ_ASSERT(JS::GetReservedSlot(self, Slots::PenaltyBoxName).isString()); + JS::RootedString pb_name_val(cx, JS::GetReservedSlot(self, Slots::PenaltyBoxName).toString()); + auto pb_name = core::encode(cx, pb_name_val); + if (!pb_name) { + return false; + } + + auto res = host_api::EdgeRateLimiter::check_rate(rc_name, entry, delta, window, limit, pb_name, + timeToLive); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + args.rval().setBoolean(res.unwrap()); + return true; +} + +const JSFunctionSpec EdgeRateLimiter::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec EdgeRateLimiter::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec EdgeRateLimiter::methods[] = { + JS_FN("checkRate", checkRate, 5, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec EdgeRateLimiter::properties[] = { + JS_STRING_SYM_PS(toStringTag, "EdgeRateLimiter", JSPROP_READONLY), JS_PS_END}; + +// Open a penalty-box identified by the given name +// constructor(name: string); +bool EdgeRateLimiter::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The EdgeRateLimiter builtin"); + CTOR_HEADER("EdgeRateLimiter", 2); + + auto rc = args.get(0); + if (!RateCounter::is_instance(rc)) { + JS_ReportErrorASCII( + cx, + "EdgeRateLimiter constructor: rateCounter parameter must be an instance of RateCounter"); + return false; + } + + auto rc_name = RateCounter::get_name(rc.toObjectOrNull()); + if (!rc_name) { + return false; + } + + auto pb = args.get(1); + if (!PenaltyBox::is_instance(pb)) { + JS_ReportErrorASCII( + cx, "EdgeRateLimiter constructor: penaltyBox parameter must be an instance of PenaltyBox"); + return false; + } + + auto pb_name = PenaltyBox::get_name(pb.toObjectOrNull()); + if (!pb_name) { + return false; + } + + JS::RootedObject instance(cx, JS_NewObjectForConstructor(cx, &class_, args)); + if (!instance) { + return false; + } + JS::SetReservedSlot(instance, static_cast(Slots::RateCounterName), + JS::StringValue(rc_name)); + + JS::SetReservedSlot(instance, static_cast(Slots::PenaltyBoxName), + JS::StringValue(pb_name)); + args.rval().setObject(*instance); + return true; +} + +bool install(api::Engine *engine) { + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + + RootedObject edge_rate_limiter_ns_obj(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + RootedValue edge_rate_limiter_ns_val(engine->cx(), JS::ObjectValue(*edge_rate_limiter_ns_obj)); + RootedObject edge_rate_limiter_obj(engine->cx(), + JS_GetConstructor(engine->cx(), EdgeRateLimiter::proto_obj)); + RootedValue edge_rate_limiter_val(engine->cx(), ObjectValue(*edge_rate_limiter_obj)); + if (!JS_SetProperty(engine->cx(), edge_rate_limiter_ns_obj, "EdgeRateLimiter", + edge_rate_limiter_val)) { + return false; + } + RootedObject penalty_box_obj(engine->cx(), + JS_GetConstructor(engine->cx(), PenaltyBox::proto_obj)); + RootedValue penalty_box_val(engine->cx(), ObjectValue(*penalty_box_obj)); + if (!JS_SetProperty(engine->cx(), edge_rate_limiter_ns_obj, "PenaltyBox", penalty_box_val)) { + return false; + } + RootedObject rate_counter_obj(engine->cx(), + JS_GetConstructor(engine->cx(), RateCounter::proto_obj)); + RootedValue rate_counter_val(engine->cx(), ObjectValue(*rate_counter_obj)); + if (!JS_SetProperty(engine->cx(), edge_rate_limiter_ns_obj, "RateCounter", rate_counter_val)) { + return false; + } + if (!engine->define_builtin_module("fastly:edge-rate-limiter", edge_rate_limiter_ns_val)) { + return false; + } + + return true; +} + +} // namespace fastly::edge_rate_limiter diff --git a/runtime/fastly/builtins/edge-rate-limiter.h b/runtime/fastly/builtins/edge-rate-limiter.h new file mode 100644 index 0000000000..0f15a99b0a --- /dev/null +++ b/runtime/fastly/builtins/edge-rate-limiter.h @@ -0,0 +1,66 @@ +#ifndef FASTLY_EDGE_RATE_LIMITER_H +#define FASTLY_EDGE_RATE_LIMITER_H + +#include "builtin.h" +#include "extension-api.h" + +namespace fastly::edge_rate_limiter { + +class PenaltyBox final : public builtins::BuiltinImpl { + static bool add(JSContext *cx, unsigned argc, JS::Value *vp); + static bool has(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "PenaltyBox"; + enum Slots { Name, Count }; + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static const unsigned ctor_length = 0; + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + + static JSString *get_name(JSObject *self); +}; + +class RateCounter final : public builtins::BuiltinImpl { + static bool increment(JSContext *cx, unsigned argc, JS::Value *vp); + static bool lookupRate(JSContext *cx, unsigned argc, JS::Value *vp); + static bool lookupCount(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "RateCounter"; + enum Slots { Name, Count }; + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static const unsigned ctor_length = 0; + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + + static JSString *get_name(JSObject *self); +}; + +class EdgeRateLimiter final : public builtins::BuiltinImpl { + static bool checkRate(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "EdgeRateLimiter"; + enum Slots { RateCounterName, PenaltyBoxName, Count }; + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static const unsigned ctor_length = 0; + + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); +}; + +} // namespace fastly::edge_rate_limiter + +#endif diff --git a/runtime/fastly/builtins/fastly.cpp b/runtime/fastly/builtins/fastly.cpp index fecb0384f9..1dbde90ce0 100644 --- a/runtime/fastly/builtins/fastly.cpp +++ b/runtime/fastly/builtins/fastly.cpp @@ -5,19 +5,26 @@ #include "js/experimental/TypedData.h" // used in "js/Conversions.h" #pragma clang diagnostic pop #include "../../StarlingMonkey/builtins/web/url.h" +#include "./fetch/request-response.h" #include "encode.h" #include "fastly.h" #include "js/Conversions.h" #include "js/JSON.h" +#include "logger.h" using builtins::web::url::URL; using builtins::web::url::URLSearchParams; using fastly::fastly::Fastly; +using fastly::fetch::RequestOrResponse; +using fastly::fetch::Response; +using fastly::logger::Logger; namespace { bool DEBUG_LOGGING_ENABLED = false; +api::Engine *ENGINE; + } // namespace bool debug_logging_enabled() { return DEBUG_LOGGING_ENABLED; } @@ -32,65 +39,29 @@ const JSErrorFormatString *FastlyGetErrorMessage(void *userRef, unsigned errorNu return nullptr; } -namespace { - -bool enableDebugLogging(JSContext *cx, unsigned argc, JS::Value *vp) { - JS::CallArgs args = CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, __func__, 1)) - return false; - DEBUG_LOGGING_ENABLED = JS::ToBoolean(args[0]); - args.rval().setUndefined(); - return true; -} - -} // namespace - JS::PersistentRooted Fastly::env; JS::PersistentRooted Fastly::baseURL; JS::PersistentRooted Fastly::defaultBackend; bool Fastly::allowDynamicBackends = false; -bool Fastly::version_get(JSContext *cx, unsigned argc, JS::Value *vp) { +bool Fastly::dump(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = CallArgsFromVp(argc, vp); - JS::RootedString version_str(cx, JS_NewStringCopyN(cx, RUNTIME_VERSION, strlen(RUNTIME_VERSION))); - args.rval().setString(version_str); - return true; -} - -bool Env::env_get(JSContext *cx, unsigned argc, JS::Value *vp) { - JS::CallArgs args = CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, "fastly.env.get", 1)) + if (!args.requireAtLeast(cx, __func__, 1)) return false; - auto var_name_chars = core::encode(cx, args[0]); - if (!var_name_chars) { - return false; - } - JS::RootedString env_var(cx, JS_NewStringCopyZ(cx, getenv(var_name_chars.begin()))); - if (!env_var) - return false; + ENGINE->dump_value(args[0], stdout); - args.rval().setString(env_var); + args.rval().setUndefined(); return true; } -const JSFunctionSpec Env::static_methods[] = { - JS_FS_END, -}; - -const JSPropertySpec Env::static_properties[] = { - JS_PS_END, -}; - -const JSFunctionSpec Env::methods[] = {JS_FN("get", env_get, 1, JSPROP_ENUMERATE), JS_FS_END}; - -const JSPropertySpec Env::properties[] = {JS_PS_END}; - -JSObject *Env::create(JSContext *cx) { - JS::RootedObject env(cx, JS_NewPlainObject(cx)); - if (!env || !JS_DefineFunctions(cx, env, methods)) - return nullptr; - return env; +bool Fastly::enableDebugLogging(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, __func__, 1)) + return false; + DEBUG_LOGGING_ENABLED = JS::ToBoolean(args[0]); + args.rval().setUndefined(); + return true; } bool Fastly::getGeolocationForIpAddress(JSContext *cx, unsigned argc, JS::Value *vp) { @@ -110,29 +81,28 @@ bool Fastly::getGeolocationForIpAddress(JSContext *cx, unsigned argc, JS::Value return JS_ParseJSON(cx, geo_info_str, args.rval()); } -// TODO(GB): reimplement -// // TODO(performance): consider allowing logger creation during initialization, but then throw -// // when trying to log. -// // https://github.com/fastly/js-compute-runtime/issues/225 -// bool Fastly::getLogger(JSContext *cx, unsigned argc, JS::Value *vp) { -// JS::CallArgs args = CallArgsFromVp(argc, vp); -// REQUEST_HANDLER_ONLY("fastly.getLogger"); -// JS::RootedObject self(cx, &args.thisv().toObject()); -// if (!args.requireAtLeast(cx, "fastly.getLogger", 1)) -// return false; - -// auto name = core::encode(cx, args[0]); -// if (!name) -// return false; - -// JS::RootedObject logger(cx, builtins::Logger::create(cx, name.begin())); -// if (!logger) { -// return false; -// } - -// args.rval().setObject(*logger); -// return true; -// } +// TODO(performance): consider allowing logger creation during initialization, but then throw +// when trying to log. +// https://github.com/fastly/js-compute-runtime/issues/225 +bool Fastly::getLogger(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + REQUEST_HANDLER_ONLY("fastly.getLogger"); + JS::RootedObject self(cx, &args.thisv().toObject()); + if (!args.requireAtLeast(cx, "fastly.getLogger", 1)) + return false; + + auto name = core::encode(cx, args[0]); + if (!name) + return false; + + JS::RootedObject logger(cx, Logger::create(cx, name.begin())); + if (!logger) { + return false; + } + + args.rval().setObject(*logger); + return true; +} bool Fastly::includeBytes(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = CallArgsFromVp(argc, vp); @@ -175,69 +145,65 @@ bool Fastly::includeBytes(JSContext *cx, unsigned argc, JS::Value *vp) { return true; } -// TODO(GB): reimplement -// bool Fastly::createFanoutHandoff(JSContext *cx, unsigned argc, JS::Value *vp) { -// JS::CallArgs args = CallArgsFromVp(argc, vp); -// REQUEST_HANDLER_ONLY("createFanoutHandoff"); -// if (!args.requireAtLeast(cx, "createFanoutHandoff", 2)) { -// return false; -// } - -// auto request_value = args.get(0); -// if (!Request::is_instance(request_value)) { -// JS_ReportErrorUTF8(cx, "createFanoutHandoff: request parameter must be an instance of -// Request"); return false; -// } - -// auto response_handle = host_api::HttpResp::make(); -// if (auto *err = response_handle.to_err()) { -// HANDLE_ERROR(cx, *err); -// return false; -// } -// auto body_handle = host_api::HttpBody::make(); -// if (auto *err = body_handle.to_err()) { -// HANDLE_ERROR(cx, *err); -// return false; -// } - -// JS::RootedObject response_instance(cx, JS_NewObjectWithGivenProto(cx, -// &builtins::Response::class_, -// builtins::Response::proto_obj)); -// if (!response_instance) { -// return false; -// } - -// auto backend_value = args.get(1); -// auto backend_chars = core::encode(cx, backend_value); -// if (!backend_chars) { -// return false; -// } -// if (backend_chars.len == 0) { -// JS_ReportErrorUTF8(cx, "createFanoutHandoff: Backend parameter can not be an empty string"); -// return false; -// } - -// if (backend_chars.len > 254) { -// JS_ReportErrorUTF8(cx, "createFanoutHandoff: name can not be more than 254 characters"); -// return false; -// } - -// bool is_upstream = true; -// bool is_grip_upgrade = true; -// JS::RootedObject response( -// cx, builtins::Response::create(cx, response_instance, response_handle.unwrap(), -// body_handle.unwrap(), is_upstream, is_grip_upgrade, -// std::move(backend_chars.ptr))); -// if (!response) { -// return false; -// } - -// builtins::RequestOrResponse::set_url(response, -// builtins::RequestOrResponse::url(&request_value.toObject())); -// args.rval().setObject(*response); - -// return true; -// } +bool Fastly::createFanoutHandoff(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + REQUEST_HANDLER_ONLY("createFanoutHandoff"); + if (!args.requireAtLeast(cx, "createFanoutHandoff", 2)) { + return false; + } + + auto request_value = args.get(0); + if (!Request::is_instance(request_value)) { + JS_ReportErrorUTF8(cx, "createFanoutHandoff: request parameter must be an instance of Request"); + return false; + } + + auto response_handle = host_api::HttpResp::make(); + if (auto *err = response_handle.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + auto body_handle = host_api::HttpBody::make(); + if (auto *err = body_handle.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + JS::RootedObject response_instance( + cx, JS_NewObjectWithGivenProto(cx, &Response::class_, Response::proto_obj)); + if (!response_instance) { + return false; + } + + auto backend_value = args.get(1); + auto backend_chars = core::encode(cx, backend_value); + if (!backend_chars) { + return false; + } + if (backend_chars.len == 0) { + JS_ReportErrorUTF8(cx, "createFanoutHandoff: Backend parameter can not be an empty string"); + return false; + } + + if (backend_chars.len > 254) { + JS_ReportErrorUTF8(cx, "createFanoutHandoff: name can not be more than 254 characters"); + return false; + } + + bool is_upstream = true; + bool is_grip_upgrade = true; + JS::RootedObject response(cx, Response::create(cx, response_instance, response_handle.unwrap(), + body_handle.unwrap(), is_upstream, is_grip_upgrade, + std::move(backend_chars.ptr))); + if (!response) { + return false; + } + + RequestOrResponse::set_url(response, RequestOrResponse::url(&request_value.toObject())); + args.rval().setObject(*response); + + return true; +} bool Fastly::now(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = CallArgsFromVp(argc, vp); @@ -251,6 +217,49 @@ bool Fastly::env_get(JSContext *cx, unsigned argc, JS::Value *vp) { return true; } +bool Env::env_get(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "fastly.env.get", 1)) + return false; + + auto var_name_chars = core::encode(cx, args[0]); + if (!var_name_chars) { + return false; + } + JS::RootedString env_var(cx, JS_NewStringCopyZ(cx, getenv(var_name_chars.begin()))); + if (!env_var) + return false; + + args.rval().setString(env_var); + return true; +} + +const JSFunctionSpec Env::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec Env::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec Env::methods[] = {JS_FN("get", env_get, 1, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec Env::properties[] = {JS_PS_END}; + +JSObject *Env::create(JSContext *cx) { + JS::RootedObject env(cx, JS_NewPlainObject(cx)); + if (!env || !JS_DefineFunctions(cx, env, methods)) + return nullptr; + return env; +} + +bool Fastly::version_get(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + JS::RootedString version_str(cx, JS_NewStringCopyN(cx, RUNTIME_VERSION, strlen(RUNTIME_VERSION))); + args.rval().setString(version_str); + return true; +} + bool Fastly::baseURL_get(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = CallArgsFromVp(argc, vp); args.rval().setObjectOrNull(baseURL); @@ -313,6 +322,8 @@ const JSPropertySpec Fastly::properties[] = { JS_PS_END}; bool install(api::Engine *engine) { + ENGINE = engine; + bool ENABLE_EXPERIMENTAL_HIGH_RESOLUTION_TIME_METHODS = std::string(std::getenv("ENABLE_EXPERIMENTAL_HIGH_RESOLUTION_TIME_METHODS")) == "1"; @@ -337,16 +348,14 @@ bool install(api::Engine *engine) { JSFunctionSpec end = JS_FS_END; const JSFunctionSpec methods[] = { - // TODO(GB): reimplement - // JS_FN("dump", dump, 1, 0), - JS_FN("enableDebugLogging", enableDebugLogging, 1, JSPROP_ENUMERATE), + JS_FN("dump", Fastly::dump, 1, 0), + JS_FN("enableDebugLogging", Fastly::enableDebugLogging, 1, JSPROP_ENUMERATE), JS_FN("getGeolocationForIpAddress", Fastly::getGeolocationForIpAddress, 1, JSPROP_ENUMERATE), - // TODO(GB): reimplement - // JS_FN("getLogger", getLogger, 1, JSPROP_ENUMERATE), + JS_FN("getLogger", Fastly::getLogger, 1, JSPROP_ENUMERATE), JS_FN("includeBytes", Fastly::includeBytes, 1, JSPROP_ENUMERATE), - // TODO(GB): reimplement - // JS_FN("createFanoutHandoff", createFanoutHandoff, 2, JSPROP_ENUMERATE), - ENABLE_EXPERIMENTAL_HIGH_RESOLUTION_TIME_METHODS ? nowfn : end, end}; + JS_FN("createFanoutHandoff", Fastly::createFanoutHandoff, 2, JSPROP_ENUMERATE), + ENABLE_EXPERIMENTAL_HIGH_RESOLUTION_TIME_METHODS ? nowfn : end, + end}; if (!JS_DefineFunctions(engine->cx(), fastly, methods) || !JS_DefineProperties(engine->cx(), fastly, Fastly::properties)) { @@ -370,7 +379,11 @@ bool install(api::Engine *engine) { // fastly:experimental RootedObject experimental(engine->cx(), JS_NewObject(engine->cx(), nullptr)); RootedValue experimental_val(engine->cx(), JS::ObjectValue(*experimental)); - if (!JS_SetProperty(engine->cx(), experimental, "includeBytes", experimental_val)) { + RootedValue include_bytes_val(engine->cx()); + if (!JS_GetProperty(engine->cx(), fastly, "includeBytes", &include_bytes_val)) { + return false; + } + if (!JS_SetProperty(engine->cx(), experimental, "includeBytes", include_bytes_val)) { return false; } auto set_default_backend = @@ -414,92 +427,17 @@ bool install(api::Engine *engine) { if (!engine->define_builtin_module("fastly:geolocation", geo_builtin_val)) { return false; } - - // fastly:cache - // TODO(GB): move this into core-cache when that is ported - RootedObject cache(engine->cx(), JS_NewObject(engine->cx(), nullptr)); - RootedValue cache_val(engine->cx(), JS::ObjectValue(*cache)); - // TODO(GB): Implement core cache (placeholders used for now) - if (!JS_SetProperty(engine->cx(), cache, "CoreCache", cache_val)) { - return false; - } - if (!JS_SetProperty(engine->cx(), cache, "CacheEntry", cache_val)) { - return false; - } - if (!JS_SetProperty(engine->cx(), cache, "CacheState", cache_val)) { - return false; - } - if (!JS_SetProperty(engine->cx(), cache, "TransactionCacheEntry", cache_val)) { - return false; - } - RootedValue simple_cache_val(engine->cx()); - if (!JS_GetProperty(engine->cx(), engine->global(), "SimpleCache", &simple_cache_val)) { - return false; - } - if (!JS_SetProperty(engine->cx(), cache, "SimpleCache", simple_cache_val)) { - return false; - } - RootedValue simple_cache_entry_val(engine->cx()); - if (!JS_GetProperty(engine->cx(), engine->global(), "SimpleCacheEntry", - &simple_cache_entry_val)) { - return false; - } - if (!JS_SetProperty(engine->cx(), cache, "SimpleCacheEntry", simple_cache_entry_val)) { - return false; - } - if (!engine->define_builtin_module("fastly:cache", cache_val)) { - return false; - } - - // TODO(GB): all of the following builtin modules are just placeholder shapes for now - if (!engine->define_builtin_module("fastly:body", env_builtin_val)) { - return false; - } - if (!engine->define_builtin_module("fastly:config-store", env_builtin_val)) { - return false; - } - RootedObject device_device(engine->cx(), JS_NewObject(engine->cx(), nullptr)); - RootedValue device_device_val(engine->cx(), JS::ObjectValue(*device_device)); - if (!JS_SetProperty(engine->cx(), device_device, "Device", device_device_val)) { - return false; - } - if (!engine->define_builtin_module("fastly:device", device_device_val)) { - return false; - } - RootedObject dictionary(engine->cx(), JS_NewObject(engine->cx(), nullptr)); - RootedValue dictionary_val(engine->cx(), JS::ObjectValue(*dictionary)); - if (!JS_SetProperty(engine->cx(), dictionary, "Dictionary", dictionary_val)) { - return false; - } - if (!engine->define_builtin_module("fastly:dictionary", dictionary_val)) { - return false; - } - RootedObject edge_rate_limiter(engine->cx(), JS_NewObject(engine->cx(), nullptr)); - RootedValue edge_rate_limiter_val(engine->cx(), JS::ObjectValue(*edge_rate_limiter)); - if (!JS_SetProperty(engine->cx(), edge_rate_limiter, "RateCounter", edge_rate_limiter_val)) { - return false; - } - if (!JS_SetProperty(engine->cx(), edge_rate_limiter, "PenaltyBox", edge_rate_limiter_val)) { - return false; - } - if (!JS_SetProperty(engine->cx(), edge_rate_limiter, "EdgeRateLimiter", edge_rate_limiter_val)) { - return false; - } - if (!engine->define_builtin_module("fastly:edge-rate-limiter", edge_rate_limiter_val)) { - return false; - } + // fastly:fanout RootedObject fanout(engine->cx(), JS_NewObject(engine->cx(), nullptr)); RootedValue fanout_val(engine->cx(), JS::ObjectValue(*fanout)); - if (!JS_SetProperty(engine->cx(), fanout, "createFanoutHandoff", fanout_val)) { + RootedValue create_fanout_handoff_val(engine->cx()); + if (!JS_GetProperty(engine->cx(), fastly, "createFanoutHandoff", &create_fanout_handoff_val)) { return false; } - if (!engine->define_builtin_module("fastly:fanout", fanout_val)) { + if (!JS_SetProperty(engine->cx(), fanout, "createFanoutHandoff", create_fanout_handoff_val)) { return false; } - if (!engine->define_builtin_module("fastly:logger", env_builtin_val)) { - return false; - } - if (!engine->define_builtin_module("fastly:secret-store", env_builtin_val)) { + if (!engine->define_builtin_module("fastly:fanout", fanout_val)) { return false; } diff --git a/runtime/fastly/builtins/fastly.h b/runtime/fastly/builtins/fastly.h index 57782a3df1..62fcf4d62a 100644 --- a/runtime/fastly/builtins/fastly.h +++ b/runtime/fastly/builtins/fastly.h @@ -9,13 +9,11 @@ #include "fastly.h" #include "host_api.h" -using namespace builtins; - namespace fastly::fastly { #define RUNTIME_VERSION "starlingmonkey-dev" -class Env : public BuiltinNoConstructor { +class Env : public builtins::BuiltinNoConstructor { private: static bool env_get(JSContext *cx, unsigned argc, JS::Value *vp); @@ -32,10 +30,9 @@ class Env : public BuiltinNoConstructor { const JSErrorFormatString *FastlyGetErrorMessage(void *userRef, unsigned errorNumber); -class Fastly : public BuiltinNoConstructor { +class Fastly : public builtins::BuiltinNoConstructor { private: - // TODO(GB): reimplement - // static bool log(JSContext *cx, unsigned argc, JS::Value *vp); + static bool log(JSContext *cx, unsigned argc, JS::Value *vp); public: static constexpr const char *class_name = "Fastly"; @@ -47,15 +44,12 @@ class Fastly : public BuiltinNoConstructor { static const JSPropertySpec properties[]; - // TODO(GB): reimplement - // static bool createFanoutHandoff(JSContext *cx, unsigned argc, JS::Value *vp); + static bool createFanoutHandoff(JSContext *cx, unsigned argc, JS::Value *vp); static bool now(JSContext *cx, unsigned argc, JS::Value *vp); - // TODO(GB): reimplement - // static bool dump(JSContext *cx, unsigned argc, JS::Value *vp); - // static bool enableDebugLogging(JSContext *cx, unsigned argc, JS::Value *vp); + static bool dump(JSContext *cx, unsigned argc, JS::Value *vp); + static bool enableDebugLogging(JSContext *cx, unsigned argc, JS::Value *vp); static bool getGeolocationForIpAddress(JSContext *cx, unsigned argc, JS::Value *vp); - // TODO(GB): reimplement - // static bool getLogger(JSContext *cx, unsigned argc, JS::Value *vp); + static bool getLogger(JSContext *cx, unsigned argc, JS::Value *vp); static bool includeBytes(JSContext *cx, unsigned argc, JS::Value *vp); static bool version_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool env_get(JSContext *cx, unsigned argc, JS::Value *vp); diff --git a/runtime/fastly/builtins/fetch-event.h b/runtime/fastly/builtins/fetch-event.h index cc8a358138..cdb5ea854c 100644 --- a/runtime/fastly/builtins/fetch-event.h +++ b/runtime/fastly/builtins/fetch-event.h @@ -6,11 +6,9 @@ #include "extension-api.h" #include "host_api.h" -using builtins::BuiltinNoConstructor; - namespace fastly::fetch_event { -class ClientInfo final : public BuiltinNoConstructor { +class ClientInfo final : public builtins::BuiltinNoConstructor { static bool address_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool geo_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool tls_cipher_openssl_name_get(JSContext *cx, unsigned argc, JS::Value *vp); @@ -42,7 +40,7 @@ class ClientInfo final : public BuiltinNoConstructor { void dispatch_fetch_event(HandleObject event, double *total_compute); -class FetchEvent final : public BuiltinNoConstructor { +class FetchEvent final : public builtins::BuiltinNoConstructor { static bool respondWith(JSContext *cx, unsigned argc, JS::Value *vp); static bool client_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool request_get(JSContext *cx, unsigned argc, JS::Value *vp); diff --git a/runtime/fastly/builtins/fetch/fetch.cpp b/runtime/fastly/builtins/fetch/fetch.cpp index 9feb4a49bb..f23f84dc7b 100644 --- a/runtime/fastly/builtins/fetch/fetch.cpp +++ b/runtime/fastly/builtins/fetch/fetch.cpp @@ -110,9 +110,9 @@ bool fetch(JSContext *cx, unsigned argc, Value *vp) { if (!response_promise) return ReturnPromiseRejectedWithPendingError(cx, args); - // if (!Request::apply_cache_override(cx, request)) { - // return false; - // } + if (!Request::apply_cache_override(cx, request)) { + return false; + } if (!Request::apply_auto_decompress_gzip(cx, request)) { return false; diff --git a/runtime/fastly/builtins/fetch/headers.h b/runtime/fastly/builtins/fetch/headers.h index 36790ab4e9..3f6b674593 100644 --- a/runtime/fastly/builtins/fetch/headers.h +++ b/runtime/fastly/builtins/fetch/headers.h @@ -3,11 +3,9 @@ #include "builtin.h" -using builtins::BuiltinImpl; - namespace fastly::fetch { -class Headers final : public BuiltinImpl { +class Headers final : public builtins::BuiltinImpl { static bool get(JSContext *cx, unsigned argc, JS::Value *vp); static bool set(JSContext *cx, unsigned argc, JS::Value *vp); static bool has(JSContext *cx, unsigned argc, JS::Value *vp); diff --git a/runtime/fastly/builtins/fetch/request-response.h b/runtime/fastly/builtins/fetch/request-response.h index e58fea9a99..8c3385d7e7 100644 --- a/runtime/fastly/builtins/fetch/request-response.h +++ b/runtime/fastly/builtins/fetch/request-response.h @@ -100,7 +100,7 @@ class RequestOrResponse final { bool create_if_undefined); }; -class Request final : public BuiltinImpl { +class Request final : public builtins::BuiltinImpl { static bool method_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool headers_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool url_get(JSContext *cx, unsigned argc, JS::Value *vp); @@ -172,7 +172,7 @@ class Request final : public BuiltinImpl { static JSObject *create_instance(JSContext *cx); }; -class Response final : public BuiltinImpl { +class Response final : public builtins::BuiltinImpl { static bool waitUntil(JSContext *cx, unsigned argc, JS::Value *vp); static bool ok_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool status_get(JSContext *cx, unsigned argc, JS::Value *vp); diff --git a/runtime/fastly/builtins/kv-store.cpp b/runtime/fastly/builtins/kv-store.cpp index a5ee74aef3..66b22a844e 100644 --- a/runtime/fastly/builtins/kv-store.cpp +++ b/runtime/fastly/builtins/kv-store.cpp @@ -22,9 +22,11 @@ #include "builtin.h" #include "kv-store.h" +using builtins::BuiltinImpl; using builtins::web::streams::NativeStreamSource; using fastly::fastly::convertBodyInit; using fastly::fastly::FastlyGetErrorMessage; +using fastly::fetch::RequestOrResponse; namespace fastly::kv_store { @@ -233,7 +235,6 @@ bool KVStore::delete_(JSContext *cx, unsigned argc, JS::Value *vp) { auto task = new FastlyAsyncTask(handle, ENGINE->cx(), self, result_promise, KVStore::process_pending_kv_store_delete); - ENGINE->queue_async_task(task); args.rval().setObject(*result_promise); diff --git a/runtime/fastly/builtins/kv-store.h b/runtime/fastly/builtins/kv-store.h index 318cd2b534..e759457415 100644 --- a/runtime/fastly/builtins/kv-store.h +++ b/runtime/fastly/builtins/kv-store.h @@ -1,16 +1,14 @@ -#ifndef JS_COMPUTE_RUNTIME_KV_STORE_H -#define JS_COMPUTE_RUNTIME_KV_STORE_H +#ifndef FASTLY_KV_STORE_H +#define FASTLY_KV_STORE_H #include "../host-api/host_api_fastly.h" #include "./fetch/request-response.h" #include "builtin.h" -using fastly::fetch::RequestOrResponse; - namespace fastly::kv_store { -class KVStoreEntry final : public BuiltinImpl { - template +class KVStoreEntry final : public builtins::BuiltinImpl { + template static bool bodyAll(JSContext *cx, unsigned argc, JS::Value *vp); static bool body_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool bodyUsed_get(JSContext *cx, unsigned argc, JS::Value *vp); @@ -18,7 +16,7 @@ class KVStoreEntry final : public BuiltinImpl { public: static constexpr const char *class_name = "KVStoreEntry"; - using Slots = RequestOrResponse::Slots; + using Slots = fetch::RequestOrResponse::Slots; static const JSFunctionSpec static_methods[]; static const JSPropertySpec static_properties[]; static const JSFunctionSpec methods[]; @@ -31,7 +29,7 @@ class KVStoreEntry final : public BuiltinImpl { static JSObject *create(JSContext *cx, host_api::HttpBody body_handle); }; -class KVStore final : public BuiltinImpl { +class KVStore final : public builtins::BuiltinImpl { static bool delete_(JSContext *cx, unsigned argc, JS::Value *vp); static bool get(JSContext *cx, unsigned argc, JS::Value *vp); static bool put(JSContext *cx, unsigned argc, JS::Value *vp); @@ -49,7 +47,6 @@ class KVStore final : public BuiltinImpl { static const unsigned ctor_length = 1; - static bool init_class(JSContext *cx, JS::HandleObject global); static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); static bool process_pending_kv_store_lookup(FastlyHandle handle, JS::HandleObject context, JS::HandleObject promise); diff --git a/runtime/fastly/builtins/logger.cpp b/runtime/fastly/builtins/logger.cpp new file mode 100644 index 0000000000..fdd54b80f4 --- /dev/null +++ b/runtime/fastly/builtins/logger.cpp @@ -0,0 +1,94 @@ +#include "logger.h" +#include "../../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" + +using builtins::BuiltinImpl; + +namespace fastly::logger { + +bool Logger::log(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + host_api::LogEndpoint endpoint(JS::GetReservedSlot(self, Logger::Slots::Endpoint).toInt32()); + + auto msg = core::encode(cx, args.get(0)); + if (!msg) { + return false; + } + + auto res = endpoint.write(msg); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + args.rval().setUndefined(); + return true; +} + +const JSFunctionSpec Logger::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec Logger::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec Logger::methods[] = {JS_FN("log", log, 1, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec Logger::properties[] = {JS_PS_END}; + +JSObject *Logger::create(JSContext *cx, const char *name) { + JS::RootedObject logger(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + if (!logger) { + return nullptr; + } + + auto res = host_api::LogEndpoint::get(std::string_view{name, strlen(name)}); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return nullptr; + } + + JS::SetReservedSlot(logger, Slots::Endpoint, JS::Int32Value(res.unwrap().handle)); + + return logger; +} + +bool Logger::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The Logger builtin"); + CTOR_HEADER("Logger", 1); + + auto name = core::encode(cx, args[0]); + auto handle_res = host_api::LogEndpoint::get(name); + if (auto *err = handle_res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + JS::RootedObject logger(cx, JS_NewObjectForConstructor(cx, &class_, args)); + JS::SetReservedSlot(logger, Slots::Endpoint, JS::Int32Value(handle_res.unwrap().handle)); + args.rval().setObject(*logger); + return true; +} + +bool install(api::Engine *engine) { + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + + RootedObject logger_ns_obj(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + RootedValue logger_ns_val(engine->cx(), JS::ObjectValue(*logger_ns_obj)); + RootedObject logger_obj(engine->cx(), JS_GetConstructor(engine->cx(), Logger::proto_obj)); + RootedValue logger_val(engine->cx(), ObjectValue(*logger_obj)); + if (!JS_SetProperty(engine->cx(), logger_ns_obj, "Logger", logger_val)) { + return false; + } + if (!engine->define_builtin_module("fastly:logger", logger_ns_val)) { + return false; + } + + return true; +} + +} // namespace fastly::logger diff --git a/runtime/fastly/builtins/logger.h b/runtime/fastly/builtins/logger.h new file mode 100644 index 0000000000..ad00881c6e --- /dev/null +++ b/runtime/fastly/builtins/logger.h @@ -0,0 +1,29 @@ +#ifndef FASTLY_LOGGER_H +#define FASTLY_LOGGER_H + +#include "builtin.h" +#include "extension-api.h" + +namespace fastly::logger { + +class Logger : public builtins::BuiltinImpl { +private: + static bool log(JSContext *cx, unsigned argc, JS::Value *vp); + +public: + static constexpr const char *class_name = "Logger"; + static const int ctor_length = 1; + + enum Slots { Endpoint, Count }; + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static JSObject *create(JSContext *cx, const char *name); + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); +}; + +} // namespace fastly::logger + +#endif diff --git a/runtime/fastly/builtins/secret-store.cpp b/runtime/fastly/builtins/secret-store.cpp new file mode 100644 index 0000000000..30d4af17e7 --- /dev/null +++ b/runtime/fastly/builtins/secret-store.cpp @@ -0,0 +1,238 @@ +#include "secret-store.h" +#include "../../../StarlingMonkey/runtime/encode.h" +#include "../host-api/host_api_fastly.h" +#include "fastly.h" + +using builtins::BuiltinImpl; +using fastly::fastly::FastlyGetErrorMessage; + +namespace fastly::secret_store { + +host_api::Secret SecretStoreEntry::secret_handle(JSObject *obj) { + JS::Value val = JS::GetReservedSlot(obj, SecretStoreEntry::Slots::Handle); + return host_api::Secret(val.toInt32()); +} + +bool SecretStoreEntry::plaintext(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(0) + + // Ensure that we throw an exception for all unexpected host errors. + auto res = SecretStoreEntry::secret_handle(self).plaintext(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + auto ret = std::move(res.unwrap()); + if (!ret.has_value()) { + return false; + } + + JS::RootedString text(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(ret->begin(), ret->size()))); + if (!text) { + return false; + } + + args.rval().setString(text); + return true; +} + +const JSFunctionSpec SecretStoreEntry::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec SecretStoreEntry::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec SecretStoreEntry::methods[] = { + JS_FN("plaintext", plaintext, 0, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec SecretStoreEntry::properties[] = {JS_PS_END}; + +bool SecretStoreEntry::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + JS_ReportErrorUTF8(cx, "SecretStoreEntry can't be instantiated directly"); + return false; +} + +JSObject *SecretStoreEntry::create(JSContext *cx, host_api::Secret secret) { + JS::RootedObject entry( + cx, JS_NewObjectWithGivenProto(cx, &SecretStoreEntry::class_, SecretStoreEntry::proto_obj)); + if (!entry) { + return nullptr; + } + + JS::SetReservedSlot(entry, Slots::Handle, JS::Int32Value(secret.handle)); + + return entry; +} + +host_api::SecretStore SecretStore::secret_store_handle(JSObject *obj) { + JS::Value val = JS::GetReservedSlot(obj, SecretStore::Slots::Handle); + return host_api::SecretStore(val.toInt32()); +} + +bool SecretStore::get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + JS::RootedObject result_promise(cx, JS::NewPromiseObject(cx, nullptr)); + if (!result_promise) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + auto key = core::encode(cx, args[0]); + if (!key) { + return false; + } + // If the converted string has a length of 0 then we throw an Error + // because keys have to be at-least 1 character. + if (key.len == 0) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_SECRET_STORE_KEY_EMPTY); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // key has to be less than 256 + if (key.len > 255) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_SECRET_STORE_KEY_TOO_LONG); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // key must contain only letters, numbers, dashes (-), underscores (_), and periods (.). + auto is_valid_key = std::all_of(key.begin(), key.end(), [&](auto character) { + return std::isalnum(character) || character == '_' || character == '-' || character == '.'; + }); + + if (!is_valid_key) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_SECRET_STORE_KEY_CONTAINS_INVALID_CHARACTER); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // Ensure that we throw an exception for all unexpected host errors. + auto get_res = SecretStore::secret_store_handle(self).get(key); + if (auto *err = get_res.to_err()) { + HANDLE_ERROR(cx, *err); + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + // When no entry is found, we are going to resolve the Promise with `null`. + auto secret = get_res.unwrap(); + if (!secret.has_value()) { + JS::RootedValue result(cx); + result.setNull(); + JS::ResolvePromise(cx, result_promise, result); + } else { + JS::RootedObject entry(cx, SecretStoreEntry::create(cx, *secret)); + if (!entry) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + JS::RootedValue result(cx); + result.setObject(*entry); + JS::ResolvePromise(cx, result_promise, result); + } + + args.rval().setObject(*result_promise); + + return true; +} + +const JSFunctionSpec SecretStore::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec SecretStore::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec SecretStore::methods[] = {JS_FN("get", get, 1, JSPROP_ENUMERATE), JS_FS_END}; + +const JSPropertySpec SecretStore::properties[] = {JS_PS_END}; + +bool SecretStore::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + REQUEST_HANDLER_ONLY("The SecretStore builtin"); + CTOR_HEADER("SecretStore", 1); + + auto name = core::encode(cx, args[0]); + if (!name) { + return false; + } + + // If the converted string has a length of 0 then we throw an Error + // because names have to be at-least 1 character. + if (name.len == 0) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_SECRET_STORE_NAME_EMPTY); + return false; + } + + // If the converted string has a length of more than 255 then we throw an Error + // because names have to be less than 255 characters. + if (name.len > 255) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_SECRET_STORE_NAME_TOO_LONG); + return false; + } + + // Name must contain only letters, numbers, dashes (-), underscores (_), and periods (.). + auto is_valid_name = std::all_of(name.begin(), name.end(), [&](auto character) { + return std::isalnum(character) || character == '_' || character == '-' || character == '.'; + }); + + if (!is_valid_name) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_SECRET_STORE_NAME_CONTAINS_INVALID_CHARACTER); + return false; + } + + JS::RootedObject secret_store(cx, JS_NewObjectForConstructor(cx, &class_, args)); + if (!secret_store) { + return false; + } + + auto res = host_api::SecretStore::open(name); + if (auto *err = res.to_err()) { + if (host_api::error_is_optional_none(*err)) { + JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, + JSMSG_SECRET_STORE_DOES_NOT_EXIST, name.begin()); + return false; + } else { + HANDLE_ERROR(cx, *err); + return false; + } + } + + JS::SetReservedSlot(secret_store, SecretStore::Slots::Handle, + JS::Int32Value(res.unwrap().handle)); + args.rval().setObject(*secret_store); + return true; +} + +bool install(api::Engine *engine) { + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + if (!BuiltinImpl::init_class_impl(engine->cx(), engine->global())) { + return false; + } + + RootedObject secret_store_ns_obj(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + RootedValue secret_store_ns_val(engine->cx(), JS::ObjectValue(*secret_store_ns_obj)); + RootedObject secret_store_obj(engine->cx(), + JS_GetConstructor(engine->cx(), SecretStore::proto_obj)); + RootedValue secret_store_val(engine->cx(), ObjectValue(*secret_store_obj)); + if (!JS_SetProperty(engine->cx(), secret_store_ns_obj, "SecretStore", secret_store_val)) { + return false; + } + RootedObject secret_store_entry_obj(engine->cx(), + JS_GetConstructor(engine->cx(), SecretStoreEntry::proto_obj)); + RootedValue secret_store_entry_val(engine->cx(), ObjectValue(*secret_store_entry_obj)); + if (!JS_SetProperty(engine->cx(), secret_store_ns_obj, "SecretStoreEntry", + secret_store_entry_val)) { + return false; + } + if (!engine->define_builtin_module("fastly:secret-store", secret_store_ns_val)) { + return false; + } + + return true; +} + +} // namespace fastly::secret_store diff --git a/runtime/fastly/builtins/secret-store.h b/runtime/fastly/builtins/secret-store.h new file mode 100644 index 0000000000..5138f442a3 --- /dev/null +++ b/runtime/fastly/builtins/secret-store.h @@ -0,0 +1,47 @@ +#ifndef FASTLY_SECRET_STORE_H +#define FASTLY_SECRET_STORE_H + +#include "../host-api/host_api_fastly.h" +#include "builtin.h" +#include "extension-api.h" + +namespace fastly::secret_store { + +class SecretStoreEntry : public builtins::BuiltinImpl { +private: +public: + static constexpr const char *class_name = "SecretStoreEntry"; + static const int ctor_length = 0; + enum Slots { Handle, Count }; + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool plaintext(JSContext *cx, unsigned argc, JS::Value *vp); + + static host_api::Secret secret_handle(JSObject *obj); + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + static JSObject *create(JSContext *cx, host_api::Secret handle); +}; + +class SecretStore : public builtins::BuiltinImpl { +private: +public: + static constexpr const char *class_name = "SecretStore"; + static const int ctor_length = 1; + enum Slots { Handle, Count }; + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static bool get(JSContext *cx, unsigned argc, JS::Value *vp); + + static host_api::SecretStore secret_store_handle(JSObject *obj); + static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); +}; + +} // namespace fastly::secret_store + +#endif diff --git a/src/compileApplicationToWasm.js b/src/compileApplicationToWasm.js index 326eb0d5b6..b42db10169 100644 --- a/src/compileApplicationToWasm.js +++ b/src/compileApplicationToWasm.js @@ -1,6 +1,8 @@ -import { dirname, resolve } from "node:path"; +import { dirname, resolve, sep, normalize } from "node:path"; +import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; -import { mkdir, readFile } from "node:fs/promises"; +import { mkdir, readFile, mkdtemp, writeFile } from "node:fs/promises"; +import { rmSync } from "node:fs"; import { isFile } from "./isFile.js"; import { isFileOrDoesNotExist } from "./isFileOrDoesNotExist.js"; import wizer from "@bytecodealliance/wizer"; @@ -9,6 +11,10 @@ import { enableTopLevelAwait } from "./enableTopLevelAwait.js"; import { bundle } from "./bundle.js"; import { containsSyntaxErrors } from "./containsSyntaxErrors.js"; +async function getTmpDir () { + return await mkdtemp(normalize(tmpdir() + sep)); +} + export async function compileApplicationToWasm( input, output, @@ -86,20 +92,27 @@ export async function compileApplicationToWasm( process.exit(1); } - let wizerInput; - if (!starlingMonkey) { - let contents = await bundle(input, enableExperimentalTopLevelAwait); + let wizerInput, cleanup = () => {}; - wizerInput = precompile( - contents.outputFiles[0].text, - undefined, - enableExperimentalTopLevelAwait - ); - if (enableExperimentalTopLevelAwait) { - wizerInput = enableTopLevelAwait(wizerInput); - } - } else { - wizerInput = resolve(input); + let contents = await bundle(input, enableExperimentalTopLevelAwait); + wizerInput = precompile( + contents.outputFiles[0].text, + undefined, + enableExperimentalTopLevelAwait + ); + if (enableExperimentalTopLevelAwait && !starlingMonkey) { + wizerInput = enableTopLevelAwait(wizerInput); + } + + // for StarlingMonkey, we need to write to a tmpdir + if (starlingMonkey) { + const tmpDir = await getTmpDir(); + const outPath = resolve(tmpDir, 'input.js'); + await writeFile(outPath, wizerInput); + wizerInput = outPath; + cleanup = () => { + rmSync(tmpDir, { recursive: true }); + }; } try { @@ -108,7 +121,8 @@ export async function compileApplicationToWasm( [ "--inherit-env=true", "--allow-wasi", - `--dir=${starlingMonkey ? resolve('/') : '.'}`, + "--dir=.", + ...starlingMonkey ? [`--dir=${dirname(wizerInput)}`] : [], `--wasm-bulk-memory=true`, "-r _start=wizer.resume", `-o=${output}`, @@ -137,5 +151,7 @@ export async function compileApplicationToWasm( error.message ); process.exit(1); + } finally { + cleanup(); } }