Last active
March 6, 2021 12:37
-
-
Save jasonbyrne/d59aa1a43ff7fe03dda8f0b2c4ef4be2 to your computer and use it in GitHub Desktop.
CloudFlare Workers Router in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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