Skip to content

Instantly share code, notes, and snippets.

@jasonbyrne
Last active March 6, 2021 12:37
Show Gist options
  • Save jasonbyrne/d59aa1a43ff7fe03dda8f0b2c4ef4be2 to your computer and use it in GitHub Desktop.
Save jasonbyrne/d59aa1a43ff7fe03dda8f0b2c4ef4be2 to your computer and use it in GitHub Desktop.
CloudFlare Workers Router in TypeScript
import { Router } from "./helpers/router";
import { getQueryString } from "./helpers/querystring";
const MAGIC_WORD = "flo";
function catchAll(request: Request): Response {
return fetch(request.url, request);
}
function validateTokenThenFetch(
request: Request
): Promise<Response> | Response {
const qs = getQueryString(request);
const requestToken = qs.token || "";
if (!requestToken) {
return new Response("No token", { status: 401 });
}
if (requestToken !== MAGIC_WORD) {
return new Response("Invalid token", { status: 403 });
}
return fetch(request.url, request);
}
async function handleRequest(event: FetchEvent) {
const r = new Router();
// Protect anything in /premium folder
r.any(/^\/premium\/.*/, e => validateTokenThenFetch(e.request));
// But allow anything else
r.all(e => catchAll(e.request));
return event.respondWith(r.route(event));
}
addEventListener("fetch", handleRequest);
export function getQueryString(request: Request): { [key: string]: string } {
const params = {};
const url = new URL(request.url);
const queryString = url.search.slice(1).split("&");
queryString.forEach(item => {
const kv = item.split("=");
if (kv[0]) {
params[kv[0]] = kv[1] || true;
}
});
return params;
}
/**
* Helper functions that when passed a request will return a
* boolean indicating if the request uses that HTTP method,
* header, host or referrer.
*/
const Method = (method: string) => (req: Request) =>
req.method.toLowerCase() === method.toLowerCase();
const Connect = Method("connect");
const Delete = Method("delete");
const Get = Method("get");
const Head = Method("head");
const Options = Method("options");
const Patch = Method("patch");
const Post = Method("post");
const Put = Method("put");
const Trace = Method("trace");
const Header = (header: string, val: string) => (req: Request) =>
req.headers.get(header) === val;
const Host = (host: string) => Header("host", host.toLowerCase());
const Referrer = (host: string) => Header("referrer", host.toLowerCase());
const Path = (pattern: RegExp) => (req: Request): boolean => {
const path = new URL(req.url).pathname;
return path.match(pattern) !== null;
};
type Condition = (req: Request) => boolean;
type Handler = (e: FetchEvent) => Response | Promise<Response>;
interface Route {
conditions: Condition[];
handler: Handler;
}
/**
* The Router handles determines which handler is matched given the
* conditions present for each request.
*/
export class Router {
public routes: Route[] = [];
handle(conditions: Condition[], handler: Handler) {
this.routes.push({
conditions,
handler
});
return this;
}
connect(pattern: RegExp, handler: Handler) {
return this.handle([Connect, Path(pattern)], handler);
}
delete(pattern: RegExp, handler: Handler) {
return this.handle([Delete, Path(pattern)], handler);
}
get(pattern: RegExp, handler: Handler) {
return this.handle([Get, Path(pattern)], handler);
}
head(pattern: RegExp, handler: Handler) {
return this.handle([Head, Path(pattern)], handler);
}
options(pattern: RegExp, handler: Handler) {
return this.handle([Options, Path(pattern)], handler);
}
patch(pattern: RegExp, handler: Handler) {
return this.handle([Patch, Path(pattern)], handler);
}
post(pattern: RegExp, handler: Handler) {
return this.handle([Post, Path(pattern)], handler);
}
put(pattern: RegExp, handler: Handler) {
return this.handle([Put, Path(pattern)], handler);
}
trace(pattern: RegExp, handler: Handler) {
return this.handle([Trace, Path(pattern)], handler);
}
any(pattern: RegExp, handler: Handler) {
return this.handle([Path(pattern)], handler);
}
host(hostName: string, handler: Handler) {
return this.handle([Host(hostName)], handler);
}
referrer(referrer: string, handler: Handler) {
return this.handle([Referrer(referrer)], handler);
}
all(handler: Handler) {
return this.handle([], handler);
}
route(e: FetchEvent): Response | Promise<Response> {
// Find a route that matches
const route = this.resolve(e.request);
// If we found one
if (route !== undefined) {
return route.handler(e);
}
// If not, fall back to a 404
return new Response("resource not found", {
status: 404,
statusText: "not found",
headers: {
"content-type": "text/plain"
}
});
}
/**
* resolve returns the matching route for a request that returns
* true for all conditions (if any).
*/
resolve(req: Request): Route | undefined {
return this.routes.find(r => {
// If there are no conditions, this one automatically matches
if (
!r.conditions ||
(Array.isArray(r.conditions) && !r.conditions.length)
) {
return true;
}
// Otherwise, they all have to match
return r.conditions.every(c => c(req));
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment