-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathproxy.ts
More file actions
91 lines (83 loc) · 3.11 KB
/
Copy pathproxy.ts
File metadata and controls
91 lines (83 loc) · 3.11 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
import { NextResponse, type NextRequest } from "next/server";
import {
getAuthRetryAfterMs,
recordAuthFailure,
retryAfterSeconds,
} from "@/lib/auth-throttle";
import {
isApiRequestAllowed,
isApiRequestHostAllowed,
} from "@/lib/request-security";
import {
isValidWebSessionToken,
isValidBasicAuthorization,
isWebPasswordEnabled,
PI_WEB_SESSION_COOKIE,
} from "@/lib/web-auth";
function tooManyAttempts(retryAfterMs: number): NextResponse {
return new NextResponse("Too many failed attempts", {
status: 429,
headers: {
"Cache-Control": "no-store",
"Retry-After": String(retryAfterSeconds(retryAfterMs)),
},
});
}
export function proxy(request: NextRequest) {
const isApiRequest = request.nextUrl.pathname === "/api"
|| request.nextUrl.pathname.startsWith("/api/");
const isTrustedRequest = isApiRequest
? isApiRequestAllowed(request)
: isApiRequestHostAllowed(request);
if (!isTrustedRequest) {
if (!isApiRequest) {
return new NextResponse("Untrusted request", { status: 403 });
}
return NextResponse.json({ error: "Untrusted API request" }, { status: 403 });
}
const password = process.env.PI_WEB_PASSWORD;
if (!isWebPasswordEnabled(password)) {
if (request.nextUrl.pathname === "/login") {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}
let authenticated = isValidWebSessionToken(request.cookies.get(PI_WEB_SESSION_COOKIE)?.value, password);
const authorization = isApiRequest ? request.headers.get("authorization") : null;
if (!authenticated && authorization && /^Basic\s/i.test(authorization)) {
// Every Basic header is a password guess, so it shares the login form's
// throttle; otherwise any API path (or GET /api/web-auth) answers guesses
// at full speed. While blocked even the right password is refused, or the
// answer would leak. A success does not reset the counter: Basic clients
// authenticate on every request, and each reset would hand an interleaved
// guesser a fresh short block.
const retryAfterMs = getAuthRetryAfterMs();
if (retryAfterMs > 0) return tooManyAttempts(retryAfterMs);
authenticated = isValidBasicAuthorization(authorization, password);
if (!authenticated) recordAuthFailure();
}
if (request.nextUrl.pathname === "/login") {
return authenticated
? NextResponse.redirect(new URL("/", request.url))
: NextResponse.next();
}
if (request.nextUrl.pathname === "/api/web-auth") return NextResponse.next();
if (!authenticated) {
if (!isApiRequest) {
const loginUrl = new URL("/login", request.url);
if (request.nextUrl.search) {
loginUrl.searchParams.set("next", `${request.nextUrl.pathname}${request.nextUrl.search}`);
}
return NextResponse.redirect(loginUrl);
}
return new NextResponse("Authentication required", {
status: 401,
headers: {
"Cache-Control": "no-store",
"WWW-Authenticate": 'Basic realm="Pi Web", charset="UTF-8"',
},
});
}
return NextResponse.next();
}
export const config = { matcher: ["/", "/login", "/api/:path*"] };