diff --git a/runtime/fastly/CMakeLists.txt b/runtime/fastly/CMakeLists.txt index 3bed3fc4c5..457646092e 100644 --- a/runtime/fastly/CMakeLists.txt +++ b/runtime/fastly/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.27) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + include("../StarlingMonkey/cmake/add_as_subproject.cmake") add_builtin( diff --git a/runtime/fastly/builtins/fastly.cpp b/runtime/fastly/builtins/fastly.cpp index 9e0ce57f79..22af29ce5a 100644 --- a/runtime/fastly/builtins/fastly.cpp +++ b/runtime/fastly/builtins/fastly.cpp @@ -169,6 +169,116 @@ bool Fastly::getGeolocationForIpAddress(JSContext *cx, unsigned argc, JS::Value return JS_ParseJSON(cx, geo_info_str, args.rval()); } +bool Fastly::inspect(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + REQUEST_HANDLER_ONLY("inspect"); + if (!args.requireAtLeast(cx, "inspect", 1)) { + return false; + } + + auto request_value = args.get(0); + if (!Request::is_instance(request_value)) { + JS_ReportErrorUTF8(cx, "inspect: request parameter must be an instance of Request"); + return false; + } + auto inspect_response_obj = &request_value.toObject(); + + auto options_value = args.get(1); + JS::RootedObject options_obj(cx, options_value.isObject() ? &options_value.toObject() : nullptr); + + host_api::InspectOptions inspect_options(Request::request_handle(inspect_response_obj), + RequestOrResponse::body_handle(inspect_response_obj)); + + if (options_value != nullptr) { + + host_api::HostString corp_str; + JS::RootedValue corp_val(cx); + if (!JS_GetProperty(cx, options, "corp", &corp_val)) { + return false; + } + if (!corp_val.isNullOrUndefined()) { + if (!corp_val.isString()) { + api::throw_error(cx, api::Errors::TypeError, "inspect", "corp", "be a string"); + return false; + } + corp_str = core::encode(cx, corp_val); + if (!corp_str) { + return false; + } + std::optional corp = corp_str; + if (corp) { + inspect_options.corp_len = corp->length(); + inspect_options.corp = std::move(corp->data()); + } + } + + host_api::HostString workspace_str; + JS::RootedValue workspace_val(cx); + if (!JS_GetProperty(cx, options, "workspace", &workspace_val)) { + return false; + } + if (!workspace_val.isNullOrUndefined()) { + if (!workspace_val.isString()) { + api::throw_error(cx, api::Errors::TypeError, "inspect", "workspace", "be a string"); + return false; + } + workspace_str = core::encode(cx, workspace_val); + if (!workspace_str) { + return false; + } + std::optional workspace = workspace_str; + if (workspace) { + inspect_options.workspace_len = workspace->length(); + inspect_options.workspace = std::move(workspace->data()); + } + } + + host_api::HostString override_client_ip_str; + JS::RootedValue override_client_ip_val(cx); + if (!JS_GetProperty(cx, options, "overrideClientIp", &override_client_ip_val)) { + return false; + } + if (!override_client_ip_val.isNullOrUndefined()) { + if (!override_client_ip_val.isString()) { + api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "overrideClientIp", + "be a string"); + return false; + } + override_client_ip_str = core::encode(cx, override_client_ip_val); + if (!override_client_ip_str) { + return false; + } + + // TODO: Remove all of this and rely on the host for validation as the hostcall only takes one + // user-supplied parameter + int format = AF_INET; + size_t octets_len = 4; + if (std::find(override_client_ip_str.begin(), override_client_ip_str.end(), ':') != + override_client_ip_str.end()) { + format = AF_INET6; + octets_len = 16; + } + + uint8_t octets[sizeof(struct in6_addr)]; + if (inet_pton(format, override_client_ip_str.begin(), octets) != 1) { + api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "overrideClientIp", + "be a valid IP address"); + return false; + } + inspect_options.override_client_ip_len = octets_len; + inspect_options.override_client_ip = std::move(octets->data()); + } + } + + auto res = request_value->inspect(&inspect_options); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + return JS_ParseJSON(cx, inspect_info_str, args.rval()); +} + // TODO(performance): consider allowing logger creation during initialization, but then throw // when trying to log. // https://github.com/fastly/js-compute-runtime/issues/225 @@ -610,6 +720,7 @@ bool install(api::Engine *engine) { JS_FN("enableDebugLogging", Fastly::enableDebugLogging, 1, JSPROP_ENUMERATE), JS_FN("debugLog", debugLog, 1, JSPROP_ENUMERATE), JS_FN("getGeolocationForIpAddress", Fastly::getGeolocationForIpAddress, 1, JSPROP_ENUMERATE), + JS_FN("inspect", Fastly::inspect, 1, JSPROP_ENUMERATE), JS_FN("getLogger", Fastly::getLogger, 1, JSPROP_ENUMERATE), JS_FN("includeBytes", Fastly::includeBytes, 1, JSPROP_ENUMERATE), JS_FN("createFanoutHandoff", Fastly::createFanoutHandoff, 2, JSPROP_ENUMERATE), @@ -751,6 +862,21 @@ bool install(api::Engine *engine) { if (!engine->define_builtin_module("fastly:fanout", fanout_val)) { return false; } + + // fastly:security + RootedValue inspect_val(engine->cx()); + if (!JS_GetProperty(engine->cx(), fastly, "inspect", &inspect_val)) { + return false; + } + RootedObject security_builtin(engine->cx(), JS_NewObject(engine->cx(), nullptr)); + RootedValue security_builtin_val(engine->cx(), JS::ObjectValue(*security_builtin)); + if (!JS_SetProperty(engine->cx(), security_builtin, "inspect", inspect_val)) { + return false; + } + if (!engine->define_builtin_module("fastly:security", security_builtin_val)) { + return false; + } + // fastly:websocket RootedObject websocket(engine->cx(), JS_NewObject(engine->cx(), nullptr)); RootedValue websocket_val(engine->cx(), JS::ObjectValue(*websocket)); diff --git a/runtime/fastly/builtins/fastly.h b/runtime/fastly/builtins/fastly.h index 93ece02108..c0fd2f76e8 100644 --- a/runtime/fastly/builtins/fastly.h +++ b/runtime/fastly/builtins/fastly.h @@ -60,6 +60,7 @@ class Fastly : public builtins::BuiltinNoConstructor { static bool defaultBackend_set(JSContext *cx, unsigned argc, JS::Value *vp); static bool allowDynamicBackends_get(JSContext *cx, unsigned argc, JS::Value *vp); static bool allowDynamicBackends_set(JSContext *cx, unsigned argc, JS::Value *vp); + static bool inspect(JSContext *cx, unsigned argc, JS::Value *vp); }; JS::Result> convertBodyInit(JSContext *cx, diff --git a/runtime/fastly/builtins/fetch/request-response.h b/runtime/fastly/builtins/fetch/request-response.h index f4da11e657..86f4d08939 100644 --- a/runtime/fastly/builtins/fetch/request-response.h +++ b/runtime/fastly/builtins/fetch/request-response.h @@ -289,7 +289,7 @@ class Response final : public builtins::FinalizableBuiltinImpl { /** * Base-level response creation handler, for both upstream and downstream requests. */ - static JSObject *create(JSContext *cx, JS::HandleObject response, + staticeJSObject *create(JSContext *cx, JS::HandleObject response, host_api::HttpResp response_handle, host_api::HttpBody body_handle, bool is_upstream, JSObject *grip_upgrade_request, JSObject *websocket_upgrade_request, JS::HandleString backend); diff --git a/runtime/fastly/host-api/fastly.h b/runtime/fastly/host-api/fastly.h index b0d0cb28c6..a531facf8b 100644 --- a/runtime/fastly/host-api/fastly.h +++ b/runtime/fastly/host-api/fastly.h @@ -39,6 +39,15 @@ typedef struct fastly_host_http_response { uint32_t f1; } fastly_host_http_response; +typedef struct fastly_host_http_inspect_options { + uint8_t *corp; + uint32_t corp_len; + uint8_t *workspace; + uint32_t workspace_len; + uint8_t *override_client_ip_ptr; + uint32_t override_client_ip_len; +} fastly_host_http_inspect_options; + typedef fastly_host_http_response fastly_world_tuple2_handle_handle; #define WASM_IMPORT(module, name) __attribute__((import_module(module), import_name(name))) @@ -265,6 +274,13 @@ typedef enum BodyWriteEnd { #define CACHE_OVERRIDE_STALE_WHILE_REVALIDATE (1u << 2) #define CACHE_OVERRIDE_PCI (1u << 3) +typedef uint32_t req_inspect_config_options_mask; + +#define FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_RESERVED = 1 << 0; +#define FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_CORP = 1 << 1; +#define FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_WORKSPACE = 1 << 2; +#define FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_OVERRIDE_CLIENT_IP = 1 << 3; + WASM_IMPORT("fastly_abi", "init") int init(uint64_t abi_version); @@ -620,6 +636,12 @@ int req_pending_req_wait_v2(uint32_t req_handle, fastly_host_http_send_error_detail *send_error_detail, uint32_t *resp_handle_out, uint32_t *resp_body_handle_out); +WASM_IMPORT("fastly_http_req", "inspect") +int req_inspect(uint32_t req_handle, uint32_t body_handle, + req_inspect_config_options_mask config_options_mask, + fastly_host_http_inspect_options *config, uint8_t *inspect_res_buf, + uint32_t inspect_res_buf_len, uint32_t *nwritten_out); + // Module fastly_http_resp WASM_IMPORT("fastly_http_resp", "new") int resp_new(uint32_t *resp_handle_out); diff --git a/runtime/fastly/host-api/host_api.cpp b/runtime/fastly/host-api/host_api.cpp index ab33543bfc..60c281b7b3 100644 --- a/runtime/fastly/host-api/host_api.cpp +++ b/runtime/fastly/host-api/host_api.cpp @@ -2117,6 +2117,35 @@ Result HttpReq::get_suggested_cache_key() const { return Result::ok(make_host_string(str)); } +Result Request::inspect(const InspectConfig *config) { + TRACE_CALL() + uint32_t inspect_opts_mask{0}; + + if (config.corp != nullptr) { + inspect_opts_mask |= FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_CORP; + } + + if (config.workspace != nullptr) { + inspect_opts_mask |= FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_WORKSPACE; + } + + if (config.override_client_ip != nullptr) { + inspect_opts_mask |= FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_OVERRIDE_CLIENT_IP; + } + + fastly::fastly_host_error err; + fastly::fastly_world_string ret; + ret.ptr = static_cast(cabi_malloc(HOSTCALL_BUFFER_LEN, 4)); + if (!convert_result(fastly::req_inspect(this->req.handle, this->body.handle, inspect_opts_mask, + config, ret.ptr, &ret.len, ), + &err)) { + res.emplace_err(err); + } else { + res.emplace(make_host_string(ret)); + } + return res; +} + // HttpCacheEntry method implementations Result HttpCacheEntry::lookup(const HttpReq &req, std::span override_key) { TRACE_CALL() diff --git a/runtime/fastly/host-api/host_api_fastly.h b/runtime/fastly/host-api/host_api_fastly.h index ad444c67d0..70e8fb8d24 100644 --- a/runtime/fastly/host-api/host_api_fastly.h +++ b/runtime/fastly/host-api/host_api_fastly.h @@ -374,7 +374,7 @@ struct TlsVersion { uint8_t value = 0; explicit TlsVersion(uint8_t raw); - explicit TlsVersion(){}; + explicit TlsVersion() {}; uint8_t get_version() const; double get_version_number() const; @@ -515,6 +515,21 @@ enum class FramingHeadersMode : uint8_t { ManuallyFromHeaders, }; +class InspectOptions final { +public: + uint8_t *corp = nullptr; + uint32_t corp_len = 0; + uint8_t *workspace = nullptr; + uint32_t workspace_len = 0; + uint8_t *override_client_ip_ptr = nullptr; + uint32_t override_client_ip_len = 0; + uint32_t req_handle; + uint32_t body_handle; + + InspectOptions() = default; + explicit InspectOptions(uint32_t req, uint32_t body) : req_handle{req}, body_handle{body} {} +}; + class HttpReq final : public HttpBase { public: using Handle = uint32_t; @@ -658,6 +673,8 @@ struct Request { Request() = default; Request(HttpReq req, HttpBody body) : req{req}, body{body} {} + + Result inspect(const InspectConfig *config); }; class GeoIp final { diff --git a/types/html-rewriter.d.ts b/types/html-rewriter.d.ts index d8cc3fa247..7387b05039 100644 --- a/types/html-rewriter.d.ts +++ b/types/html-rewriter.d.ts @@ -1,95 +1,94 @@ declare module 'fastly:html-rewriter' { - + /** + * Stream for rewriting HTML content. + */ + export class HTMLRewritingStream implements TransformStream { + constructor(); /** - * Stream for rewriting HTML content. + * Registers a callback for elements matching the selector. + * @param selector CSS selector string + * @param handler Function called with each matching Element + * @returns The HTMLRewritingStream instance for chaining + * @throws {Error} If the selector or handler is invalid */ - export class HTMLRewritingStream implements TransformStream { - constructor(); - /** - * Registers a callback for elements matching the selector. - * @param selector CSS selector string - * @param handler Function called with each matching Element - * @returns The HTMLRewritingStream instance for chaining - * @throws {Error} If the selector or handler is invalid - */ - onElement(selector: string, handler: (element: Element) => void): this; + onElement(selector: string, handler: (element: Element) => void): this; - /** - * The writable stream to which HTML content should be written. - */ - writable: WritableStream; - /** - * The readable stream from which transformed HTML content can be read. - */ - readable: ReadableStream; - } + /** + * The writable stream to which HTML content should be written. + */ + writable: WritableStream; + /** + * The readable stream from which transformed HTML content can be read. + */ + readable: ReadableStream; + } + /** + * Options for rewriting HTML elements. + */ + export interface ElementRewriterOptions { /** - * Options for rewriting HTML elements. + * Whether to escape HTML in rewritten content. */ - export interface ElementRewriterOptions { - /** - * Whether to escape HTML in rewritten content. - */ - escapeHTML?: boolean; - } + escapeHTML?: boolean; + } + /** + * Represents an HTML element in the rewriting stream. + */ + export class Element { + /** + * Sets an attribute on the element. + * @param name Attribute name + * @param value Attribute value + */ + setAttribute(name: string, value: string): void; + /** + * Gets the value of an attribute. + * @param name Attribute name + * @returns Attribute value or null if not present + */ + getAttribute(name: string): string | null; + /** + * Removes an attribute from the element. + * @param name Attribute name + */ + removeAttribute(name: string): void; + /** + * Replaces the element with new content. + * @param content Replacement HTML or text + * @param options Optional rewriting options + */ + replaceWith(content: string, options?: ElementRewriterOptions): void; + /** + * Replaces the element's children with new content. + * @param content Replacement HTML or text + * @param options Optional rewriting options + */ + replaceChildren(content: string, options?: ElementRewriterOptions): void; + /** + * Inserts content before the element. + * @param content HTML or text to insert + * @param options Optional rewriting options + */ + before(content: string, options?: ElementRewriterOptions): void; + /** + * Inserts content after the element. + * @param content HTML or text to insert + * @param options Optional rewriting options + */ + after(content: string, options?: ElementRewriterOptions): void; + /** + * Prepends content to the element's children. + * @param content HTML or text to prepend + * @param options Optional rewriting options + */ + prepend(content: string, options?: ElementRewriterOptions): void; /** - * Represents an HTML element in the rewriting stream. + * Appends content to the element's children. + * @param content HTML or text to append + * @param options Optional rewriting options */ - export class Element { - /** - * Sets an attribute on the element. - * @param name Attribute name - * @param value Attribute value - */ - setAttribute(name: string, value: string): void; - /** - * Gets the value of an attribute. - * @param name Attribute name - * @returns Attribute value or null if not present - */ - getAttribute(name: string): string | null; - /** - * Removes an attribute from the element. - * @param name Attribute name - */ - removeAttribute(name: string): void; - /** - * Replaces the element with new content. - * @param content Replacement HTML or text - * @param options Optional rewriting options - */ - replaceWith(content: string, options?: ElementRewriterOptions): void; - /** - * Replaces the element's children with new content. - * @param content Replacement HTML or text - * @param options Optional rewriting options - */ - replaceChildren(content: string, options?: ElementRewriterOptions): void; - /** - * Inserts content before the element. - * @param content HTML or text to insert - * @param options Optional rewriting options - */ - before(content: string, options?: ElementRewriterOptions): void; - /** - * Inserts content after the element. - * @param content HTML or text to insert - * @param options Optional rewriting options - */ - after(content: string, options?: ElementRewriterOptions): void; - /** - * Prepends content to the element's children. - * @param content HTML or text to prepend - * @param options Optional rewriting options - */ - prepend(content: string, options?: ElementRewriterOptions): void; - /** - * Appends content to the element's children. - * @param content HTML or text to append - * @param options Optional rewriting options - */ - append(content: string, options?: ElementRewriterOptions): void; - } -} \ No newline at end of file + append(content: string, options?: ElementRewriterOptions): void; + } +}