180 lines
4.5 KiB
TypeScript
180 lines
4.5 KiB
TypeScript
import { ABC, Base64, Path } from "./deps.ts";
|
|
import config from "./config.ts";
|
|
import * as Storage from "./storage.ts";
|
|
|
|
|
|
export const isValidPackageName = (name: string) => /^[@]?[a-zA-Z][\d\w\-\_]*$/g.test(name);
|
|
export const isValidVersion = (version: string) =>
|
|
/^\d+(\.\d+)?(\.\d+)?$/g.test(version);
|
|
export const isValidFullVersion = (version: string) =>
|
|
/^\d+(\.\d+)(\.\d+)$/g.test(version);
|
|
|
|
const ALg = 1;
|
|
const ASm = -ALg;
|
|
const Equ = 0;
|
|
|
|
export const sortVersions = (a: string, b: string) => {
|
|
const [a1, a2, a3] = a.split(".").map(Number);
|
|
const [b1, b2, b3] = b.split(".").map(Number);
|
|
|
|
if (a1 > b1) return ALg;
|
|
if (a1 < b1) return ASm;
|
|
|
|
if (a2 > b2) return ALg;
|
|
if (a2 < b2) return ASm;
|
|
|
|
if (a3 > b3) return ALg;
|
|
if (a3 < b3) return ASm;
|
|
|
|
return Equ;
|
|
};
|
|
|
|
export const basicauth = (realm: string) => (next: ABC.HandlerFunc) => (
|
|
ctx: ABC.Context
|
|
) => {
|
|
const value = ctx.request.headers.get("authorization");
|
|
|
|
console.log("Header:", value);
|
|
|
|
if (value && value.toLowerCase().startsWith("basic ")) {
|
|
const credentials = value.slice(6);
|
|
const [username, passwd] = new TextDecoder()
|
|
.decode(Base64.decode(credentials))
|
|
.split(":", 2);
|
|
|
|
if (config?.user[username]?.password === passwd) {
|
|
console.log("User authenticated!");
|
|
if (!ctx.customContext) ctx.customContext = {};
|
|
ctx.customContext.user = username;
|
|
return next(ctx);
|
|
}
|
|
}
|
|
|
|
console.log("Authentication required");
|
|
ctx.response.status = 401;
|
|
ctx.response.headers.set("WWW-Authenticate", "Basic realm=" + realm);
|
|
return {
|
|
statusCode: 401,
|
|
error: "Authentication required",
|
|
};
|
|
};
|
|
|
|
export function extractPackagePath(path: string): [string, string | undefined] {
|
|
let packageName = "";
|
|
path = path.toLowerCase();
|
|
|
|
if (path.startsWith("@")) {
|
|
packageName = "@";
|
|
path = path.slice(1);
|
|
}
|
|
|
|
|
|
const parts = path.split("@");
|
|
if (parts.length > 2) throw new Error("Invalid package name!");
|
|
|
|
packageName += parts[0];
|
|
const packageVersion: string | undefined = parts[1];
|
|
console.log({ path, parts, packageName, packageVersion });
|
|
|
|
if (!isValidPackageName(packageName))
|
|
throw new Error("Invalid package name!");
|
|
|
|
if (packageVersion && packageVersion !== "") {
|
|
if (!isValidVersion(packageVersion))
|
|
throw new Error("Invalid package version!");
|
|
}
|
|
|
|
return [packageName, packageVersion];
|
|
}
|
|
|
|
import type { IPackage } from "./db.ts";
|
|
|
|
export function getAbsolutePackageVersion(
|
|
pkg?: IPackage | null,
|
|
version?: string
|
|
) {
|
|
if (!pkg || pkg.versions.length < 1) return undefined;
|
|
|
|
const versions = pkg.versions.sort(sortVersions).reverse();
|
|
|
|
if (!version || version === "latest") {
|
|
version = versions[0];
|
|
} else {
|
|
const v = versions.filter((e) => e.startsWith(version as string));
|
|
if (v.length < 1) return undefined;
|
|
version = v[0];
|
|
}
|
|
|
|
return version;
|
|
}
|
|
|
|
export function getFilePath(
|
|
pkgName: string,
|
|
version: string,
|
|
file: string
|
|
) {
|
|
if (file.startsWith("/")) {
|
|
file = file.substr(1);
|
|
}
|
|
|
|
const bucketPath = (
|
|
pkgName +
|
|
"/" +
|
|
version +
|
|
"/" +
|
|
file
|
|
).replace(/@/g, "§");
|
|
|
|
return bucketPath;
|
|
}
|
|
|
|
export async function getOneOf(pkgName: string, version: string, files: (string | undefined)[]) {
|
|
for (const file of files) {
|
|
if (!file) continue;
|
|
const res = await getFile(pkgName, version, file);
|
|
if (res) return res;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export async function getFile(
|
|
pkgName: string,
|
|
version: string | undefined,
|
|
file: string
|
|
): Promise<{ etag: string; data: Uint8Array } | null | undefined> {
|
|
if (!version) return undefined;
|
|
const bucketPath = await getFilePath(pkgName, version, file);
|
|
if (!bucketPath) return null;
|
|
|
|
console.log("Getting file from:", bucketPath);
|
|
|
|
try {
|
|
const res = await Storage.readFile(bucketPath);
|
|
if (!res) return undefined;
|
|
|
|
return {
|
|
etag: Base64.encodeBase64(await crypto.subtle.digest("sha-1", res)),
|
|
data: res,
|
|
};
|
|
} catch (err) {
|
|
const msg = err.message as string;
|
|
if (msg.indexOf("404") >= 0) return null;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
const exts = new Map<string, string>();
|
|
|
|
exts.set(".js", "text/javascript");
|
|
exts.set(".mjs", "text/javascript");
|
|
exts.set(".json", "application/json");
|
|
exts.set(".jsonld", "application/ld+json");
|
|
exts.set(".css", "text/css");
|
|
exts.set(".html", "text/html");
|
|
exts.set(".htm", "text/html");
|
|
|
|
export function getContentType(filename: string) {
|
|
const ext = Path.extname(filename);
|
|
return exts.get(ext);
|
|
}
|