From af5667f33734302c8c50f6530c5aca6cabfd40ca Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 7 Aug 2023 19:28:25 +0800 Subject: [PATCH] Update img_verify --- config.ts | 5 -- fresh.gen.ts | 16 +++-- routes/api/_middleware.ts | 38 ++---------- routes/api/file/[id].ts | 32 +++++----- routes/api/thumbnail/[id].ts | 48 ++++++--------- routes/file/[id].ts | 48 +++++++++++++++ routes/file/_middleware.ts | 29 +++++++++ routes/thumbnail/[id].ts | 83 ++++++++++++++++++++++++++ routes/thumbnail/_middleware.ts | 1 + server/SortableURLSearchParams.ts | 46 ++++++++++++++ server/SortableURLSearchParams_test.ts | 11 ++++ server/check_auth.ts | 29 +++++++++ server/files.ts | 8 +++ static/sw.ts | 6 +- 14 files changed, 310 insertions(+), 90 deletions(-) create mode 100644 routes/file/[id].ts create mode 100644 routes/file/_middleware.ts create mode 100644 routes/thumbnail/[id].ts create mode 100644 routes/thumbnail/_middleware.ts create mode 100644 server/SortableURLSearchParams.ts create mode 100644 server/SortableURLSearchParams_test.ts create mode 100644 server/check_auth.ts diff --git a/config.ts b/config.ts index 976af5e..4536d49 100644 --- a/config.ts +++ b/config.ts @@ -23,7 +23,6 @@ export type ConfigType = { thumbnail_dir: string; remove_previous_gallery: boolean; img_verify_secret?: string; - img_verify_bypass_auth: boolean; }; export enum ThumbnailMethod { @@ -137,9 +136,6 @@ export class Config { get img_verify_secret() { return this._return_string("img_verify_secret"); } - get img_verify_bypass_auth() { - return this._return_bool("img_verify_bypass_auth") || false; - } to_json(): ConfigType { return { cookies: typeof this.cookies === "string", @@ -163,7 +159,6 @@ export class Config { thumbnail_dir: this.thumbnail_dir, remove_previous_gallery: this.remove_previous_gallery, img_verify_secret: this.img_verify_secret, - img_verify_bypass_auth: this.img_verify_bypass_auth, }; } } diff --git a/fresh.gen.ts b/fresh.gen.ts index 6f00b52..7a1deda 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -20,8 +20,12 @@ import * as $14 from "./routes/api/task.ts"; import * as $15 from "./routes/api/thumbnail/[id].ts"; import * as $16 from "./routes/api/token.ts"; import * as $17 from "./routes/api/user.ts"; -import * as $18 from "./routes/index.tsx"; -import * as $19 from "./routes/manifest.json.ts"; +import * as $18 from "./routes/file/[id].ts"; +import * as $19 from "./routes/file/_middleware.ts"; +import * as $20 from "./routes/index.tsx"; +import * as $21 from "./routes/manifest.json.ts"; +import * as $22 from "./routes/thumbnail/[id].ts"; +import * as $23 from "./routes/thumbnail/_middleware.ts"; import * as $$0 from "./islands/Container.tsx"; import * as $$1 from "./islands/Settings.tsx"; import * as $$2 from "./islands/TaskManager.tsx"; @@ -46,8 +50,12 @@ const manifest = { "./routes/api/thumbnail/[id].ts": $15, "./routes/api/token.ts": $16, "./routes/api/user.ts": $17, - "./routes/index.tsx": $18, - "./routes/manifest.json.ts": $19, + "./routes/file/[id].ts": $18, + "./routes/file/_middleware.ts": $19, + "./routes/index.tsx": $20, + "./routes/manifest.json.ts": $21, + "./routes/thumbnail/[id].ts": $22, + "./routes/thumbnail/_middleware.ts": $23, }, islands: { "./islands/Container.tsx": $$0, diff --git a/routes/api/_middleware.ts b/routes/api/_middleware.ts index c2cc142..0f2b31b 100644 --- a/routes/api/_middleware.ts +++ b/routes/api/_middleware.ts @@ -3,10 +3,8 @@ import { get_task_manager } from "../../server.ts"; import { parse_cookies } from "../../server/cookies.ts"; import { return_error } from "../../server/utils.ts"; import type { Token } from "../../db.ts"; -import pbkdf2Hmac from "pbkdf2-hmac"; -import { encode } from "std/encoding/base64.ts"; -async function handle_auth(req: Request, ctx: MiddlewareHandlerContext) { +function handle_auth(req: Request, ctx: MiddlewareHandlerContext) { if (req.method === "OPTIONS") return true; const m = get_task_manager(); if (m.db.get_user_count() === 0) return true; @@ -18,43 +16,19 @@ async function handle_auth(req: Request, ctx: MiddlewareHandlerContext) { token = cookies.get("token"); is_from_cookie = true; } - const check = async () => { + const check = () => { if (u.pathname === "/api/token" && req.method === "PUT") return true; if (u.pathname === "/api/status" && req.method === "GET") return true; - if (m.cfg.img_verify_bypass_auth) { - if ( - u.pathname.startsWith("/api/file") || - u.pathname.startsWith("/api/thumbnail") - ) { - if (ctx.params.id) { - const verify = u.searchParams.get("verify"); - if (verify) { - const tverify = encode( - new Uint8Array( - await pbkdf2Hmac( - `${ctx.params.id}`, - m.cfg.img_verify_secret, - 1000, - 64, - "SHA-512", - ), - ), - ); - if (verify === tverify) return true; - } - } - } - } return false; }; - if (!token) return await check(); + if (!token) return check(); const t = m.db.get_token(token); const now = (new Date()).getTime(); - if (!t || t.expired.getTime() < now) return await check(); + if (!t || t.expired.getTime() < now) return check(); const user = m.db.get_user(t.uid); if (!user) { m.db.delete_token(token); - return await check(); + return check(); } ctx.state.user = user; ctx.state.is_from_cookie = is_from_cookie; @@ -63,7 +37,7 @@ async function handle_auth(req: Request, ctx: MiddlewareHandlerContext) { } export async function handler(req: Request, ctx: MiddlewareHandlerContext) { - if (!(await handle_auth(req, ctx))) { + if (!(handle_auth(req, ctx))) { return return_error(401, "Unauthorized"); } const res = await ctx.next(); diff --git a/routes/api/file/[id].ts b/routes/api/file/[id].ts index ce65a02..86c171a 100644 --- a/routes/api/file/[id].ts +++ b/routes/api/file/[id].ts @@ -4,10 +4,11 @@ import { get_file_response, GetFileResponseOptions, } from "../../../server/get_file_response.ts"; -import { get_string } from "../../../server/parse_form.ts"; +import { parse_bool } from "../../../server/parse_form.ts"; import pbkdf2Hmac from "pbkdf2-hmac"; import { encode } from "std/encoding/base64.ts"; -import { get_host } from "../../../server/utils.ts"; +import { get_host, return_data } from "../../../server/utils.ts"; +import type { EhFileExtend } from "../../../server/files.ts"; export const handler: Handlers = { async GET(req, ctx) { @@ -16,21 +17,24 @@ export const handler: Handlers = { return new Response("Bad Request", { status: 400 }); } const m = get_task_manager(); + const u = new URL(req.url); const f = m.db.get_file(id); + const data = await parse_bool(u.searchParams.get("data"), false); if (!f) { return new Response("File not found.", { status: 404 }); } + if (data) { + return return_data({ + id: f.id, + height: f.height, + width: f.width, + is_original: f.is_original, + token: f.token, + }); + } const opts: GetFileResponseOptions = {}; if (m.cfg.img_verify_secret) { - let verify = null; - try { - const form = await req.formData(); - verify = await get_string(form.get("verify")); - } catch (_) { - null; - const u = new URL(req.url); - verify = u.searchParams.get("verify"); - } + const verify = u.searchParams.get("verify"); const tverify = encode( new Uint8Array( await pbkdf2Hmac( @@ -46,14 +50,10 @@ export const handler: Handlers = { const b = new URLSearchParams(); b.append("verify", tverify); return Response.redirect( - `${get_host(req)}/api/file/${f.id}?${b}`, + `${get_host(req)}/file/${f.id}?${b}`, ); } - if (verify !== tverify) { - return new Response("Invalid verify.", { status: 400 }); - } } - opts.cache_control = "public, max-age=31536000"; opts.range = req.headers.get("range"); opts.if_modified_since = req.headers.get("If-Modified-Since"); opts.if_unmodified_since = req.headers.get("If-Unmodified-Since"); diff --git a/routes/api/thumbnail/[id].ts b/routes/api/thumbnail/[id].ts index 5b76fc9..5fd4cd9 100644 --- a/routes/api/thumbnail/[id].ts +++ b/routes/api/thumbnail/[id].ts @@ -1,11 +1,7 @@ import { Handlers } from "$fresh/server.ts"; import { exists } from "std/fs/exists.ts"; import { get_task_manager } from "../../../server.ts"; -import { - get_string, - parse_bool, - parse_int, -} from "../../../server/parse_form.ts"; +import { parse_bool, parse_int } from "../../../server/parse_form.ts"; import { generate_filename, ThumbnailConfig } from "../../../thumbnail/base.ts"; import { sure_dir } from "../../../utils.ts"; import { ThumbnailMethod } from "../../../config.ts"; @@ -17,6 +13,7 @@ import { import { get_host } from "../../../server/utils.ts"; import pbkdf2Hmac from "pbkdf2-hmac"; import { encode } from "std/encoding/base64.ts"; +import { SortableURLSearchParams } from "../../../server/SortableURLSearchParams.ts"; export const handler: Handlers = { async GET(req, ctx) { @@ -80,38 +77,27 @@ export const handler: Handlers = { } const opts: GetFileResponseOptions = {}; if (m.cfg.img_verify_secret) { - let verify = null; - try { - const form = await req.formData(); - verify = await get_string(form.get("verify")); - } catch (_) { - null; - const u = new URL(req.url); - verify = u.searchParams.get("verify"); - } - const tverify = encode( - new Uint8Array( - await pbkdf2Hmac( - `${id}`, - m.cfg.img_verify_secret, - 1000, - 64, - "SHA-512", - ), - ), - ); + const verify = u.searchParams.get("verify"); if (verify === null) { - const b = new URLSearchParams(); + const bs = new SortableURLSearchParams(u.search, ["verify"]); + const tverify = encode( + new Uint8Array( + await pbkdf2Hmac( + `${id}${bs.toString2()}`, + m.cfg.img_verify_secret, + 1000, + 64, + "SHA-512", + ), + ), + ); + const b = new URLSearchParams(bs.toString()); b.append("verify", tverify); return Response.redirect( - `${get_host(req)}/api/thumbnail/${f.id}?${b}`, + `${get_host(req)}/thumbnail/${f.id}?${b}`, ); } - if (verify !== tverify) { - return new Response("Invalid verify.", { status: 400 }); - } } - opts.cache_control = "public, max-age=31536000"; opts.range = req.headers.get("range"); opts.if_modified_since = req.headers.get("If-Modified-Since"); opts.if_unmodified_since = req.headers.get("If-Unmodified-Since"); diff --git a/routes/file/[id].ts b/routes/file/[id].ts new file mode 100644 index 0000000..286af94 --- /dev/null +++ b/routes/file/[id].ts @@ -0,0 +1,48 @@ +import { Handlers } from "$fresh/server.ts"; +import { get_task_manager } from "../../server.ts"; +import { + get_file_response, + GetFileResponseOptions, +} from "../../server/get_file_response.ts"; +import pbkdf2Hmac from "pbkdf2-hmac"; +import { encode } from "std/encoding/base64.ts"; + +export const handler: Handlers = { + async GET(req, ctx) { + const id = parseInt(ctx.params.id); + if (isNaN(id)) { + return new Response("Bad Request", { status: 400 }); + } + const m = get_task_manager(); + const u = new URL(req.url); + if (!m.cfg.img_verify_secret) { + return new Response("Can not verify.", { status: 400 }); + } + const verify = u.searchParams.get("verify"); + if (!verify) return new Response("Verify is needed.", { status: 400 }); + const tverify = encode( + new Uint8Array( + await pbkdf2Hmac( + `${id}`, + m.cfg.img_verify_secret, + 1000, + 64, + "SHA-512", + ), + ), + ); + if (verify !== tverify) { + return new Response("verify is invalid.", { status: 400 }); + } + const f = m.db.get_file(id); + if (!f) { + return new Response("File not found.", { status: 404 }); + } + const opts: GetFileResponseOptions = {}; + opts.cache_control = "public, no-transform, max-age=31536000"; + opts.range = req.headers.get("range"); + opts.if_modified_since = req.headers.get("If-Modified-Since"); + opts.if_unmodified_since = req.headers.get("If-Unmodified-Since"); + return await get_file_response(f.path, opts); + }, +}; diff --git a/routes/file/_middleware.ts b/routes/file/_middleware.ts new file mode 100644 index 0000000..1d0ac85 --- /dev/null +++ b/routes/file/_middleware.ts @@ -0,0 +1,29 @@ +import { MiddlewareHandlerContext } from "$fresh/server.ts"; + +export async function handler(req: Request, ctx: MiddlewareHandlerContext) { + const res = await ctx.next(); + if (req.method === "OPTIONS" && res.status === 405) { + const headers = new Headers(); + const allow = res.headers.get("Accept"); + if (allow) headers.set("Allow", allow); + const origin = req.headers.get("origin"); + if (origin) { + headers.set("Access-Control-Allow-Origin", "*"); + if (allow) headers.set("Access-Control-Allow-Methods", allow); + headers.set("Access-Control-Allow-Headers", "Content-Type, Range"); + } + return new Response(null, { status: 204, headers }); + } else { + if (res.status === 101) return res; + const headers = new Headers(res.headers); + const origin = req.headers.get("origin"); + if (origin) { + headers.set("Access-Control-Allow-Origin", "*"); + } + return new Response(res.body, { + status: res.status, + headers: headers, + statusText: res.statusText, + }); + } +} diff --git a/routes/thumbnail/[id].ts b/routes/thumbnail/[id].ts new file mode 100644 index 0000000..bbe9ee3 --- /dev/null +++ b/routes/thumbnail/[id].ts @@ -0,0 +1,83 @@ +import { Handlers } from "$fresh/server.ts"; +import { exists } from "std/fs/exists.ts"; +import { get_task_manager } from "../../server.ts"; +import { parse_int } from "../../server/parse_form.ts"; +import { generate_filename, ThumbnailConfig } from "../../thumbnail/base.ts"; +import { sure_dir } from "../../utils.ts"; +import { + get_file_response, + GetFileResponseOptions, +} from "../../server/get_file_response.ts"; +import pbkdf2Hmac from "pbkdf2-hmac"; +import { encode } from "std/encoding/base64.ts"; +import { SortableURLSearchParams } from "../../server/SortableURLSearchParams.ts"; + +export const handler: Handlers = { + async GET(req, ctx) { + const id = parseInt(ctx.params.id); + if (isNaN(id)) { + return new Response("Bad Request", { status: 400 }); + } + const m = get_task_manager(); + const b = m.cfg.thumbnail_dir; + await sure_dir(b); + const f = m.db.get_file(id); + if (!f) { + return new Response("File not found.", { status: 404 }); + } + const u = new URL(req.url); + if (!m.cfg.img_verify_secret) { + return new Response("Can not verify.", { status: 400 }); + } + const verify = u.searchParams.get("verify"); + if (!verify) return new Response("Verify is needed.", { status: 400 }); + const bs = new SortableURLSearchParams(u.search, ["verify"]); + const tverify = encode( + new Uint8Array( + await pbkdf2Hmac( + `${id}${bs.toString2()}`, + m.cfg.img_verify_secret, + 1000, + 64, + "SHA-512", + ), + ), + ); + if (verify !== tverify) { + return new Response("verify is invalid.", { status: 400 }); + } + const max = await parse_int(u.searchParams.get("max"), 1200); + const width = await parse_int(u.searchParams.get("width"), null); + const height = await parse_int(u.searchParams.get("height"), null); + const quality = await parse_int(u.searchParams.get("quality"), 1); + const cfg: ThumbnailConfig = { width: 0, height: 0, quality }; + if (width !== null && height !== null) { + cfg.width = width; + cfg.height = height; + } else if (width !== null) { + cfg.width = width; + cfg.height = Math.round(f.height / f.width * width); + } else if (height !== null) { + cfg.height = height; + cfg.width = Math.round(f.width / f.height * height); + } else { + if (f.width > f.height) { + cfg.width = max; + cfg.height = Math.round(f.height / f.width * max); + } else { + cfg.height = max; + cfg.width = Math.round(f.width / f.height * max); + } + } + const output = generate_filename(b, f, cfg); + if (!(await exists(output))) { + return new Response("file not exists.", { status: 500 }); + } + const opts: GetFileResponseOptions = {}; + opts.cache_control = "public, no-transform, max-age=31536000"; + opts.range = req.headers.get("range"); + opts.if_modified_since = req.headers.get("If-Modified-Since"); + opts.if_unmodified_since = req.headers.get("If-Unmodified-Since"); + return await get_file_response(output, opts); + }, +}; diff --git a/routes/thumbnail/_middleware.ts b/routes/thumbnail/_middleware.ts new file mode 100644 index 0000000..f37a9ff --- /dev/null +++ b/routes/thumbnail/_middleware.ts @@ -0,0 +1 @@ +export { handler } from "../file/_middleware.ts"; diff --git a/server/SortableURLSearchParams.ts b/server/SortableURLSearchParams.ts new file mode 100644 index 0000000..617573e --- /dev/null +++ b/server/SortableURLSearchParams.ts @@ -0,0 +1,46 @@ +export class SortableURLSearchParams extends URLSearchParams { + excludes; + constructor( + init?: string[][] | Record | string | URLSearchParams, + excludes: string[] = [], + ) { + super(init); + this.excludes = excludes; + } + entries(): IterableIterator<[string, string]> { + this.sort(); + const a: [string, string][] = []; + for (const i of super.entries()) { + if (!this.excludes.includes(i[0])) a.push(i); + } + return a.values(); + } + forEach( + callbackfn: (value: string, key: string, parent: this) => void, + thisArg?: unknown, + ): void { + for (const [k, v] of this.entries()) { + callbackfn.apply(thisArg, [v, k, this]); + } + } + keys(): IterableIterator { + this.sort(); + const a: string[] = []; + for (const i of super.keys()) { + if (!this.excludes.includes(i)) a.push(i); + } + return a.values(); + } + toString(): string { + return Array.from(this.entries()).map((v) => + `${encodeURIComponent(v[0])}=${encodeURIComponent(v[1])}` + ).join("&"); + } + toString2(): string { + const s = this.toString(); + return s.length ? `?${s}` : ""; + } + values(): IterableIterator { + return Array.from(this.entries()).map((v) => v[1]).values(); + } +} diff --git a/server/SortableURLSearchParams_test.ts b/server/SortableURLSearchParams_test.ts new file mode 100644 index 0000000..66126c6 --- /dev/null +++ b/server/SortableURLSearchParams_test.ts @@ -0,0 +1,11 @@ +import { assertEquals } from "std/assert/mod.ts"; +import { SortableURLSearchParams } from "./SortableURLSearchParams.ts"; + +Deno.test("SortableURLSearchParams_test", () => { + const s = new SortableURLSearchParams(undefined, ["dad"]); + s.append("a", "1"); + s.append("d", "3"); + s.append("b", "4"); + s.append("dad", "4"); + assertEquals(s.toString(), "a=1&b=4&d=3"); +}); diff --git a/server/check_auth.ts b/server/check_auth.ts new file mode 100644 index 0000000..d008c3a --- /dev/null +++ b/server/check_auth.ts @@ -0,0 +1,29 @@ +import { get_task_manager } from "../server.ts"; +import { parse_cookies } from "./cookies.ts"; + +export function check_auth(req: Request) { + if (req.method === "OPTIONS") return true; + const m = get_task_manager(); + if (m.db.get_user_count() === 0) return true; + const u = new URL(req.url); + let token: string | null | undefined = req.headers.get("X-TOKEN"); + const cookies = parse_cookies(req.headers.get("Cookie")); + if (!token) { + token = cookies.get("token"); + } + const check = () => { + if (u.pathname === "/api/token" && req.method === "PUT") return true; + if (u.pathname === "/api/status" && req.method === "GET") return true; + return false; + }; + if (!token) return check(); + const t = m.db.get_token(token); + const now = (new Date()).getTime(); + if (!t || t.expired.getTime() < now) return check(); + const user = m.db.get_user(t.uid); + if (!user) { + m.db.delete_token(token); + return check(); + } + return true; +} diff --git a/server/files.ts b/server/files.ts index 429d866..fef3445 100644 --- a/server/files.ts +++ b/server/files.ts @@ -5,4 +5,12 @@ export type EhFileBasic = { is_original: boolean; }; +export type EhFileExtend = { + id: number; + width: number; + height: number; + is_original: boolean; + token: string; +}; + export type EhFiles = Record; diff --git a/static/sw.ts b/static/sw.ts index 7c07340..658929b 100644 --- a/static/sw.ts +++ b/static/sw.ts @@ -50,8 +50,10 @@ function match_url(u: URL) { /**@ts-ignore */ self.addEventListener("fetch", async (e: FetchEvent) => { const u = new URL(e.request.url); - if (u.origin === self.location.origin && u.pathname.startsWith("/api/")) { - return; + if (u.origin === self.location.origin) { + if (u.pathname.startsWith("/api/")) return; + if (u.pathname.startsWith("/file/")) return; + if (u.pathname.startsWith("/thumbnail/")) return; } if (!inited) await deleteOldCaches(); const r = e.request;