Skip to content
Closed
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
162 changes: 124 additions & 38 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,122 @@ function hasParameter(parameters: Record<string, string>, name: string) {
return Object.getOwnPropertyDescriptor(parameters, name) !== undefined;
}

type RequestProtocol = "http" | "https" | "ws" | "wss";

const commonPathname = /^\/[A-Za-z0-9/_-]*$/;
const commonHost = /^[A-Za-z0-9.-]+(?::[0-9]+)?$/;
const commonDomainHost = /^[A-Za-z]/;
const commonIpv4 = /^(?:0|[1-9][0-9]{0,2})(?:\.(?:0|[1-9][0-9]{0,2})){3}$/;
const safePathname = /^\/[A-Za-z0-9\-._~!$&'()*+,;=:@/%]*$/;
const dotPathSegment = /(?:^|\/)(?:(?:\.|%2e){1,2})(?:\/|$)/i;
const requestHost = Symbol();
const requestTarget = Symbol();
const requestUrl = Symbol();

interface LazyRequestContext extends RequestContext {
[requestHost]: string;
[requestTarget]: string;
[requestUrl]?: URL;
}

function createRequestUrlDescriptor(
protocol: RequestProtocol,
): PropertyDescriptor {
return {
configurable: true,
enumerable: true,
get(this: LazyRequestContext) {
this[requestUrl] ??= new URL(
this[requestTarget],
`${protocol}://${this[requestHost]}`,
);
return this[requestUrl];
},
set(this: LazyRequestContext, url: URL) {
this[requestUrl] = url;
},
};
}

const requestUrlDescriptors: Record<RequestProtocol, PropertyDescriptor> = {
http: createRequestUrlDescriptor("http"),
https: createRequestUrlDescriptor("https"),
ws: createRequestUrlDescriptor("ws"),
wss: createRequestUrlDescriptor("wss"),
};

function isCommonIpv4(hostname: string): boolean {
if (!commonIpv4.test(hostname)) {
return false;
}
let octet = 0;
for (const character of hostname) {
if (character === ".") {
octet = 0;
} else {
octet = octet * 10 + character.charCodeAt(0) - 48;
if (octet > 255) {
return false;
}
}
}
return true;
}

function isCommonHost(host: string): boolean {
if (!commonHost.test(host)) {
return false;
}
const portDelimiter = host.lastIndexOf(":");
if (portDelimiter >= 0 && Number(host.slice(portDelimiter + 1)) > 65_535) {
return false;
}
const hostname = host.slice(0, portDelimiter < 0 ? undefined : portDelimiter);
if (commonDomainHost.test(hostname)) {
return true;
}
return isCommonIpv4(hostname);
}

function createRequestContext(
req: IncomingMessage,
res: ServerResponse,
protocol: RequestProtocol,
): RequestContext {
const context = {
rawRequest: req,
rawResponse: res,
[requestHost]: req.headers.host || "localhost",
[requestTarget]: req.url || "",
routeParameters: {},
response: new HTTPResult(404, "Not Found"),
} as unknown as LazyRequestContext;
Object.defineProperty(context, "url", requestUrlDescriptors[protocol]);
if (!isCommonHost(context[requestHost])) {
void context.url;
}
return context;
}
Comment thread
Upd4ting marked this conversation as resolved.

function getPathname(requestContext: RequestContext): string {
const requestTarget = requestContext.rawRequest.url;
if (!requestTarget || requestTarget.startsWith("//")) {
return requestContext.url.pathname;
}

const delimiterIndex = requestTarget.search(/[?#]/);
const pathname = requestTarget.slice(
0,
delimiterIndex < 0 ? undefined : delimiterIndex,
);
if (commonPathname.test(pathname)) {
return pathname;
}
return safePathname.test(pathname) && !dotPathSegment.test(pathname)
? pathname
: requestContext.url.pathname;
}

function findHandlers(
path: string[],
depth: number,
Expand Down Expand Up @@ -828,27 +944,13 @@ function processRequest(
res: ServerResponse,
protocol: "http" | "https",
): Awaitable<void> {
const url = new URL(
req.url || "",
`${protocol}://${req.headers.host || "localhost"}`,
);
const requestContext: RequestContext = {
rawRequest: req,
rawResponse: res,
url,
routeParameters: {},
response: new HTTPResult(404, "Not Found"),
};
const path = url.pathname.split("/").filter((part) => part);
const requestContext = createRequestContext(req, res, protocol);
const pathname = getPathname(requestContext);
const path = pathname.split("/").filter(Boolean);
const method = req.method?.toLowerCase() || "get";

try {
const execution = executeRequest(
method,
path,
url.pathname,
requestContext,
);
const execution = executeRequest(method, path, pathname, requestContext);
const then = getThen(execution);
if (then) {
return resolveThenable(execution, then).then(
Expand Down Expand Up @@ -887,33 +989,17 @@ export async function upgradeListener(
protocol: "ws" | "wss",
) {
const res = new ServerResponse(req);
const url = new URL(
req.url || "",
`${protocol}://${req.headers.host || "localhost"}`,
);
const requestContext: RequestContext = {
rawRequest: req,
rawResponse: res,
url,
routeParameters: {},
response: new HTTPResult(404, "Not Found"),
};

const path = url.pathname.split("/").filter((part) => part);
const requestContext = createRequestContext(req, res, protocol);
const pathname = getPathname(requestContext);
const path = pathname.split("/").filter(Boolean);
const method = req.method?.toLowerCase() || "get";
let requestError: unknown;
let hasUpgradedConnection = false;
let mustSendResponse = false;
let mustDestroySocket = false;

try {
const handler = getHandler(
method,
path,
roots.websocket,
false,
url.pathname,
);
const handler = getHandler(method, path, roots.websocket, false, pathname);
if (!handler || Array.isArray(handler)) {
mustSendResponse = true;
mustDestroySocket = true;
Expand Down
Loading
Loading