-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
79 lines (69 loc) · 2.42 KB
/
Copy pathmiddleware.ts
File metadata and controls
79 lines (69 loc) · 2.42 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
// middleware.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
const signSecret = process.env.ACCESS_SIGNING_SECRET!;
// 指定6サービスだけUAで即遮断
const BLOCK_UA =
/(GPTBot|ChatGPT-User|OAI-SearchBot|CensysInspect|VirusTotal|ia_archiver|archive\.org_bot|urlscan)/i;
// --- 署名検証(有効期限のみ) ---
function hex(ab: ArrayBuffer) {
const v = new Uint8Array(ab);
let s = '';
for (const b of v) s += b.toString(16).padStart(2, '0');
return s;
}
async function sign(payload: string, secret: string) {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
return hex(sig);
}
async function verifyToken(token: string, secret: string, now = Date.now()) {
const [encJson, sigHex] = token.split('.');
if (!encJson || !sigHex) return false;
const json = decodeURIComponent(encJson);
const expected = await sign(json, secret);
if (expected !== sigHex) return false;
const data = JSON.parse(json) as { exp: number };
if (!data.exp || now > data.exp) return false;
return true;
}
export async function middleware(req: NextRequest) {
const { pathname, search } = req.nextUrl;
const accept = req.headers.get('accept') || '';
const ua = req.headers.get('user-agent') || '';
// 1) 指定UAなら本文ゼロで即403
if (BLOCK_UA.test(ua)) {
return new NextResponse(null, { status: 403 });
}
// 2) ゲート(/entry)・検証API・静的は常時許可
if (
pathname.startsWith('/entry') ||
pathname.startsWith('/api/verify') ||
pathname.startsWith('/_next') ||
pathname === '/favicon.ico' ||
pathname === '/robots.txt' ||
pathname === '/sitemap.xml'
) {
return NextResponse.next();
}
// 3) 認証済みなら許可
const tok = req.cookies.get('sbk')?.value;
if (tok && (await verifyToken(tok, signSecret))) {
return NextResponse.next();
}
// 4) HTML希望は/entryへ、それ以外は本文ゼロ403
if (accept.includes('text/html')) {
const url = req.nextUrl.clone();
url.pathname = '/entry';
url.search = `?next=${encodeURIComponent(pathname + (search || ''))}`;
return NextResponse.redirect(url);
}
return new NextResponse(null, { status: 403 });
}
export const config = { matcher: '/:path*' };