From 4e87195739f2a5d9a05451b48773c8afdc680765 Mon Sep 17 00:00:00 2001 From: akiyamn Date: Sun, 24 Sep 2023 23:22:21 +1000 Subject: Initial commit (by create-cloudflare CLI) --- .../wrangler/templates/middleware/common.ts | 67 ++++++ .../templates/middleware/loader-modules.ts | 137 +++++++++++++ .../wrangler/templates/middleware/loader-sw.ts | 228 +++++++++++++++++++++ .../templates/middleware/middleware-d1-beta.d.ts | 3 + .../middleware/middleware-miniflare3-json-error.ts | 33 +++ .../middleware/middleware-multiworker-dev.d.ts | 4 + .../middleware/middleware-multiworker-dev.ts | 59 ++++++ .../middleware/middleware-pretty-error.ts | 40 ++++ .../templates/middleware/middleware-scheduled.ts | 15 ++ .../middleware/middleware-serve-static-assets.d.ts | 6 + .../middleware/middleware-serve-static-assets.ts | 56 +++++ 11 files changed, 648 insertions(+) create mode 100644 node_modules/wrangler/templates/middleware/common.ts create mode 100644 node_modules/wrangler/templates/middleware/loader-modules.ts create mode 100644 node_modules/wrangler/templates/middleware/loader-sw.ts create mode 100644 node_modules/wrangler/templates/middleware/middleware-d1-beta.d.ts create mode 100644 node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts create mode 100644 node_modules/wrangler/templates/middleware/middleware-multiworker-dev.d.ts create mode 100644 node_modules/wrangler/templates/middleware/middleware-multiworker-dev.ts create mode 100644 node_modules/wrangler/templates/middleware/middleware-pretty-error.ts create mode 100644 node_modules/wrangler/templates/middleware/middleware-scheduled.ts create mode 100644 node_modules/wrangler/templates/middleware/middleware-serve-static-assets.d.ts create mode 100644 node_modules/wrangler/templates/middleware/middleware-serve-static-assets.ts (limited to 'node_modules/wrangler/templates/middleware') diff --git a/node_modules/wrangler/templates/middleware/common.ts b/node_modules/wrangler/templates/middleware/common.ts new file mode 100644 index 0000000..1319e5d --- /dev/null +++ b/node_modules/wrangler/templates/middleware/common.ts @@ -0,0 +1,67 @@ +export type Awaitable = T | Promise; +// TODO: allow dispatching more events? +export type Dispatcher = ( + type: "scheduled", + init: { cron?: string } +) => Awaitable; + +export type IncomingRequest = Request< + unknown, + IncomingRequestCfProperties +>; + +export interface MiddlewareContext { + dispatch: Dispatcher; + next(request: IncomingRequest, env: any): Awaitable; +} + +export type Middleware = ( + request: IncomingRequest, + env: any, + ctx: ExecutionContext, + middlewareCtx: MiddlewareContext +) => Awaitable; + +const __facade_middleware__: Middleware[] = []; + +// The register functions allow for the insertion of one or many middleware, +// We register internal middleware first in the stack, but have no way of controlling +// the order that addMiddleware is run in service workers so need an internal function. +export function __facade_register__(...args: (Middleware | Middleware[])[]) { + __facade_middleware__.push(...args.flat()); +} +export function __facade_registerInternal__( + ...args: (Middleware | Middleware[])[] +) { + __facade_middleware__.unshift(...args.flat()); +} + +function __facade_invokeChain__( + request: IncomingRequest, + env: any, + ctx: ExecutionContext, + dispatch: Dispatcher, + middlewareChain: Middleware[] +): Awaitable { + const [head, ...tail] = middlewareChain; + const middlewareCtx: MiddlewareContext = { + dispatch, + next(newRequest, newEnv) { + return __facade_invokeChain__(newRequest, newEnv, ctx, dispatch, tail); + }, + }; + return head(request, env, ctx, middlewareCtx); +} + +export function __facade_invoke__( + request: IncomingRequest, + env: any, + ctx: ExecutionContext, + dispatch: Dispatcher, + finalMiddleware: Middleware +): Awaitable { + return __facade_invokeChain__(request, env, ctx, dispatch, [ + ...__facade_middleware__, + finalMiddleware, + ]); +} diff --git a/node_modules/wrangler/templates/middleware/loader-modules.ts b/node_modules/wrangler/templates/middleware/loader-modules.ts new file mode 100644 index 0000000..3da0c44 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/loader-modules.ts @@ -0,0 +1,137 @@ +// // This loads all middlewares exposed on the middleware object +// // and then starts the invocation chain. +// // The big idea is that we can add these to the middleware export dynamically +// // through wrangler, or we can potentially let users directly add them as a sort +// // of "plugin" system. + +import { + Dispatcher, + Middleware, + __facade_invoke__, + __facade_register__, +} from "./common"; + +import worker from "__ENTRY_POINT__"; + +// We need to preserve all of the exports from the worker +export * from "__ENTRY_POINT__"; + +class __Facade_ScheduledController__ implements ScheduledController { + #noRetry: ScheduledController["noRetry"]; + + constructor( + readonly scheduledTime: number, + readonly cron: string, + noRetry: ScheduledController["noRetry"] + ) { + this.#noRetry = noRetry; + } + + noRetry() { + if (!(this instanceof __Facade_ScheduledController__)) { + throw new TypeError("Illegal invocation"); + } + // Need to call native method immediately in case uncaught error thrown + this.#noRetry(); + } +} + +const __facade_modules_fetch__: ExportedHandlerFetchHandler = function ( + request, + env, + ctx +) { + if (worker.fetch === undefined) + throw new Error("Handler does not export a fetch() function."); + return worker.fetch(request, env, ctx); +}; + +function getMaskedEnv(rawEnv: unknown) { + let env = rawEnv as Record; + if (worker.envWrappers && worker.envWrappers.length > 0) { + for (const wrapFn of worker.envWrappers) { + env = wrapFn(env); + } + } + return env; +} + +/** + * This type is here to cause a type error if a new export handler is added to + * `ExportHandler` without it being included in the `facade` below. + */ +type MissingExportHandlers = Omit< + Required, + "tail" | "trace" | "scheduled" | "queue" | "test" | "email" | "fetch" +>; + +let registeredMiddleware = false; + +const facade: ExportedHandler & MissingExportHandlers = { + ...(worker.tail && { + tail: maskHandlerEnv(worker.tail), + }), + ...(worker.trace && { + trace: maskHandlerEnv(worker.trace), + }), + ...(worker.scheduled && { + scheduled: maskHandlerEnv(worker.scheduled), + }), + ...(worker.queue && { + queue: maskHandlerEnv(worker.queue), + }), + ...(worker.test && { + test: maskHandlerEnv(worker.test), + }), + ...(worker.email && { + email: maskHandlerEnv(worker.email), + }), + + fetch(request, rawEnv, ctx) { + const env = getMaskedEnv(rawEnv); + // Get the chain of middleware from the worker object + if (worker.middleware && worker.middleware.length > 0) { + // Make sure we only register middleware once: + // https://github.com/cloudflare/workers-sdk/issues/2386#issuecomment-1614715911 + if (!registeredMiddleware) { + registeredMiddleware = true; + for (const middleware of worker.middleware) { + __facade_register__(middleware); + } + } + + const __facade_modules_dispatch__: Dispatcher = function (type, init) { + if (type === "scheduled" && worker.scheduled !== undefined) { + const controller = new __Facade_ScheduledController__( + Date.now(), + init.cron ?? "", + () => {} + ); + return worker.scheduled(controller, env, ctx); + } + }; + + return __facade_invoke__( + request, + env, + ctx, + __facade_modules_dispatch__, + __facade_modules_fetch__ + ); + } else { + // We didn't have any middleware so we can skip the invocation chain, + // and just call the fetch handler directly + + // We "don't care" if this is undefined as we want to have the same behavior + // as if the worker completely bypassed middleware. + return __facade_modules_fetch__(request, env, ctx); + } + }, +}; + +type HandlerFn = (data: D, env: unknown, ctx: ExecutionContext) => R; +function maskHandlerEnv(handler: HandlerFn): HandlerFn { + return (data, env, ctx) => handler(data, getMaskedEnv(env), ctx); +} + +export default facade; diff --git a/node_modules/wrangler/templates/middleware/loader-sw.ts b/node_modules/wrangler/templates/middleware/loader-sw.ts new file mode 100644 index 0000000..9e465f9 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/loader-sw.ts @@ -0,0 +1,228 @@ +import { + Awaitable, + Dispatcher, + IncomingRequest, + Middleware, + __facade_invoke__, + __facade_register__, + __facade_registerInternal__, +} from "./common"; +export { __facade_register__, __facade_registerInternal__ }; + +// Miniflare 2's `EventTarget` follows the spec and doesn't allow exceptions to +// be caught by `dispatchEvent`. Instead it has a custom `ThrowingEventTarget` +// class that rethrows errors from event listeners in `dispatchEvent`. +// We'd like errors to be propagated to the top-level `addEventListener`, so +// we'd like to use `ThrowingEventTarget`. Unfortunately, `ThrowingEventTarget` +// isn't exposed on the global scope, but `WorkerGlobalScope` (which extends +// `ThrowingEventTarget`) is. Therefore, we get at it in this nasty way. +let __FACADE_EVENT_TARGET__: EventTarget; +if ((globalThis as any).MINIFLARE) { + __FACADE_EVENT_TARGET__ = new (Object.getPrototypeOf(WorkerGlobalScope))(); +} else { + __FACADE_EVENT_TARGET__ = new EventTarget(); +} + +function __facade_isSpecialEvent__( + type: string +): type is "fetch" | "scheduled" { + return type === "fetch" || type === "scheduled"; +} +const __facade__originalAddEventListener__ = globalThis.addEventListener; +const __facade__originalRemoveEventListener__ = globalThis.removeEventListener; +const __facade__originalDispatchEvent__ = globalThis.dispatchEvent; + +globalThis.addEventListener = function (type, listener, options) { + if (__facade_isSpecialEvent__(type)) { + __FACADE_EVENT_TARGET__.addEventListener( + type, + listener as EventListenerOrEventListenerObject, + options + ); + } else { + __facade__originalAddEventListener__(type, listener, options); + } +}; +globalThis.removeEventListener = function (type, listener, options) { + if (__facade_isSpecialEvent__(type)) { + __FACADE_EVENT_TARGET__.removeEventListener( + type, + listener as EventListenerOrEventListenerObject, + options + ); + } else { + __facade__originalRemoveEventListener__(type, listener, options); + } +}; +globalThis.dispatchEvent = function (event) { + if (__facade_isSpecialEvent__(event.type)) { + return __FACADE_EVENT_TARGET__.dispatchEvent(event); + } else { + return __facade__originalDispatchEvent__(event); + } +}; + +declare global { + var addMiddleware: typeof __facade_register__; + var addMiddlewareInternal: typeof __facade_registerInternal__; +} +globalThis.addMiddleware = __facade_register__; +globalThis.addMiddlewareInternal = __facade_registerInternal__; + +const __facade_waitUntil__ = Symbol("__facade_waitUntil__"); +const __facade_response__ = Symbol("__facade_response__"); +const __facade_dispatched__ = Symbol("__facade_dispatched__"); + +class __Facade_ExtendableEvent__ extends Event { + [__facade_waitUntil__]: Awaitable[] = []; + + waitUntil(promise: Awaitable) { + if (!(this instanceof __Facade_ExtendableEvent__)) { + throw new TypeError("Illegal invocation"); + } + this[__facade_waitUntil__].push(promise); + } +} + +interface FetchEventInit extends EventInit { + request: Request; + passThroughOnException: FetchEvent["passThroughOnException"]; +} + +class __Facade_FetchEvent__ extends __Facade_ExtendableEvent__ { + #request: Request; + #passThroughOnException: FetchEvent["passThroughOnException"]; + [__facade_response__]?: Awaitable; + [__facade_dispatched__] = false; + + constructor(type: "fetch", init: FetchEventInit) { + super(type); + this.#request = init.request; + this.#passThroughOnException = init.passThroughOnException; + } + + get request() { + return this.#request; + } + + respondWith(response: Awaitable) { + if (!(this instanceof __Facade_FetchEvent__)) { + throw new TypeError("Illegal invocation"); + } + if (this[__facade_response__] !== undefined) { + throw new DOMException( + "FetchEvent.respondWith() has already been called; it can only be called once.", + "InvalidStateError" + ); + } + if (this[__facade_dispatched__]) { + throw new DOMException( + "Too late to call FetchEvent.respondWith(). It must be called synchronously in the event handler.", + "InvalidStateError" + ); + } + this.stopImmediatePropagation(); + this[__facade_response__] = response; + } + + passThroughOnException() { + if (!(this instanceof __Facade_FetchEvent__)) { + throw new TypeError("Illegal invocation"); + } + // Need to call native method immediately in case uncaught error thrown + this.#passThroughOnException(); + } +} + +interface ScheduledEventInit extends EventInit { + scheduledTime: number; + cron: string; + noRetry: ScheduledEvent["noRetry"]; +} + +class __Facade_ScheduledEvent__ extends __Facade_ExtendableEvent__ { + #scheduledTime: number; + #cron: string; + #noRetry: ScheduledEvent["noRetry"]; + + constructor(type: "scheduled", init: ScheduledEventInit) { + super(type); + this.#scheduledTime = init.scheduledTime; + this.#cron = init.cron; + this.#noRetry = init.noRetry; + } + + get scheduledTime() { + return this.#scheduledTime; + } + + get cron() { + return this.#cron; + } + + noRetry() { + if (!(this instanceof __Facade_ScheduledEvent__)) { + throw new TypeError("Illegal invocation"); + } + // Need to call native method immediately in case uncaught error thrown + this.#noRetry(); + } +} + +__facade__originalAddEventListener__("fetch", (event) => { + const ctx: ExecutionContext = { + waitUntil: event.waitUntil.bind(event), + passThroughOnException: event.passThroughOnException.bind(event), + }; + + const __facade_sw_dispatch__: Dispatcher = function (type, init) { + if (type === "scheduled") { + const facadeEvent = new __Facade_ScheduledEvent__("scheduled", { + scheduledTime: Date.now(), + cron: init.cron ?? "", + noRetry() {}, + }); + + __FACADE_EVENT_TARGET__.dispatchEvent(facadeEvent); + event.waitUntil(Promise.all(facadeEvent[__facade_waitUntil__])); + } + }; + + const __facade_sw_fetch__: Middleware = function (request, _env, ctx) { + const facadeEvent = new __Facade_FetchEvent__("fetch", { + request, + passThroughOnException: ctx.passThroughOnException, + }); + + __FACADE_EVENT_TARGET__.dispatchEvent(facadeEvent); + facadeEvent[__facade_dispatched__] = true; + event.waitUntil(Promise.all(facadeEvent[__facade_waitUntil__])); + + const response = facadeEvent[__facade_response__]; + if (response === undefined) { + throw new Error("No response!"); // TODO: proper error message + } + return response; + }; + + event.respondWith( + __facade_invoke__( + event.request as IncomingRequest, + globalThis, + ctx, + __facade_sw_dispatch__, + __facade_sw_fetch__ + ) + ); +}); + +__facade__originalAddEventListener__("scheduled", (event) => { + const facadeEvent = new __Facade_ScheduledEvent__("scheduled", { + scheduledTime: event.scheduledTime, + cron: event.cron, + noRetry: event.noRetry.bind(event), + }); + + __FACADE_EVENT_TARGET__.dispatchEvent(facadeEvent); + event.waitUntil(Promise.all(facadeEvent[__facade_waitUntil__])); +}); diff --git a/node_modules/wrangler/templates/middleware/middleware-d1-beta.d.ts b/node_modules/wrangler/templates/middleware/middleware-d1-beta.d.ts new file mode 100644 index 0000000..5ee0e6f --- /dev/null +++ b/node_modules/wrangler/templates/middleware/middleware-d1-beta.d.ts @@ -0,0 +1,3 @@ +declare module "config:middleware/d1-beta" { + export const D1_IMPORTS: string[]; +} diff --git a/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts b/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts new file mode 100644 index 0000000..9377781 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts @@ -0,0 +1,33 @@ +import type { Middleware } from "./common"; + +interface JsonError { + message?: string; + name?: string; + stack?: string; + cause?: JsonError; +} + +function reduceError(e: any): JsonError { + return { + name: e?.name, + message: e?.message ?? String(e), + stack: e?.stack, + cause: e?.cause === undefined ? undefined : reduceError(e.cause), + }; +} + +// See comment in `bundle.ts` for details on why this is needed +const jsonError: Middleware = async (request, env, _ctx, middlewareCtx) => { + try { + return await middlewareCtx.next(request, env); + } catch (e: any) { + const error = reduceError(e); + return Response.json(error, { + status: 500, + headers: { "MF-Experimental-Error-Stack": "true" }, + }); + } +}; + +export default jsonError; +export const wrap = undefined; diff --git a/node_modules/wrangler/templates/middleware/middleware-multiworker-dev.d.ts b/node_modules/wrangler/templates/middleware/middleware-multiworker-dev.d.ts new file mode 100644 index 0000000..704b273 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/middleware-multiworker-dev.d.ts @@ -0,0 +1,4 @@ +declare module "config:middleware/multiworker-dev" { + import type { WorkerRegistry } from "../../src/dev-registry"; + export const workers: WorkerRegistry; +} diff --git a/node_modules/wrangler/templates/middleware/middleware-multiworker-dev.ts b/node_modules/wrangler/templates/middleware/middleware-multiworker-dev.ts new file mode 100644 index 0000000..fee33d5 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/middleware-multiworker-dev.ts @@ -0,0 +1,59 @@ +// @ts-nocheck +/// + +import { workers } from "config:middleware/multiworker-dev"; +import type { WorkerRegistry } from "../../src/dev-registry"; + +export function wrap(env: Record) { + const facadeEnv = { ...env }; + // For every Worker definition that's available, + // create a fetcher for it on the facade env. + // for const [name, binding] of env + // if Workers[name] + // const details = Workers[name]; + + for (const [name, details] of Object.entries(workers as WorkerRegistry)) { + if (details) { + facadeEnv[name] = { + async fetch(...reqArgs: Parameters) { + const reqFromArgs = new Request(...reqArgs); + if (details.headers) { + for (const [key, value] of Object.entries(details.headers)) { + // In remote mode, you need to add a couple of headers + // to make sure it's talking to the 'dev' preview session + // (much like wrangler dev already does via proxy.ts) + reqFromArgs.headers.set(key, value); + } + return (env[name] as Fetcher).fetch(reqFromArgs); + } + + const url = new URL(reqFromArgs.url); + if (details.protocol !== undefined) { + url.protocol = details.protocol; + } + if (details.host !== undefined) { + url.host = details.host; + } + if (details.port !== undefined) { + url.port = details.port.toString(); + } + + const request = new Request(url.toString(), reqFromArgs); + return fetch(request); + }, + }; + } else { + // This means there's no dev binding available. + // Let's use whatever's available, or put a shim with a message. + facadeEnv[name] = facadeEnv[name] || { + async fetch() { + return new Response( + `You should start up wrangler dev --local on the ${name} worker`, + { status: 404 } + ); + }, + }; + } + } + return facadeEnv; +} diff --git a/node_modules/wrangler/templates/middleware/middleware-pretty-error.ts b/node_modules/wrangler/templates/middleware/middleware-pretty-error.ts new file mode 100644 index 0000000..29dc6d0 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/middleware-pretty-error.ts @@ -0,0 +1,40 @@ +import type { Middleware } from "./common"; + +// A middleware has to be a function of type Middleware +const prettyError: Middleware = async (request, env, _ctx, middlewareCtx) => { + try { + const response = await middlewareCtx.next(request, env); + return response; + } catch (e: any) { + const html = ` + + + + + + + Error 🚨 + + + +
${e.stack}
+ + + `; + + return new Response(html, { + status: 500, + headers: { "Content-Type": "text/html;charset=utf-8" }, + }); + } +}; + +export default prettyError; diff --git a/node_modules/wrangler/templates/middleware/middleware-scheduled.ts b/node_modules/wrangler/templates/middleware/middleware-scheduled.ts new file mode 100644 index 0000000..3d9da28 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/middleware-scheduled.ts @@ -0,0 +1,15 @@ +import type { Middleware } from "./common"; + +// A middleware has to be a function of type Middleware +const scheduled: Middleware = async (request, env, _ctx, middlewareCtx) => { + const url = new URL(request.url); + if (url.pathname === "/__scheduled") { + const cron = url.searchParams.get("cron") ?? ""; + await middlewareCtx.dispatch("scheduled", { cron }); + + return new Response("Ran scheduled event"); + } + return middlewareCtx.next(request, env); +}; + +export default scheduled; diff --git a/node_modules/wrangler/templates/middleware/middleware-serve-static-assets.d.ts b/node_modules/wrangler/templates/middleware/middleware-serve-static-assets.d.ts new file mode 100644 index 0000000..88ff173 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/middleware-serve-static-assets.d.ts @@ -0,0 +1,6 @@ +declare module "config:middleware/serve-static-assets" { + import type { CacheControl } from "@cloudflare/kv-asset-handler"; + + export const spaMode: boolean; + export const cacheControl: Partial; +} diff --git a/node_modules/wrangler/templates/middleware/middleware-serve-static-assets.ts b/node_modules/wrangler/templates/middleware/middleware-serve-static-assets.ts new file mode 100644 index 0000000..4f72f40 --- /dev/null +++ b/node_modules/wrangler/templates/middleware/middleware-serve-static-assets.ts @@ -0,0 +1,56 @@ +/// + +import { + getAssetFromKV, + NotFoundError, + MethodNotAllowedError, + serveSinglePageApp, +} from "@cloudflare/kv-asset-handler"; +import type { Options } from "@cloudflare/kv-asset-handler"; +import { spaMode, cacheControl } from "config:middleware/serve-static-assets"; +import type * as kvAssetHandler from "@cloudflare/kv-asset-handler"; +import manifest from "__STATIC_CONTENT_MANIFEST"; +const ASSET_MANIFEST = JSON.parse(manifest); + +import type { Middleware } from "./common"; + +const staticAssets: Middleware = async (request, env, _ctx, middlewareCtx) => { + let options: Partial = { + ASSET_MANIFEST, + ASSET_NAMESPACE: env.__STATIC_CONTENT, + cacheControl: cacheControl, + mapRequestToAsset: spaMode ? serveSinglePageApp : undefined, + }; + + try { + const page = await (getAssetFromKV as typeof kvAssetHandler.getAssetFromKV)( + { + request, + waitUntil(promise) { + return _ctx.waitUntil(promise); + }, + }, + options + ); + + // allow headers to be altered + const response = new Response(page.body, page); + + response.headers.set("X-XSS-Protection", "1; mode=block"); + response.headers.set("X-Content-Type-Options", "nosniff"); + response.headers.set("X-Frame-Options", "DENY"); + response.headers.set("Referrer-Policy", "unsafe-url"); + response.headers.set("Feature-Policy", "none"); + + return response; + } catch (e) { + if (e instanceof NotFoundError || e instanceof MethodNotAllowedError) { + // if a known error is thrown then serve from actual worker + return await middlewareCtx.next(request, env); + } + // otherwise it's a real error, so throw it + throw e; + } +}; + +export default staticAssets; -- cgit v1.2.3