shokupan 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/context.d.ts +63 -2
- package/dist/index.cjs +84 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +85 -25
- package/dist/index.js.map +1 -1
- package/dist/plugins/proxy.d.ts +2 -0
- package/dist/plugins/rate-limit.d.ts +1 -0
- package/dist/router.d.ts +95 -36
- package/dist/shokupan.d.ts +64 -0
- package/dist/types.d.ts +33 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { Eta } from "eta";
|
|
3
3
|
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
4
|
-
import { resolve, join, basename } from "path";
|
|
4
|
+
import { resolve, join, sep, basename } from "path";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
7
7
|
import * as os from "node:os";
|
|
@@ -76,6 +76,15 @@ class ShokupanResponse {
|
|
|
76
76
|
return this._headers !== null;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
function isValidCookieDomain(domain, currentHost) {
|
|
80
|
+
const hostWithoutPort = currentHost.split(":")[0];
|
|
81
|
+
if (domain === hostWithoutPort) return true;
|
|
82
|
+
if (domain.startsWith(".")) {
|
|
83
|
+
const domainWithoutDot = domain.slice(1);
|
|
84
|
+
return hostWithoutPort.endsWith(domainWithoutDot);
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
79
88
|
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
80
89
|
100,
|
|
81
90
|
101,
|
|
@@ -235,16 +244,20 @@ class ShokupanContext {
|
|
|
235
244
|
*/
|
|
236
245
|
get query() {
|
|
237
246
|
if (this._cachedQuery) return this._cachedQuery;
|
|
238
|
-
const q =
|
|
247
|
+
const q = /* @__PURE__ */ Object.create(null);
|
|
248
|
+
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
239
249
|
const entries = Object.entries(this.url.searchParams);
|
|
240
250
|
for (let i = 0; i < entries.length; i++) {
|
|
241
251
|
const [key, value] = entries[i];
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
252
|
+
if (blocklist.includes(key)) continue;
|
|
253
|
+
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
254
|
+
if (Array.isArray(q[key])) {
|
|
255
|
+
q[key].push(value);
|
|
256
|
+
} else {
|
|
257
|
+
q[key] = [q[key], value];
|
|
258
|
+
}
|
|
246
259
|
} else {
|
|
247
|
-
q[key] =
|
|
260
|
+
q[key] = value;
|
|
248
261
|
}
|
|
249
262
|
}
|
|
250
263
|
this._cachedQuery = q;
|
|
@@ -321,6 +334,12 @@ class ShokupanContext {
|
|
|
321
334
|
* @param options Cookie options
|
|
322
335
|
*/
|
|
323
336
|
setCookie(name, value, options = {}) {
|
|
337
|
+
if (options.domain) {
|
|
338
|
+
const currentHost = this.hostname;
|
|
339
|
+
if (!isValidCookieDomain(options.domain, currentHost)) {
|
|
340
|
+
throw new Error(`Invalid cookie domain: ${options.domain} for host ${currentHost}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
324
343
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
325
344
|
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
326
345
|
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
@@ -614,11 +633,24 @@ function RateLimitMiddleware(options = {}) {
|
|
|
614
633
|
const statusCode = options.statusCode || 429;
|
|
615
634
|
const headers = options.headers !== false;
|
|
616
635
|
const mode = options.mode || "user";
|
|
636
|
+
const trustedProxies = options.trustedProxies || [];
|
|
617
637
|
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
618
638
|
if (mode === "absolute") {
|
|
619
639
|
return "global";
|
|
620
640
|
}
|
|
621
|
-
|
|
641
|
+
const xForwardedFor = ctx.headers.get("x-forwarded-for");
|
|
642
|
+
if (xForwardedFor && trustedProxies.length > 0) {
|
|
643
|
+
const ips = xForwardedFor.split(",").map((ip) => ip.trim());
|
|
644
|
+
for (let i = ips.length - 1; i >= 0; i--) {
|
|
645
|
+
const ip = ips[i];
|
|
646
|
+
if (!trustedProxies.includes(ip)) {
|
|
647
|
+
if (/^[\d.:a-fA-F]+$/.test(ip)) {
|
|
648
|
+
return ip;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
622
654
|
});
|
|
623
655
|
const skip = options.skip || (() => false);
|
|
624
656
|
const hits = /* @__PURE__ */ new Map();
|
|
@@ -1294,12 +1326,23 @@ function serveStatic(config, prefix) {
|
|
|
1294
1326
|
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
1295
1327
|
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
1296
1328
|
if (relative.length === 0) relative = "/";
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1329
|
+
if (relative.includes("\0")) {
|
|
1330
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
1331
|
+
}
|
|
1332
|
+
try {
|
|
1333
|
+
relative = decodeURIComponent(relative);
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
return ctx.json({ error: "Bad Request" }, 400);
|
|
1336
|
+
}
|
|
1337
|
+
if (relative.includes("\0")) {
|
|
1338
|
+
return ctx.json({ error: "Forbidden" }, 403);
|
|
1339
|
+
}
|
|
1340
|
+
if (relative.includes("../") || relative.includes("..\\")) {
|
|
1300
1341
|
return ctx.json({ error: "Forbidden" }, 403);
|
|
1301
1342
|
}
|
|
1302
|
-
|
|
1343
|
+
const requestPath = resolve(join(rootPath, relative));
|
|
1344
|
+
const normalizedRoot = resolve(rootPath);
|
|
1345
|
+
if (!requestPath.startsWith(normalizedRoot + sep) && requestPath !== normalizedRoot) {
|
|
1303
1346
|
return ctx.json({ error: "Forbidden" }, 403);
|
|
1304
1347
|
}
|
|
1305
1348
|
if (config.hooks?.onRequest) {
|
|
@@ -2148,10 +2191,13 @@ class ShokupanRouter {
|
|
|
2148
2191
|
}
|
|
2149
2192
|
parsePath(path) {
|
|
2150
2193
|
const keys = [];
|
|
2194
|
+
if (path.length > 2048) {
|
|
2195
|
+
throw new Error("Path too long");
|
|
2196
|
+
}
|
|
2151
2197
|
const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
2152
2198
|
keys.push(key);
|
|
2153
|
-
return "([^/]
|
|
2154
|
-
}).replace(/\*\*/g, "
|
|
2199
|
+
return "([^/]{1,255})";
|
|
2200
|
+
}).replace(/\*\*/g, ".{0,1000}").replace(/\*/g, "[^/]{1,255}");
|
|
2155
2201
|
return {
|
|
2156
2202
|
regex: new RegExp(`^${pattern}$`),
|
|
2157
2203
|
keys
|
|
@@ -2939,9 +2985,10 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2939
2985
|
} else {
|
|
2940
2986
|
return ctx.text("Provider config error", 500);
|
|
2941
2987
|
}
|
|
2942
|
-
|
|
2988
|
+
const isSecure = ctx.secure;
|
|
2989
|
+
ctx.res.headers.set("Set-Cookie", `oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
|
|
2943
2990
|
if (codeVerifier) {
|
|
2944
|
-
ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; Max-Age=600`);
|
|
2991
|
+
ctx.res.headers.append("Set-Cookie", `oauth_verifier=${codeVerifier}; Path=/; HttpOnly; SameSite=Lax${isSecure ? "; Secure" : ""}; Max-Age=600`);
|
|
2945
2992
|
}
|
|
2946
2993
|
return ctx.redirect(url.toString());
|
|
2947
2994
|
});
|
|
@@ -2982,7 +3029,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2982
3029
|
return ctx.json({ token: jwt, user });
|
|
2983
3030
|
} catch (e) {
|
|
2984
3031
|
console.error("Auth Error", e);
|
|
2985
|
-
return ctx.text("Authentication failed
|
|
3032
|
+
return ctx.text("Authentication failed. Please try again.", 500);
|
|
2986
3033
|
}
|
|
2987
3034
|
});
|
|
2988
3035
|
}
|
|
@@ -3193,14 +3240,21 @@ function Cors(options = {}) {
|
|
|
3193
3240
|
const origin = ctx.headers.get("origin");
|
|
3194
3241
|
const set = (k, v) => headers.set(k, v);
|
|
3195
3242
|
const append = (k, v) => headers.append(k, v);
|
|
3243
|
+
if (origin === "null" && opts.origin !== "null") {
|
|
3244
|
+
return next();
|
|
3245
|
+
}
|
|
3196
3246
|
if (opts.origin === "*") {
|
|
3197
3247
|
set("Access-Control-Allow-Origin", "*");
|
|
3198
3248
|
} else if (typeof opts.origin === "string") {
|
|
3199
3249
|
set("Access-Control-Allow-Origin", opts.origin);
|
|
3200
3250
|
} else if (Array.isArray(opts.origin)) {
|
|
3201
|
-
if (origin
|
|
3202
|
-
|
|
3203
|
-
|
|
3251
|
+
if (origin) {
|
|
3252
|
+
const normalizedOrigin = origin.toLowerCase();
|
|
3253
|
+
const normalizedAllowed = opts.origin.map((o) => o.toLowerCase());
|
|
3254
|
+
if (normalizedAllowed.includes(normalizedOrigin)) {
|
|
3255
|
+
set("Access-Control-Allow-Origin", origin);
|
|
3256
|
+
append("Vary", "Origin");
|
|
3257
|
+
}
|
|
3204
3258
|
}
|
|
3205
3259
|
} else if (typeof opts.origin === "function") {
|
|
3206
3260
|
const allowed = opts.origin(ctx);
|
|
@@ -3887,11 +3941,17 @@ function unsign(input, secret) {
|
|
|
3887
3941
|
if (typeof secret !== "string") throw new TypeError("Secret string must be provided.");
|
|
3888
3942
|
const tentValue = input.slice(0, input.lastIndexOf("."));
|
|
3889
3943
|
const expectedInput = sign(tentValue, secret);
|
|
3890
|
-
const
|
|
3891
|
-
const
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3944
|
+
const maxLength = Math.max(expectedInput.length, input.length);
|
|
3945
|
+
const paddedExpected = Buffer.alloc(maxLength);
|
|
3946
|
+
const paddedInput = Buffer.alloc(maxLength);
|
|
3947
|
+
Buffer.from(expectedInput).copy(paddedExpected);
|
|
3948
|
+
Buffer.from(input).copy(paddedInput);
|
|
3949
|
+
try {
|
|
3950
|
+
const valid = require("crypto").timingSafeEqual(paddedExpected, paddedInput);
|
|
3951
|
+
return valid ? tentValue : false;
|
|
3952
|
+
} catch {
|
|
3953
|
+
return false;
|
|
3954
|
+
}
|
|
3895
3955
|
}
|
|
3896
3956
|
function Session(options) {
|
|
3897
3957
|
const store = options.store || new MemoryStore();
|