@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
240 lines (237 loc) • 8.84 kB
JavaScript
import bytes from '../bytes.js';
import { as } from '../object.js';
import Exception from '../error/Exception.js';
import '../external/event-target-polyfill/index.js';
import { getMIME } from '../filetype.js';
import { exists, readFile, readDir } from './fs.js';
import { sha256 } from '../hash/web.js';
import { renderDirectoryPage } from '../http/internal.js';
export { withWeb } from '../http/internal.js';
import { Server } from '../http/server.js';
import { parseRange, ifNoneMatch, ifMatch } from '../http/util.js';
export { HTTP_METHODS, HTTP_STATUS, getCookie, getCookies, parseAccepts, parseBasicAuth, parseContentType, parseCookie, parseCookies, parseRequest, parseResponse, setCookie, setFilename, stringifyCookie, stringifyCookies, stringifyRequest, stringifyResponse, suggestResponseType, verifyBasicAuth } from '../http/util.js';
import { join, extname } from '../path.js';
import { readAsArray } from '../reader.js';
import { stripStart } from '../string.js';
import { WebSocketServer } from './ws.js';
import runtime from '../runtime.js';
import { startsWith } from '../path/util.js';
export { parseUserAgent } from '../http/user-agent.js';
async function etag(data) {
var _a;
if (typeof data === "string" || data instanceof Uint8Array) {
if (!data.length) {
// a short circuit for zero length entities
return `0-47DEQpj8HBSa+/TImW+5JCeuQeR`;
}
if (typeof data === "string") {
data = bytes(data);
}
const hash = await sha256(data, "base64");
return `${data.length.toString(16)}-${hash.slice(0, 27)}`;
}
const mtime = (_a = data.mtime) !== null && _a !== void 0 ? _a : new Date();
const hash = await sha256(mtime.toISOString(), "base64");
return `${data.size.toString(16)}-${hash.slice(0, 27)}`;
}
async function randomPort(prefer = undefined) {
throw new Error("Unsupported runtime");
}
function serve(options) {
const { identity } = runtime();
const type = identity === "workerd" ? options.type || "classic" : "classic";
const ws = new WebSocketServer(options.ws);
const { fetch, onError, onListen, headers } = options;
// @ts-ignore
return new Server(async () => {
return { http: null, hostname: "", port: 0 };
}, { type, fetch, onError, onListen, ws, headers });
}
async function serveStatic(req, options = {}) {
var _a, _b, _c, _d, _e, _f;
// @ts-ignore
const kv = (_a = options.kv) !== null && _a !== void 0 ? _a : globalThis["__STATIC_CONTENT"];
if (!kv) {
return new Response("Service Unavailable", {
status: 503,
statusText: "Service Unavailable",
});
}
const extraHeaders = (_b = options.headers) !== null && _b !== void 0 ? _b : {};
const prefix = options.urlPrefix ? join(options.urlPrefix) : "";
const url = new URL(req.url);
const pathname = decodeURIComponent(url.pathname);
if (prefix && !startsWith(pathname, prefix)) {
return new Response("Not Found", {
status: 404,
statusText: "Not Found",
headers: extraHeaders,
});
}
let filename = stripStart(pathname.slice(prefix.length), "/");
if (filename === "/" || filename === ".") {
filename = "";
}
if (pathname.endsWith("/")) {
const indexHtml = filename ? join(filename, "index.html") : "index.html";
const indexHtm = filename ? join(filename, "index.htm") : "index.htm";
if (await exists(indexHtml, { root: kv })) {
const data = await readFile(indexHtml, { root: kv });
return await serveFile(data, {
filename: indexHtml,
reqHeaders: req.headers,
extraHeaders,
maxAge: (_c = options.maxAge) !== null && _c !== void 0 ? _c : 0,
});
}
else if (await exists(indexHtm, { root: kv })) {
const data = await readFile(indexHtm, { root: kv });
return await serveFile(data, {
filename: indexHtm,
reqHeaders: req.headers,
extraHeaders,
maxAge: (_d = options.maxAge) !== null && _d !== void 0 ? _d : 0,
});
}
else if (options.listDir) {
const entries = await readAsArray(readDir(filename, { root: kv }));
return renderDirectoryPage(pathname, entries, extraHeaders);
}
else {
return new Response("Forbidden", {
status: 403,
statusText: "Forbidden",
headers: extraHeaders,
});
}
}
else if (filename) {
try {
const data = await readFile(filename, { root: kv });
return await serveFile(data, {
filename,
reqHeaders: req.headers,
extraHeaders,
maxAge: (_e = options.maxAge) !== null && _e !== void 0 ? _e : 0,
});
}
catch (err) {
if (((_f = as(err, Exception)) === null || _f === void 0 ? void 0 : _f.name) === "NotFoundError") {
return new Response("Not Found", {
status: 404,
statusText: "Not Found",
headers: extraHeaders,
});
}
else {
return new Response("Internal Server Error", {
status: 500,
statusText: "Internal Server Error",
headers: extraHeaders,
});
}
}
}
else {
return Response.redirect(req.url + "/", 301);
}
}
async function serveFile(data, options) {
var _a, _b;
const { filename, reqHeaders, extraHeaders } = options;
const ext = extname(filename);
const type = (_a = getMIME(ext)) !== null && _a !== void 0 ? _a : "";
const rangeValue = reqHeaders.get("Range");
let range;
if (rangeValue && data.byteLength) {
try {
range = parseRange(rangeValue);
}
catch (_c) {
return new Response("Invalid Range header", {
status: 416,
statusText: "Range Not Satisfiable",
headers: extraHeaders,
});
}
}
const _etag = await etag(data);
const headers = new Headers({
...extraHeaders,
"Accept-Ranges": "bytes",
"Etag": _etag,
});
const ifNoneMatchValue = reqHeaders.get("If-None-Match");
const ifMatchValue = reqHeaders.get("If-Match");
let modified = true;
if (ifNoneMatchValue) {
modified = ifNoneMatch(ifNoneMatchValue, _etag);
}
if (!modified) {
return new Response(null, {
status: 304,
statusText: "Not Modified",
headers,
});
}
else if (ifMatchValue && range && !ifMatch(ifMatchValue, _etag)) {
return new Response("Precondition Failed", {
status: 412,
statusText: "Precondition Failed",
headers,
});
}
if (type) {
if (/^text\/|^application\/(json|yaml|toml|xml|javascript)$/.test(type)) {
headers.set("Content-Type", type + "; charset=utf-8");
}
else {
headers.set("Content-Type", type);
}
}
else {
headers.set("Content-Type", "application/octet-stream");
}
if (options.maxAge) {
headers.set("Cache-Control", `public, max-age=${options.maxAge}`);
}
if (range) {
const { ranges, suffix: suffixLength } = range;
let start;
let end;
if (ranges.length) {
({ start } = ranges[0]);
end = Math.min((_b = ranges[0].end) !== null && _b !== void 0 ? _b : data.byteLength - 1, data.byteLength - 1);
}
else {
start = Math.max(data.byteLength - suffixLength, 0);
end = data.byteLength - 1;
}
const slice = data.subarray(start, end + 1);
headers.set("Content-Range", `bytes ${start}-${end}/${data.byteLength}`);
headers.set("Content-Length", String(end - start + 1));
return new Response(slice, {
status: 206,
statusText: "Partial Content",
headers,
});
}
else if (!data.byteLength) {
headers.set("Content-Length", "0");
return new Response("", {
status: 200,
statusText: "OK",
headers,
});
}
else {
headers.set("Content-Length", String(data.byteLength));
return new Response(data, {
status: 200,
statusText: "OK",
headers,
});
}
}
export { etag, ifMatch, ifNoneMatch, parseRange, randomPort, serve, serveStatic };
//# sourceMappingURL=http.js.map