You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
176 lines
4.3 KiB
TypeScript
176 lines
4.3 KiB
TypeScript
import { contentType } from "./deps/mediaTypes.ts";
|
|
import { extname } from "./deps/path.ts";
|
|
|
|
export type Promise_<T> = T | Promise<T>;
|
|
export type MaybeHTTPHandler = (req: Request) => Promise_<Response | null>;
|
|
export type HTTPHandler = (req: Request) => Promise_<Response>;
|
|
export type HTTPHandlerWith<T> = (req: Request, t: T) => Promise_<Response>;
|
|
|
|
export interface CompressOptions {
|
|
sizeThreshold: number,
|
|
preferredAlgorithms: ("gzip" | "deflate")[],
|
|
ignoredMimetypes: ((s: string) => boolean)[],
|
|
}
|
|
export const compressResponse = (handler: HTTPHandler, options?: Partial<CompressOptions>) => {
|
|
const o = {
|
|
sizeThreshold: 512,
|
|
preferredAlgorithms: ["gzip", "deflate"] as CompressOptions["preferredAlgorithms"],
|
|
ignoredMimetypes: [(s) => s.includes("font")],
|
|
...options,
|
|
} as CompressOptions;
|
|
|
|
return (async (req) => {
|
|
const supportedEncodings = (() => {
|
|
const accepted = req.headers.get("accept-encoding");
|
|
if (!accepted) {
|
|
return null;
|
|
}
|
|
|
|
const encodings = accepted.split(',').map(s => s.trim().split(";")[0]);
|
|
|
|
return encodings;
|
|
})();
|
|
const encoding = (() => {
|
|
if (!supportedEncodings) {
|
|
return null;
|
|
}
|
|
|
|
const encoding = o.preferredAlgorithms.find(a => supportedEncodings.includes(a));
|
|
|
|
return encoding ? [encoding, new CompressionStream(encoding)] as [string, CompressionStream] : null;
|
|
})();
|
|
|
|
const res = await handler(req);
|
|
|
|
const contentType = res.headers.get("content-type");
|
|
const body = res.body;
|
|
if (contentType && body && encoding) {
|
|
for (const p of o.ignoredMimetypes) {
|
|
if (p(contentType)) {
|
|
return res;
|
|
}
|
|
}
|
|
|
|
const contentLength = res.headers.get("content-length");
|
|
if (contentLength && Number(contentLength) < o.sizeThreshold) {
|
|
return res;
|
|
}
|
|
|
|
const [bestEncoding, bestEncoder] = encoding;
|
|
res.headers.set("content-encoding", bestEncoding);
|
|
|
|
const body_ = res.body.pipeThrough(bestEncoder);
|
|
const res_ = new Response(body_, {
|
|
headers: res.headers,
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
});
|
|
return res_;
|
|
}
|
|
|
|
return res;
|
|
}) as HTTPHandler;
|
|
};
|
|
|
|
export const setHeaders = (headers: [string, string][], handler: HTTPHandler) => {
|
|
return (async (req) => {
|
|
const res = await handler(req);
|
|
for (const [header, value] of headers) {
|
|
res.headers.set(header, value);
|
|
}
|
|
|
|
return res;
|
|
}) as HTTPHandler;
|
|
};
|
|
|
|
export const orElse = (mh: MaybeHTTPHandler, h: HTTPHandler) => {
|
|
return (async (req) => {
|
|
const mhr = await mh(req);
|
|
if (mhr) {
|
|
return mhr;
|
|
}
|
|
|
|
return h(req);
|
|
}) as HTTPHandler;
|
|
};
|
|
|
|
export const filter = (pred: (r: Request) => Promise_<boolean>, h: HTTPHandler) => {
|
|
return (async (req) => {
|
|
if (!await pred(req)) {
|
|
return null;
|
|
}
|
|
|
|
return h(req);
|
|
}) as MaybeHTTPHandler;
|
|
};
|
|
|
|
export const catching = (h: HTTPHandler, fallback: HTTPHandlerWith<unknown>) => {
|
|
return ((req) => {
|
|
try {
|
|
return h(req);
|
|
} catch (e) {
|
|
return fallback(req, e);
|
|
}
|
|
}) as HTTPHandler;
|
|
};
|
|
|
|
export const log = (h: HTTPHandler) => {
|
|
return (async (req) => {
|
|
const start = performance.now();
|
|
const res = await h(req);
|
|
|
|
const diff = performance.now() - start;
|
|
|
|
console.log(`${req.method} ${req.url} ${res.status} [${diff}ms]`);
|
|
|
|
return res;
|
|
}) as HTTPHandler;
|
|
}
|
|
|
|
export const serveDir = (dir: string, notFoundFallback: HTTPHandler) => {
|
|
return (async (req) => {
|
|
const url = new URL(req.url);
|
|
const path = url.pathname.slice(1);
|
|
if (path.includes("..")) {
|
|
return notFoundFallback(req);
|
|
}
|
|
|
|
let joined = `${dir}/${path}`;
|
|
if (joined.endsWith("/")) {
|
|
joined = `${joined}index.html`;
|
|
}
|
|
|
|
try {
|
|
const st = await Deno.stat(joined);
|
|
const ifModifiedSince = req.headers.get("if-modified-since");
|
|
const lm = new Date(st.mtime ?? 0);
|
|
lm.setMilliseconds(0);
|
|
if (ifModifiedSince) {
|
|
const imsTime = new Date(ifModifiedSince).getTime();
|
|
const lmTime = lm.getTime();
|
|
|
|
if (imsTime >= lmTime) {
|
|
return new Response(null, { status: 304 });
|
|
}
|
|
}
|
|
|
|
const f = await Deno.open(joined);
|
|
const ext = extname(joined);
|
|
|
|
return new Response(f.readable, {
|
|
headers: {
|
|
"content-size": st.size.toString(),
|
|
"content-type": contentType(ext) as string,
|
|
"last-modified": lm.toUTCString(),
|
|
},
|
|
});
|
|
} catch (e) {
|
|
if (e === Deno.errors.NotFound) {
|
|
return notFoundFallback(req);
|
|
}
|
|
|
|
throw e;
|
|
}
|
|
}) as HTTPHandler;
|
|
}
|