-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworker-wrapper.js
More file actions
106 lines (90 loc) · 3.73 KB
/
Copy pathworker-wrapper.js
File metadata and controls
106 lines (90 loc) · 3.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import openNextWorker from "./.open-next/worker.js";
import { getDdosState } from "./worker/ddos-state.js";
import { trackRequest } from "./worker/ddos-stats.js";
import { handleDdosCron } from "./worker/ddos-cron.js";
import { isBotRequest } from "./worker/bot-detect.js";
import { getRuntimeMode, handleRestrictedMode } from "./worker/runtime-mode.js";
// 式は wrangler.toml の [triggers] crons と一致させること
const CRON_ROUTES = {
"0 * * * *": "/api/cron/sync-external",
"0 3 * * *": "/api/cron/backup",
// ddos_slices は 30 分で削除されるため、それより短い間隔で取り込む必要がある
"*/10 * * * *": "/api/cron/usage",
"30 3 * * *": "/api/cron/trust",
};
/** Cron から Next.js 側のルートを内部的に叩く */
async function invokeCronRoute(path, env, ctx) {
const req = new Request(`http://localhost${path}`, {
method: "GET",
headers: { "Authorization": `Bearer ${env.CRON_SECRET || ""}` },
});
// Cron の1本が落ちても他の処理を続けたいので、ここで結果に畳み込む
try {
const res = await openNextWorker.fetch(req, env, ctx);
if (res.ok) {
console.log(`Cron ${path} processed successfully:`, await res.json());
return;
}
console.error(`Cron ${path} failed with status:`, res.status, await res.text());
} catch (e) {
console.error(`Cron ${path} fetch error:`, e);
}
}
function isDownloadPath(path) {
return path === "/api/download" || path.startsWith("/api/download/");
}
/** lib/download/countPolicy.ts と対応させること */
const DDOS_STATE_HEADER = "x-mp-ddos-state";
const BOT_HEADER = "x-mp-bot";
/**
* ダウンロード計数の判定材料をヘッダへ載せ替える。
*
* Next.js 側で D1 や cf を引き直さずに済ませるためのもの。
* クライアントが同名ヘッダを送ってきても、ここで必ず上書きするため詐称できない。
*/
function withCountSignals(req, state, isBot) {
const headers = new Headers(req.headers);
headers.set(DDOS_STATE_HEADER, state);
headers.set(BOT_HEADER, isBot ? "1" : "0");
return new Request(req, { headers });
}
export default {
/** OpenNext の fetch ハンドラをラップし、手前でDDoS統計の収集だけを行う */
async fetch(req, env, ctx) {
const url = new URL(req.url);
// 運用モードの判定は最初に行う。止めている間は本体の処理を一切走らせない
const restricted = handleRestrictedMode(
req,
url,
await getRuntimeMode(env),
env.ARCHIVE_ORIGIN || url.origin
);
if (restricted) return restricted;
const isDownload = isDownloadPath(url.pathname);
let forwarded = req;
// 集計はあくまで付随処理なので、失敗しても Next.js への転送は必ず行う
try {
const state = await getDdosState(env.DB);
const isBot = isBotRequest(req);
// 防護中は WAF 側が捌くため、集計するのは NORMAL のときだけ
if (state.currentState === "NORMAL") {
trackRequest(req, url, env, ctx, isDownload, isBot);
}
// ヘッダの付け替えはコストがかかるため、判定を使うダウンロードのみに限る
if (isDownload) {
forwarded = withCountSignals(req, state.currentState, isBot);
}
} catch (e) {
console.error("[DDOS-GUARD] Intercept error:", e);
}
return openNextWorker.fetch(forwarded, env, ctx);
},
/** Cloudflare Cron Triggers 用のハンドラ */
async scheduled(controller, env, ctx) {
await handleDdosCron(env);
const path = CRON_ROUTES[controller.cron];
if (!path) return;
console.log(`Cron triggered (${controller.cron}): invoking ${path}`);
await invokeCronRoute(path, env, ctx);
},
};