diff --git a/Dockerfile b/Dockerfile index 635789b..328f7e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:alpine +FROM oven/bun:slim # FROM oven/bun WORKDIR /app # RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories diff --git a/bun.lockb b/bun.lockb index dcfa59f..7bd773f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 3cc6cee..8430dfb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dependencies": { "@elysiajs/cors": "^0.8.0", "@elysiajs/html": "^0.8.0", + "@elysiajs/jwt": "^0.8.0", "@elysiajs/static": "^0.8.1", "elysia": "latest", "gunzip-maybe": "^1.4.2", diff --git a/src/config.ts b/src/config.ts index 8a18831..838a1d2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,11 +10,7 @@ export const BunPkgConfig = { return process.env.HOST_NAME ?? "0.0.0.0"; }, get origin() { - return ( - process.env.ORIGIN || - `${process.env.HOST_NAME}:${process.env.PORT}` || - "0.0.0.0" - ); + return process.env.ORIGIN || `http://localhost${process.env.PORT}`; }, cors: { origin: /^\//.test(Bun.env.CORS_ORIGIN ?? "") @@ -28,19 +24,40 @@ export const BunPkgConfig = { const maybe = Number(Bun.env.CACHE_GIB); return Number.isNaN(maybe) ? 4 : maybe; }, - set cacheDir(dir: string) { - Bun.env.CACHEW_DIR = dir; - }, - get npmAuthToken() { - return Bun.env.NPM_AUTH_TOKEN; - }, + get npmRegistryURL() { return (Bun.env.NPM_REGISTRY_URL || "https://registry.npmjs.org").replace( /\/$/, "", ); }, - set npmRegistryURL(neo: string) { - Bun.env.NPM_REGISTRY_URL = neo; + get npmAuthToken() { + return Bun.env.NPM_AUTH_TOKEN; }, + get jwtSecret() { + return Bun.env.JWT_SECRET; + }, + get jwtUserList() { + return (Bun.env.JWT_USERS || "").split(",").map((item) => item.trim()); + }, + // TODO: The list to ban some packages or scopes. + // banList: { + // packages: ["@some_scope/package_name"], + // scopes: [ + // { + // name: "@your_scope", + // excludes: ["package_name"], + // }, + // ], + // }, + + // // TODO: The list to only allow some packages or scopes. + // allowList: { + // packages: ["@some_scope/package_name"], + // scopes: [ + // { + // name: "@your_scope", + // }, + // ], + // }, }; diff --git a/src/index.ts b/src/index.ts index 8c1bd3a..19b9a7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,9 @@ +import { cors } from "@elysiajs/cors"; import { Elysia } from "elysia"; -import fs from "fs"; import { router } from "./routes/router"; -import { cors } from "@elysiajs/cors"; - -import { BunPkgConfig } from "./config"; import path from "path"; +import { jwt } from "@elysiajs/jwt"; +import { BunPkgConfig } from "./config"; import { err } from "./templates"; // static file server @@ -18,23 +17,70 @@ Bun.serve({ }, }); -new Elysia() +const app = new Elysia(); + +app .use(cors(BunPkgConfig.cors)) + .use( + jwt({ + name: "jwt", + secret: BunPkgConfig.jwtSecret || "NONE", + }), + ) .get("favicon.ico", () => { return new Response(""); }) .get("", ({ set }) => { set.redirect = "/"; }) - .get("/browser/*", (ctx) => { - return "TODO: file browser"; - }) - .get("/", () => { - const resp = new Response(Bun.file("src/templates/BUNPKG.html")); - resp.headers.set("Content-Type", "text/html; charset=utf8"); - return resp; - }) - .use(router) + .guard( + { + async beforeHandle({ set, jwt, path, cookie: { auth } }) { + const hasJwt = BunPkgConfig.jwtSecret; + if (!hasJwt) return; + const isSign = /\/_sign\/\w+/.test(path); + if (isSign) return; + + const profile = await jwt.verify(auth.value); + // console.log("๐Ÿš€ ~ beforeHandle ~ profile:", profile); + + if (!profile) { + set.status = 401; + return "Unauthorized"; + } + }, + }, + + (app) => + app + .get("/_sign/:name", async ({ jwt, set, cookie: { auth }, params }) => { + const userlist = BunPkgConfig.jwtUserList; + // TODO: sqlite user.db + if (!userlist.includes(params.name)) { + set.status = 401; + return "Unauthorized"; + } else { + auth.set({ + value: await jwt.sign(params), + httpOnly: true, + maxAge: 7 * 86400, + path: "/", + }); + + return "welcome"; + } + }) + + .get("/browser/*", (ctx) => { + return "TODO: file browser"; + }) + .get("/", () => { + const resp = new Response(Bun.file("src/templates/BUNPKG.html")); + resp.headers.set("Content-Type", "text/html; charset=utf8"); + return resp; + }) + .use(router), + ) .onError(({ code, error }) => { const resp = new Response( @@ -53,8 +99,21 @@ new Elysia() port: BunPkgConfig.PORT, }); +const black = /npmAuthToken|jwtSecret/; + console.log( `BUNPKG is Running at http://${BunPkgConfig.HOST_NAME}:${BunPkgConfig.PORT}`, "with BunConfig :\n", - JSON.stringify(BunPkgConfig, null, 2), + JSON.stringify( + Object.keys(BunPkgConfig).reduce( + (ok: any, key) => { + if (black.test(key)) return ok; + ok[key] = BunPkgConfig[key as keyof typeof BunPkgConfig]; + return ok; + }, + {} as typeof BunPkgConfig, + ), + null, + 2, + ), ); diff --git a/src/routes/meta.ts b/src/routes/meta.ts index b384eb0..dc69638 100644 --- a/src/routes/meta.ts +++ b/src/routes/meta.ts @@ -18,8 +18,7 @@ export const meta: WithPkgInfo = async ( } catch (error: any) { set.status = 404; throw new Error( - error.toString() || - `Cannot find an meta of ${filename} in ${packageName}@${packageVersion}`, + `Cannot find meta of ${filename} in ${packageName}@${packageVersion}. Case by ${error.toString()}`, ); } }; diff --git a/src/utils/npm.ts b/src/utils/npm.ts index b177a8c..8df01ec 100644 --- a/src/utils/npm.ts +++ b/src/utils/npm.ts @@ -180,7 +180,7 @@ export const searchPackageEntry = async ( const result = await search(tarball!, filename); - const { foundEntry: entry, matchingEntries: entries } = result; + const { foundEntry: entry, matchingEntries: entries, tried } = result; if (meta) { return { entry, entries }; @@ -188,7 +188,9 @@ export const searchPackageEntry = async ( if (!entry) { throw new Error( - `Cannot find entry ${filename} in ${packageName}@${packageVersion}`, + `Cannot find entry ${filename} in ${packageName}@${packageVersion}. tried ${tried.join( + "\n", + )}`, ); } if (entry.type === "file" && entry.path !== filename) { @@ -208,7 +210,7 @@ export const searchPackageEntry = async ( filename = indexEntry.path!; } else { throw new Error( - `Cannot find an index in ${filename} in ${packageName}@${packageVersion}`, + `Cannot find an index in ${filename} in ${packageName}@${packageVersion}, tried ${indexEntry}.`, ); } } diff --git a/src/utils/sqlite-lru-cache.ts b/src/utils/sqlite-lru-cache.ts index 570e1ba..72616e1 100644 --- a/src/utils/sqlite-lru-cache.ts +++ b/src/utils/sqlite-lru-cache.ts @@ -64,6 +64,7 @@ export class SqliteLRUCache { maxByteSize: maxSize = 1 * Math.pow(2, 30), onRemove = () => {}, }: SqlCacheOptions) { + console.log("๐Ÿš€ ~ SqliteLRUCache ~ database:", database); this.db = new Database(database ?? ":memory:", { create: true }); this.maxLen = maxLen; this.maxByteSize = maxSize; diff --git a/src/utils/stream.ts b/src/utils/stream.ts index 3904562..aaddf9c 100644 --- a/src/utils/stream.ts +++ b/src/utils/stream.ts @@ -41,6 +41,7 @@ export const search = async (tarball: IncomingMessage, filename: string) => { type SearchResult = { foundEntry: IFileMeta; matchingEntries: Record; + tried: string[]; }; return new Promise((accept, reject) => { const jsEntryFilename = `${filename}.js`; @@ -52,6 +53,7 @@ export const search = async (tarball: IncomingMessage, filename: string) => { if (filename === "/") { foundEntry = matchingEntries["/"] = { name: "/", type: "directory" }; } + const tried: string[] = []; tarball .pipe(gunzip()) @@ -68,6 +70,7 @@ export const search = async (tarball: IncomingMessage, filename: string) => { type: header.type, }; + tried.push(`[${entry.type ?? ""}] ${entry.path}`); // Skip non-files and files that don't match the entryName. if (entry.type !== "file" || !entry?.path?.startsWith(filename)) { stream.resume(); @@ -137,6 +140,7 @@ export const search = async (tarball: IncomingMessage, filename: string) => { // try a directory entry with the same name. foundEntry: foundEntry || matchingEntries[filename] || null, matchingEntries: matchingEntries, + tried, }); }); }); diff --git a/src/utils/user.ts b/src/utils/user.ts new file mode 100644 index 0000000..e351571 --- /dev/null +++ b/src/utils/user.ts @@ -0,0 +1,29 @@ +import { Database, Statement } from "bun:sqlite"; +import { serialize, deserialize } from "v8"; + +export class UserDB { + db: Database; + + constructor() { + this.db = new Database("user.db", { create: true }); + this._initdb(this.db); + } + + private _initdb(db: Database) { + db.exec("PRAGMA journal_mode = WAL;"); + db.transaction(() => { + // ๅˆๅง‹ๅŒ– + db.prepare( + `CREATE TABLE IF NOT EXISTS user ( + username TEXT PRIMARY KEY, + passwd BLOB, + ban INT + )`, + ).run(); + // ๅˆ›ๅปบ็ดขๅผ• + db.prepare( + "CREATE INDEX IF NOT EXISTS username ON cache (username)", + ).run(); + })(); + } +}