Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/constants/release-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { env } from 'cloudflare:workers';
import { type Env } from '../env';

const typedEnv = env as Env;

export type ReleaseConfig = {
bucket: R2Bucket;
directoryCache: KVNamespace;
};

export const MAINLINE_RELEASE_CONFIG: ReleaseConfig = {
bucket: typedEnv.R2_BUCKET,
directoryCache: typedEnv.DIRECTORY_CACHE,
};

export const UNOFFICIAL_BUILDS_RELEASE_CONFIG: ReleaseConfig = {
bucket: typedEnv.UNOFFICIAL_BUILDS_BUCKET,
directoryCache: typedEnv.UNOFFICIAL_BUILDS_DIRECTORY_CACHE,
};
4 changes: 4 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface Env {
*/
DIRECTORY_CACHE: KVNamespace;

UNOFFICIAL_BUILDS_BUCKET: R2Bucket;

UNOFFICIAL_BUILDS_DIRECTORY_CACHE: KVNamespace;

/**
* Temp flag for whether or not to use KV instead of S3
*/
Expand Down
39 changes: 24 additions & 15 deletions src/middleware/r2Middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@ import type { Request } from '../routes/request';
import { renderDirectoryListing } from '../utils/directoryListing';
import { getOriginalUrl, parseConditionalHeaders } from '../utils/request';
import { once } from '../utils/memo';

const getProvider = once((ctx: Context) => new R2Provider({ ctx }));
import { type ReleaseConfig } from '../constants/release-config';

export class R2Middleware implements Middleware {
#releaseConfig: ReleaseConfig;
#getProvider = once(
(ctx: Context) =>
new R2Provider({ ctx, releaseConfig: this.#releaseConfig })
);

constructor(releaseConfig: ReleaseConfig) {
this.#releaseConfig = releaseConfig;
}

async handle(request: Request, ctx: Context): Promise<Response> {
const path = getR2Path(request);
const isPathADirectory = isDirectoryPath(path);
Expand All @@ -29,32 +38,33 @@ export class R2Middleware implements Middleware {
},
});

const provider = this.#getProvider(ctx);
return isPathADirectory
? handleDirectory(request, path, ctx)
: handleFile(request, path, ctx);
? handleDirectory(request, path, provider)
: handleFile(request, path, provider);
}
}

async function handleDirectory(
request: Request,
r2Path: string,
ctx: Context
provider: R2Provider
): Promise<Response> {
if (!hasTrailingSlash(request.urlObj.pathname)) {
// We always want directory listing requests to have a trailing slash
const url = getOriginalUrl(request);
return Response.redirect(`${url}/`, 301);
}

const result = await getProvider(ctx).readDirectory(r2Path);
const result = await provider.readDirectory(r2Path);

if (result === undefined) {
return responses.directoryNotFound(request.method);
}

if (result.hasIndexHtmlFile) {
// Prioritize showing index files over directory listings
return handleFile(request, r2Path + 'index.html', ctx);
return handleFile(request, r2Path + 'index.html', provider);
}

let responseBody;
Expand Down Expand Up @@ -93,13 +103,13 @@ function responseHeaders(
function handleFile(
request: Request,
r2Path: string,
ctx: Context
provider: R2Provider
): Promise<Response> {
switch (request.method) {
case 'HEAD':
return headFile(request, r2Path, ctx);
return headFile(request, r2Path, provider);
case 'GET':
return getFile(request, r2Path, ctx);
return getFile(request, r2Path, provider);
}

throw new Error('R2Middleware handleFile unsupported method');
Expand All @@ -108,9 +118,9 @@ function handleFile(
async function headFile(
request: Request,
r2Path: string,
ctx: Context
provider: R2Provider
): Promise<Response> {
const result = await getProvider(ctx).headFile(r2Path);
const result = await provider.headFile(r2Path);

if (result === undefined) {
return responses.fileNotFound(request.method);
Expand All @@ -125,10 +135,8 @@ async function headFile(
async function getFile(
request: Request,
r2Path: string,
ctx: Context
provider: R2Provider
): Promise<Response> {
const provider = getProvider(ctx);

let result: GetFileResult | undefined;
try {
result = await provider.getFile(r2Path, {
Expand Down Expand Up @@ -161,6 +169,7 @@ async function getFile(
});
}

// TODO what to do here cause this is very dist-prod specific
function getR2Path({
urlObj,
params,
Expand Down
16 changes: 4 additions & 12 deletions src/providers/kvProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { KV_RETRY_LIMIT } from '../../lib/limits.mjs';
import type { Context } from '../context';
import { retryWrapper } from '../utils/provider';
import type {
GetFileOptions,
Expand All @@ -9,15 +8,11 @@ import type {
ReadDirectoryResult,
} from './provider';

type KvProviderCtorOptions = {
ctx: Context;
};

export class KvProvider implements Provider {
#ctx: Context;
#kvNamespace: KVNamespace;

constructor({ ctx }: KvProviderCtorOptions) {
this.#ctx = ctx;
constructor(kvNamespace: KVNamespace) {
this.#kvNamespace = kvNamespace;
}

headFile(_: string): Promise<HeadFileResult | undefined> {
Expand All @@ -30,10 +25,7 @@ export class KvProvider implements Provider {

async readDirectory(path: string): Promise<ReadDirectoryResult | undefined> {
const result = await retryWrapper(async () => {
return this.#ctx.env.DIRECTORY_CACHE.get<ReadDirectoryResult>(
path,
'json'
);
return this.#kvNamespace.get<ReadDirectoryResult>(path, 'json');
}, KV_RETRY_LIMIT);

if (result === null) {
Expand Down
20 changes: 11 additions & 9 deletions src/providers/r2Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
} from './provider';
import { S3Provider } from './s3Provider';
import { KvProvider } from './kvProvider';
import { type ReleaseConfig } from '../constants/release-config';

type CachedFile = {
name: string;
Expand All @@ -31,13 +32,18 @@ type CachedDirectory = {

type R2ProviderCtorOptions = {
ctx: Context;
releaseConfig: ReleaseConfig;
};

export class R2Provider implements Provider {
private ctx: Context;
#bucket: R2Bucket;
#kvProvider: KvProvider;

constructor({ ctx }: R2ProviderCtorOptions) {
constructor({ ctx, releaseConfig }: R2ProviderCtorOptions) {
this.ctx = ctx;
this.#bucket = releaseConfig.bucket;
this.#kvProvider = new KvProvider(releaseConfig.directoryCache);
}

async headFile(path: string): Promise<HeadFileResult | undefined> {
Expand All @@ -47,7 +53,7 @@ export class R2Provider implements Provider {
}

const object = await retryWrapper(
async () => await this.ctx.env.R2_BUCKET.head(path),
async () => await this.#bucket.head(path),
R2_RETRY_LIMIT
);

Expand Down Expand Up @@ -75,7 +81,7 @@ export class R2Provider implements Provider {
}

const object = await retryWrapper(async () => {
return this.ctx.env.R2_BUCKET.get(path, {
return this.#bucket.get(path, {
onlyIf: {
etagMatches: options?.conditionalHeaders?.ifMatch,
etagDoesNotMatch: options?.conditionalHeaders?.ifNoneMatch,
Expand All @@ -101,19 +107,15 @@ export class R2Provider implements Provider {
}

async readDirectory(path: string): Promise<ReadDirectoryResult | undefined> {
const kvProvider = new KvProvider({
ctx: this.ctx,
});

if (this.ctx.env.USE_KV) {
if (this.ctx.env.KV_DIRECTORIES !== undefined) {
for (const prefix of this.ctx.env.KV_DIRECTORIES) {
if (path.startsWith(prefix)) {
return await kvProvider.readDirectory(path);
return await this.#kvProvider.readDirectory(path);
}
}
} else {
return await kvProvider.readDirectory(path);
return await this.#kvProvider.readDirectory(path);
}
}

Expand Down
44 changes: 37 additions & 7 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import latestVersions from '../constants/latestVersions.json' assert { type: 'json' };
import {
MAINLINE_RELEASE_CONFIG,
UNOFFICIAL_BUILDS_RELEASE_CONFIG,
} from '../constants/release-config';
import { MethodNotAllowedMiddleware } from '../middleware/methodNotAllowedMiddleware';
import { NotFoundMiddleware } from '../middleware/notFoundMiddleware';
import { OptionsMiddleware } from '../middleware/optionsMiddleware';
Expand All @@ -7,18 +11,28 @@ import { R2Middleware } from '../middleware/r2Middleware';
import { RedirectionMiddleware } from '../middleware/redirectionMiddleware';
import { SubtitutionMiddleware } from '../middleware/subtituteMiddleware';
import { ThrowMiddleware } from '../middleware/throwMiddleware';
import type { Router } from './router';
import { Router } from './router';

export function registerRoutes(router: Router): void {
const r2Middleware = new R2Middleware();
/**
* Register routes shared by all routers
*/
function registerCommonRoutes(router: Router): void {
router.options('*', new OptionsMiddleware());
router.get('*', new NotFoundMiddleware());
router.head('*', new NotFoundMiddleware());
router.all('*', new MethodNotAllowedMiddleware());
}

export function getMainlineRouter(): Router {
const router = new Router();

const r2Middleware = new R2Middleware(MAINLINE_RELEASE_CONFIG);
const originMiddleware = new OriginMiddleware();

const corepackRedirectMiddleware = new RedirectionMiddleware(
'https://github.com/nodejs/corepack#readme'
);

router.options('*', new OptionsMiddleware());

router.head('/metrics/?:filePath+', r2Middleware, originMiddleware);
router.get('/metrics/?:filePath+', r2Middleware, originMiddleware);

Expand Down Expand Up @@ -64,9 +78,25 @@ export function registerRoutes(router: Router): void {

router.post('/_throw', new ThrowMiddleware());

router.get('*', new NotFoundMiddleware());
registerCommonRoutes(router);

router.all('*', new MethodNotAllowedMiddleware());
return router;
}

/**
* @see https://github.com/nodejs/build/blob/e4d53ee871b08a5ab34b0cde5727476db23b0c42/ansible/roles/nginx/templates/unofficial-builds.nodejs.org.conf.j2#L11
*/
export function getUnofficialBuildsRouter(): Router {
const router = new Router();

const r2Middleware = new R2Middleware(UNOFFICIAL_BUILDS_RELEASE_CONFIG);

router.head('*', r2Middleware);
router.get('*', r2Middleware);

registerCommonRoutes(router);

return router;
}

export * from './router';
32 changes: 28 additions & 4 deletions src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@ import * as Sentry from '@sentry/cloudflare';
import type { Env } from './env';
import responses from './responses';
import type { Context } from './context';
import { Router } from './routes/router';
import { registerRoutes } from './routes';
import {
getMainlineRouter,
getUnofficialBuildsRouter,
type Router,
} from './routes';

const router: Router = new Router();
registerRoutes(router);
const mainlineRouter = getMainlineRouter();
const unofficialBuildsRouter = getUnofficialBuildsRouter();

// TODO better place for this?
const hostnameToRouterMap: Record<string, Router> = {
'nodejs.org': mainlineRouter,
'r2.nodejs.org': mainlineRouter,
'dist-worker-prod.nodejs.workers.dev': mainlineRouter,
'r2-staging.nodejs.org': mainlineRouter,
'dist-worker-staging.nodejs.workers.dev': mainlineRouter,
'unofficial-builds.nodejs.org': unofficialBuildsRouter,
};

const handler = {
async fetch(
Expand All @@ -29,6 +42,17 @@ const handler = {
execution: ctx,
};

// todo pass this to the router so we're not doing it multiple times
const url = URL.parse(request.url);
if (url === null) {
return responses.badRequest();
}

const router = hostnameToRouterMap[url.hostname];
if (router === undefined) {
return responses.badRequest();
}

try {
const response: unknown = await router.fetch(request, context);

Expand Down
Loading
Loading