From e51bc6f711d7358a4b8b68059c6d61e9a705b87d Mon Sep 17 00:00:00 2001 From: hided62 Date: Tue, 17 Feb 2026 20:35:15 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=84=B4=20=EB=8D=B0=EB=AA=AC=EC=97=90?= =?UTF-8?q?=20cookie=20jar=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/daemon.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/src/daemon.ts b/src/daemon.ts index 090e1890..22cc101b 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -36,6 +36,57 @@ type ProcResult = { lastExecuted?: string; }; +class CookieJar { + #cookies = new Map(); + + getCookieHeader(): string | undefined { + if (this.#cookies.size === 0) { + return undefined; + } + return [...this.#cookies.entries()].map(([key, value]) => `${key}=${value}`).join("; "); + } + + ingestSetCookie(setCookieHeaders: string[]): void { + for (const setCookie of setCookieHeaders) { + const cookiePart = setCookie.split(";", 1)[0]?.trim(); + if (!cookiePart) { + continue; + } + + const eqPos = cookiePart.indexOf("="); + if (eqPos <= 0) { + continue; + } + + const key = cookiePart.slice(0, eqPos).trim(); + const value = cookiePart.slice(eqPos + 1).trim(); + + if (!key) { + continue; + } + + if (value === "") { + this.#cookies.delete(key); + continue; + } + this.#cookies.set(key, value); + } + } +} + +function getSetCookieHeaders(headers: Headers): string[] { + const headerObj = headers as unknown as { getSetCookie?: () => string[] }; + if (typeof headerObj.getSetCookie === "function") { + return headerObj.getSetCookie(); + } + + const fallback = headers.get("set-cookie"); + if (!fallback) { + return []; + } + return [fallback]; +} + function nowStr() { const d = new Date(); const pad = (n: number) => n.toString().padStart(2, "0"); @@ -131,13 +182,19 @@ async function scanOnce(): Promise { } // ============ 공개 접근 체크 ============ -async function isPublicReachable(signal?: AbortSignal): Promise { +async function isPublicReachable(signal?: AbortSignal, cookieJar?: CookieJar): Promise { if (!REQUIRE_PUBLIC) return true; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), Math.min(5_000, FETCH_TIMEOUT_MS)); const composite = anySignal([ctrl.signal, signal].filter(Boolean) as AbortSignal[]); try { - const res = await fetch(webBase, { method: "GET", signal: composite }); + const cookieHeader = cookieJar?.getCookieHeader(); + const res = await fetch(webBase, { + method: "GET", + signal: composite, + headers: cookieHeader ? { cookie: cookieHeader } : undefined, + }); + cookieJar?.ingestSetCookie(getSetCookieHeaders(res.headers)); if (!res.ok) { log(`[PUBLIC] 응답 ${res.status} → 비공개로 간주`); return false; } return true; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -150,14 +207,20 @@ async function isPublicReachable(signal?: AbortSignal): Promise { } // ============ HTTP ============ -async function httpGetJson(url: string, outerSignal?: AbortSignal): Promise { +async function httpGetJson(url: string, outerSignal?: AbortSignal, cookieJar?: CookieJar): Promise { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS); const signal = anySignal([ctrl.signal, outerSignal].filter(Boolean) as AbortSignal[]); const t0 = performance.now(); try { - const res = await fetch(url, { signal, cache: "no-store" }); + const cookieHeader = cookieJar?.getCookieHeader(); + const res = await fetch(url, { + signal, + cache: "no-store", + headers: cookieHeader ? { cookie: cookieHeader } : undefined, + }); + cookieJar?.ingestSetCookie(getSetCookieHeaders(res.headers)); const dt = Math.round(performance.now() - t0); if (!res.ok) { log("HTTPError:", res.status, url); return null; } try { @@ -195,6 +258,7 @@ class ServerRunner { #stopCtrl = new AbortController(); #stopped = false; #lastAutoResetAt = 0; + #cookieJar = new CookieJar(); constructor(entry: ServerEntry) { this.#entry = entry; @@ -222,7 +286,7 @@ class ServerRunner { while (!this.#stopCtrl.signal.aborted) { try { // 공개 접근 체크 - const pub = await isPublicReachable(this.#stopCtrl.signal); + const pub = await isPublicReachable(this.#stopCtrl.signal, publicCookieJar); if (!pub) { await delay(15_000, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ }); continue; } const hidden = await this.#entry.isHidden(); @@ -236,7 +300,7 @@ class ServerRunner { if (this.#stopCtrl.signal.aborted) break; } log(`[${this.name()}] 닫힘 - (정렬) autoreset 호출 @ mm:00${AUTO_RESET_JITTER_MS ? `+${AUTO_RESET_JITTER_MS}ms` : ""}`); - await httpGetJson(this.#entry.autoresetUrl, this.#stopCtrl.signal); + await httpGetJson(this.#entry.autoresetUrl, this.#stopCtrl.signal, this.#cookieJar); // 같은 분에 중복 호출 방지: 0초 직후 잠깐 쉬고 다음 루프는 자연스럽게 다음 분으로 정렬됨 await delay(200, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ }); continue; @@ -244,7 +308,7 @@ class ServerRunner { // 열림: proc - const data = await httpGetJson(this.#entry.procUrl, this.#stopCtrl.signal); + const data = await httpGetJson(this.#entry.procUrl, this.#stopCtrl.signal, this.#cookieJar); if (!data) { await delay(IF_LOCKED_WAIT_MS, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ }); continue; @@ -348,6 +412,7 @@ class DaemonManager { // ============ 메인 ============ const manager = new DaemonManager(); +const publicCookieJar = new CookieJar(); async function main() { // 신호 처리: SIGINT/SIGTERM → graceful shutdown @@ -372,4 +437,4 @@ async function main() { main().catch((e) => { log("치명적 예외:", e); process.exitCode = 1; -}); \ No newline at end of file +});