@tknf/matchbox 0.2.6 → 0.3.0
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/dist/cgi.d.ts +10 -21
- package/dist/cgi.js +26 -97
- package/dist/htaccess/access-control.d.ts +9 -0
- package/dist/htaccess/access-control.js +91 -0
- package/dist/htaccess/error-document.d.ts +9 -0
- package/dist/htaccess/error-document.js +16 -0
- package/dist/htaccess/headers.d.ts +32 -0
- package/dist/htaccess/headers.js +98 -0
- package/dist/htaccess/index.d.ts +8 -0
- package/dist/htaccess/index.js +20 -0
- package/dist/htaccess/parser.d.ts +9 -0
- package/dist/htaccess/parser.js +365 -0
- package/dist/htaccess/rewrite.d.ts +13 -0
- package/dist/htaccess/rewrite.js +69 -0
- package/dist/htaccess/types.d.ts +156 -0
- package/dist/htaccess/types.js +0 -0
- package/dist/htaccess/utils.d.ts +21 -0
- package/dist/htaccess/utils.js +69 -0
- package/dist/html.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/middleware/auth.d.ts +8 -0
- package/dist/middleware/auth.js +28 -0
- package/dist/middleware/htaccess.d.ts +13 -0
- package/dist/middleware/htaccess.js +42 -0
- package/dist/middleware/index.d.ts +10 -0
- package/dist/middleware/index.js +14 -0
- package/dist/middleware/protected-files.d.ts +8 -0
- package/dist/middleware/protected-files.js +14 -0
- package/dist/middleware/session.d.ts +16 -0
- package/dist/middleware/session.js +37 -0
- package/dist/middleware/trailing-slash.d.ts +8 -0
- package/dist/middleware/trailing-slash.js +12 -0
- package/dist/with-defaults.d.ts +2 -1
- package/dist/with-defaults.js +17 -28
- package/package.json +1 -1
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
function tokenize(line) {
|
|
2
|
+
const tokens = [];
|
|
3
|
+
let current = "";
|
|
4
|
+
let inQuotes = false;
|
|
5
|
+
let quoteChar = "";
|
|
6
|
+
let escaped = false;
|
|
7
|
+
for (let i = 0; i < line.length; i++) {
|
|
8
|
+
const char = line[i];
|
|
9
|
+
if (escaped) {
|
|
10
|
+
current += char;
|
|
11
|
+
escaped = false;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (char === "\\") {
|
|
15
|
+
escaped = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
19
|
+
inQuotes = true;
|
|
20
|
+
quoteChar = char;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (char === quoteChar && inQuotes) {
|
|
24
|
+
inQuotes = false;
|
|
25
|
+
tokens.push(current);
|
|
26
|
+
current = "";
|
|
27
|
+
quoteChar = "";
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (/\s/.test(char) && !inQuotes) {
|
|
31
|
+
if (current) {
|
|
32
|
+
tokens.push(current);
|
|
33
|
+
current = "";
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
current += char;
|
|
38
|
+
}
|
|
39
|
+
if (current) tokens.push(current);
|
|
40
|
+
return tokens;
|
|
41
|
+
}
|
|
42
|
+
function parseRewriteFlags(flagString) {
|
|
43
|
+
const flags = {};
|
|
44
|
+
const cleaned = flagString.replace(/^\[|\]$/g, "").trim();
|
|
45
|
+
if (!cleaned) return flags;
|
|
46
|
+
const parts = cleaned.includes(",") ? cleaned.split(",") : cleaned.split(/\s+/);
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
const trimmed = part.trim();
|
|
49
|
+
if (!trimmed) continue;
|
|
50
|
+
if (trimmed === "L") flags.last = true;
|
|
51
|
+
else if (trimmed === "NC") flags.noCase = true;
|
|
52
|
+
else if (trimmed === "QSA") flags.qsAppend = true;
|
|
53
|
+
else if (trimmed === "QSD") flags.qsDiscard = true;
|
|
54
|
+
else if (trimmed === "NE") flags.noEscape = true;
|
|
55
|
+
else if (trimmed === "F") flags.forbidden = true;
|
|
56
|
+
else if (trimmed === "G") flags.gone = true;
|
|
57
|
+
else if (trimmed === "R" || trimmed.startsWith("R=")) {
|
|
58
|
+
const match = trimmed.match(/^R(?:=(\d+))?$/);
|
|
59
|
+
flags.redirect = match?.[1] ? Number.parseInt(match[1], 10) : 302;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return flags;
|
|
63
|
+
}
|
|
64
|
+
function parseConditionFlags(flagString) {
|
|
65
|
+
const flags = {};
|
|
66
|
+
const cleaned = flagString.replace(/^\[|\]$/g, "");
|
|
67
|
+
if (!cleaned) return flags;
|
|
68
|
+
const parts = cleaned.split(",");
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
const trimmed = part.trim();
|
|
71
|
+
if (trimmed === "NC") flags.noCase = true;
|
|
72
|
+
else if (trimmed === "OR") flags.or = true;
|
|
73
|
+
}
|
|
74
|
+
return flags;
|
|
75
|
+
}
|
|
76
|
+
function parseRewriteCond(tokens) {
|
|
77
|
+
if (tokens.length < 3) {
|
|
78
|
+
throw new Error("RewriteCond requires at least 2 arguments");
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
testString: tokens[1],
|
|
82
|
+
pattern: tokens[2],
|
|
83
|
+
flags: parseConditionFlags(tokens[3] || "")
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function parseRewriteRule(tokens) {
|
|
87
|
+
if (tokens.length < 3) {
|
|
88
|
+
throw new Error("RewriteRule requires at least 2 arguments");
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
type: "rewrite",
|
|
92
|
+
pattern: tokens[1],
|
|
93
|
+
target: tokens[2],
|
|
94
|
+
flags: parseRewriteFlags(tokens[3] || ""),
|
|
95
|
+
conditions: []
|
|
96
|
+
// Will be populated by main parser
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseErrorDocument(tokens) {
|
|
100
|
+
if (tokens.length < 3) {
|
|
101
|
+
throw new Error("ErrorDocument requires status code and target");
|
|
102
|
+
}
|
|
103
|
+
const statusCode = Number.parseInt(tokens[1], 10);
|
|
104
|
+
if (Number.isNaN(statusCode) || statusCode < 100 || statusCode >= 600) {
|
|
105
|
+
throw new Error(`Invalid status code: ${tokens[1]}`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
statusCode,
|
|
109
|
+
target: tokens[2]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function parseHeader(tokens) {
|
|
113
|
+
if (tokens.length < 3) {
|
|
114
|
+
throw new Error("Header requires action and name");
|
|
115
|
+
}
|
|
116
|
+
const action = tokens[1].toLowerCase();
|
|
117
|
+
if (!["set", "append", "unset"].includes(action)) {
|
|
118
|
+
throw new Error(`Invalid header action: ${action}`);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
action,
|
|
122
|
+
name: tokens[2],
|
|
123
|
+
value: tokens[3] || void 0
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parseRedirect(tokens, directive) {
|
|
127
|
+
let code = 302;
|
|
128
|
+
let sourceIdx = 1;
|
|
129
|
+
if (directive === "RedirectPermanent") {
|
|
130
|
+
code = 301;
|
|
131
|
+
} else if (directive === "RedirectTemp") {
|
|
132
|
+
code = 302;
|
|
133
|
+
} else if (directive === "Redirect") {
|
|
134
|
+
if (tokens.length === 4) {
|
|
135
|
+
const maybeCode = Number.parseInt(tokens[1], 10);
|
|
136
|
+
code = !Number.isNaN(maybeCode) ? maybeCode : 302;
|
|
137
|
+
sourceIdx = 2;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (tokens.length < sourceIdx + 2) {
|
|
141
|
+
throw new Error(`${directive} requires source and target paths`);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
type: "redirect",
|
|
145
|
+
code,
|
|
146
|
+
source: tokens[sourceIdx],
|
|
147
|
+
target: tokens[sourceIdx + 1]
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function parseAuthType(tokens) {
|
|
151
|
+
if (tokens.length < 2) {
|
|
152
|
+
throw new Error("AuthType requires a type (Basic or Digest)");
|
|
153
|
+
}
|
|
154
|
+
const type = tokens[1];
|
|
155
|
+
if (type !== "Basic" && type !== "Digest") {
|
|
156
|
+
throw new Error(`Invalid AuthType: ${type}. Must be Basic or Digest`);
|
|
157
|
+
}
|
|
158
|
+
return type;
|
|
159
|
+
}
|
|
160
|
+
function parseRequire(tokens) {
|
|
161
|
+
if (tokens.length < 2) {
|
|
162
|
+
throw new Error("Require directive requires at least one argument");
|
|
163
|
+
}
|
|
164
|
+
const type = tokens[1];
|
|
165
|
+
if (type === "valid-user") {
|
|
166
|
+
return { type: "valid-user" };
|
|
167
|
+
}
|
|
168
|
+
if (type === "all") {
|
|
169
|
+
if (tokens.length < 3) {
|
|
170
|
+
throw new Error("Require all must specify granted or denied");
|
|
171
|
+
}
|
|
172
|
+
const granted = tokens[2] === "granted";
|
|
173
|
+
return { type: "all", granted };
|
|
174
|
+
}
|
|
175
|
+
if (type === "user") {
|
|
176
|
+
if (tokens.length < 3) {
|
|
177
|
+
throw new Error("Require user must specify at least one username");
|
|
178
|
+
}
|
|
179
|
+
return { type: "user", value: tokens.slice(2) };
|
|
180
|
+
}
|
|
181
|
+
if (type === "group") {
|
|
182
|
+
if (tokens.length < 3) {
|
|
183
|
+
throw new Error("Require group must specify at least one group name");
|
|
184
|
+
}
|
|
185
|
+
return { type: "group", value: tokens.slice(2) };
|
|
186
|
+
}
|
|
187
|
+
if (type === "ip") {
|
|
188
|
+
if (tokens.length < 3) {
|
|
189
|
+
throw new Error("Require ip must specify at least one IP or CIDR");
|
|
190
|
+
}
|
|
191
|
+
return { type: "ip", value: tokens.slice(2) };
|
|
192
|
+
}
|
|
193
|
+
if (type === "host") {
|
|
194
|
+
if (tokens.length < 3) {
|
|
195
|
+
throw new Error("Require host must specify at least one hostname");
|
|
196
|
+
}
|
|
197
|
+
return { type: "host", value: tokens.slice(2) };
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Unknown Require type: ${type}`);
|
|
200
|
+
}
|
|
201
|
+
function parseAccessRule(tokens) {
|
|
202
|
+
if (tokens.length < 3) {
|
|
203
|
+
throw new Error(`${tokens[0]} directive requires at least one argument`);
|
|
204
|
+
}
|
|
205
|
+
const fromKeyword = tokens[1];
|
|
206
|
+
if (fromKeyword !== "from") {
|
|
207
|
+
throw new Error(`${tokens[0]} directive must use 'from' keyword`);
|
|
208
|
+
}
|
|
209
|
+
const target = tokens[2];
|
|
210
|
+
if (target === "all") {
|
|
211
|
+
return { type: "all" };
|
|
212
|
+
}
|
|
213
|
+
if (target.startsWith("env=")) {
|
|
214
|
+
return { type: "env", value: target.slice(4) };
|
|
215
|
+
}
|
|
216
|
+
const isIP = /^[\d./]+$/.test(target) || target.includes(":");
|
|
217
|
+
if (isIP) {
|
|
218
|
+
return { type: "ip", value: tokens.slice(2) };
|
|
219
|
+
}
|
|
220
|
+
return { type: "host", value: tokens.slice(2) };
|
|
221
|
+
}
|
|
222
|
+
function parseHtaccess(content) {
|
|
223
|
+
const config = {
|
|
224
|
+
rewriteRules: [],
|
|
225
|
+
redirects: [],
|
|
226
|
+
errorDocuments: [],
|
|
227
|
+
headers: [],
|
|
228
|
+
authConfig: void 0,
|
|
229
|
+
accessControl: void 0
|
|
230
|
+
};
|
|
231
|
+
const lines = content.split("\n");
|
|
232
|
+
const pendingConditions = [];
|
|
233
|
+
let multiLineBuffer = "";
|
|
234
|
+
for (let i = 0; i < lines.length; i++) {
|
|
235
|
+
let line = lines[i].trim();
|
|
236
|
+
if (!line || line.startsWith("#")) continue;
|
|
237
|
+
if (line.endsWith("\\")) {
|
|
238
|
+
multiLineBuffer += line.slice(0, -1) + " ";
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (multiLineBuffer) {
|
|
242
|
+
line = multiLineBuffer + line;
|
|
243
|
+
multiLineBuffer = "";
|
|
244
|
+
}
|
|
245
|
+
const tokens = tokenize(line);
|
|
246
|
+
if (tokens.length === 0) continue;
|
|
247
|
+
const directive = tokens[0];
|
|
248
|
+
try {
|
|
249
|
+
switch (directive) {
|
|
250
|
+
case "RewriteCond":
|
|
251
|
+
pendingConditions.push(parseRewriteCond(tokens));
|
|
252
|
+
break;
|
|
253
|
+
case "RewriteRule": {
|
|
254
|
+
const rule = parseRewriteRule(tokens);
|
|
255
|
+
rule.conditions = [...pendingConditions];
|
|
256
|
+
config.rewriteRules.push(rule);
|
|
257
|
+
pendingConditions.length = 0;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "Redirect":
|
|
261
|
+
case "RedirectPermanent":
|
|
262
|
+
case "RedirectTemp":
|
|
263
|
+
config.redirects.push(parseRedirect(tokens, directive));
|
|
264
|
+
pendingConditions.length = 0;
|
|
265
|
+
break;
|
|
266
|
+
case "ErrorDocument":
|
|
267
|
+
config.errorDocuments.push(parseErrorDocument(tokens));
|
|
268
|
+
break;
|
|
269
|
+
case "Header":
|
|
270
|
+
config.headers.push(parseHeader(tokens));
|
|
271
|
+
break;
|
|
272
|
+
case "AuthType":
|
|
273
|
+
if (!config.authConfig) {
|
|
274
|
+
config.authConfig = {};
|
|
275
|
+
}
|
|
276
|
+
config.authConfig.authType = parseAuthType(tokens);
|
|
277
|
+
break;
|
|
278
|
+
case "AuthName":
|
|
279
|
+
if (!config.authConfig) {
|
|
280
|
+
config.authConfig = {};
|
|
281
|
+
}
|
|
282
|
+
if (tokens.length < 2) {
|
|
283
|
+
throw new Error("AuthName requires a realm name");
|
|
284
|
+
}
|
|
285
|
+
config.authConfig.authName = tokens.slice(1).join(" ");
|
|
286
|
+
break;
|
|
287
|
+
case "AuthUserFile":
|
|
288
|
+
if (!config.authConfig) {
|
|
289
|
+
config.authConfig = {};
|
|
290
|
+
}
|
|
291
|
+
if (tokens.length < 2) {
|
|
292
|
+
throw new Error("AuthUserFile requires a file path");
|
|
293
|
+
}
|
|
294
|
+
config.authConfig.authUserFile = tokens[1];
|
|
295
|
+
break;
|
|
296
|
+
case "AuthGroupFile":
|
|
297
|
+
if (!config.authConfig) {
|
|
298
|
+
config.authConfig = {};
|
|
299
|
+
}
|
|
300
|
+
if (tokens.length < 2) {
|
|
301
|
+
throw new Error("AuthGroupFile requires a file path");
|
|
302
|
+
}
|
|
303
|
+
config.authConfig.authGroupFile = tokens[1];
|
|
304
|
+
break;
|
|
305
|
+
case "AuthDigestProvider":
|
|
306
|
+
if (!config.authConfig) {
|
|
307
|
+
config.authConfig = {};
|
|
308
|
+
}
|
|
309
|
+
if (tokens.length < 2) {
|
|
310
|
+
throw new Error("AuthDigestProvider requires a provider name");
|
|
311
|
+
}
|
|
312
|
+
config.authConfig.authDigestProvider = tokens[1];
|
|
313
|
+
break;
|
|
314
|
+
case "Require":
|
|
315
|
+
if (!config.authConfig) {
|
|
316
|
+
config.authConfig = {};
|
|
317
|
+
}
|
|
318
|
+
if (!config.authConfig.require) {
|
|
319
|
+
config.authConfig.require = [];
|
|
320
|
+
}
|
|
321
|
+
config.authConfig.require.push(parseRequire(tokens));
|
|
322
|
+
break;
|
|
323
|
+
case "Order": {
|
|
324
|
+
if (!config.accessControl) {
|
|
325
|
+
config.accessControl = { allow: [], deny: [] };
|
|
326
|
+
}
|
|
327
|
+
if (tokens.length < 2) {
|
|
328
|
+
throw new Error("Order directive requires an argument");
|
|
329
|
+
}
|
|
330
|
+
const orderValue = tokens[1].toLowerCase();
|
|
331
|
+
if (orderValue !== "allow,deny" && orderValue !== "deny,allow" && orderValue !== "mutual-failure") {
|
|
332
|
+
throw new Error(`Invalid Order value: ${tokens[1]}`);
|
|
333
|
+
}
|
|
334
|
+
config.accessControl.order = orderValue;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case "Allow":
|
|
338
|
+
if (!config.accessControl) {
|
|
339
|
+
config.accessControl = { allow: [], deny: [] };
|
|
340
|
+
}
|
|
341
|
+
config.accessControl.allow.push(parseAccessRule(tokens));
|
|
342
|
+
break;
|
|
343
|
+
case "Deny":
|
|
344
|
+
if (!config.accessControl) {
|
|
345
|
+
config.accessControl = { allow: [], deny: [] };
|
|
346
|
+
}
|
|
347
|
+
config.accessControl.deny.push(parseAccessRule(tokens));
|
|
348
|
+
break;
|
|
349
|
+
default:
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw new Error(`Parse error at line ${i + 1}: ${error.message}`, {
|
|
354
|
+
cause: error
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (config.accessControl && !config.accessControl.order) {
|
|
359
|
+
config.accessControl.order = "allow,deny";
|
|
360
|
+
}
|
|
361
|
+
return config;
|
|
362
|
+
}
|
|
363
|
+
export {
|
|
364
|
+
parseHtaccess
|
|
365
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { RewriteRuleConfig, RewriteCondition } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Evaluate all conditions for a rewrite rule
|
|
6
|
+
*/
|
|
7
|
+
declare function evaluateConditions(conditions: RewriteCondition[], context: Context): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Create middleware for rewrite rules
|
|
10
|
+
*/
|
|
11
|
+
declare function createRewriteMiddleware(rules: RewriteRuleConfig[], basePath: string): (c: Context, next: () => Promise<void>) => Promise<Response | void>;
|
|
12
|
+
|
|
13
|
+
export { createRewriteMiddleware, evaluateConditions };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyRewriteFlags,
|
|
3
|
+
buildVariableContext,
|
|
4
|
+
expandVariables,
|
|
5
|
+
testCondition
|
|
6
|
+
} from "./utils.js";
|
|
7
|
+
function evaluateConditions(conditions, context) {
|
|
8
|
+
if (conditions.length === 0) return true;
|
|
9
|
+
let result = true;
|
|
10
|
+
let nextIsOr = false;
|
|
11
|
+
for (const condition of conditions) {
|
|
12
|
+
const varContext = buildVariableContext(context);
|
|
13
|
+
const match = testCondition(condition, varContext);
|
|
14
|
+
if (nextIsOr) {
|
|
15
|
+
result = result || match;
|
|
16
|
+
nextIsOr = condition.flags.or || false;
|
|
17
|
+
} else {
|
|
18
|
+
result = result && match;
|
|
19
|
+
nextIsOr = condition.flags.or || false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function createRewriteMiddleware(rules, basePath) {
|
|
25
|
+
return async (c, next) => {
|
|
26
|
+
const relPath = c.req.path.replace(basePath, "") || "/";
|
|
27
|
+
for (const rule of rules) {
|
|
28
|
+
if (!evaluateConditions(rule.conditions, c)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const pattern = new RegExp(rule.pattern, rule.flags.noCase ? "i" : "");
|
|
32
|
+
const match = relPath.match(pattern);
|
|
33
|
+
if (!match) continue;
|
|
34
|
+
let target = rule.target;
|
|
35
|
+
for (let i = 0; i < match.length; i++) {
|
|
36
|
+
target = target.replace(new RegExp(`\\$${i}`, "g"), match[i] || "");
|
|
37
|
+
}
|
|
38
|
+
const varContext = buildVariableContext(c);
|
|
39
|
+
target = expandVariables(target, varContext);
|
|
40
|
+
if (target !== "-" && !target.startsWith("/") && !target.startsWith("http://") && !target.startsWith("https://")) {
|
|
41
|
+
target = basePath === "" ? `/${target}` : `${basePath}/${target}`;
|
|
42
|
+
}
|
|
43
|
+
const result = applyRewriteFlags(target, rule.flags, c);
|
|
44
|
+
switch (result.type) {
|
|
45
|
+
case "redirect":
|
|
46
|
+
return new Response(null, {
|
|
47
|
+
status: result.status,
|
|
48
|
+
headers: {
|
|
49
|
+
Location: result.url
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
case "forbidden":
|
|
53
|
+
return c.text("Forbidden", 403);
|
|
54
|
+
case "gone":
|
|
55
|
+
return c.text("Gone", 410);
|
|
56
|
+
case "rewrite":
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (rule.flags.last) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await next();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
createRewriteMiddleware,
|
|
68
|
+
evaluateConditions
|
|
69
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Main configuration type for .htaccess files (replaces RewriteMap)
|
|
5
|
+
* Maps directory paths to their respective configurations
|
|
6
|
+
*/
|
|
7
|
+
type HtaccessConfig = Record<string, DirectoryConfig>;
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for a single directory
|
|
10
|
+
*/
|
|
11
|
+
interface DirectoryConfig {
|
|
12
|
+
rewriteRules: RewriteRuleConfig[];
|
|
13
|
+
redirects: RedirectConfig[];
|
|
14
|
+
errorDocuments: ErrorDocumentConfig[];
|
|
15
|
+
headers: HeaderConfig[];
|
|
16
|
+
authConfig?: AuthConfig;
|
|
17
|
+
accessControl?: AccessControlConfig;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* RewriteRule configuration
|
|
21
|
+
*/
|
|
22
|
+
interface RewriteRuleConfig {
|
|
23
|
+
type: "rewrite";
|
|
24
|
+
pattern: string;
|
|
25
|
+
target: string;
|
|
26
|
+
flags: RewriteFlags;
|
|
27
|
+
conditions: RewriteCondition[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* RewriteCond configuration
|
|
31
|
+
*/
|
|
32
|
+
interface RewriteCondition {
|
|
33
|
+
testString: string;
|
|
34
|
+
pattern: string;
|
|
35
|
+
flags: ConditionFlags;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* RewriteRule flags
|
|
39
|
+
*/
|
|
40
|
+
interface RewriteFlags {
|
|
41
|
+
last?: boolean;
|
|
42
|
+
redirect?: number;
|
|
43
|
+
forbidden?: boolean;
|
|
44
|
+
gone?: boolean;
|
|
45
|
+
noCase?: boolean;
|
|
46
|
+
qsAppend?: boolean;
|
|
47
|
+
qsDiscard?: boolean;
|
|
48
|
+
noEscape?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* RewriteCond flags
|
|
52
|
+
*/
|
|
53
|
+
interface ConditionFlags {
|
|
54
|
+
noCase?: boolean;
|
|
55
|
+
or?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* ErrorDocument configuration
|
|
59
|
+
*/
|
|
60
|
+
interface ErrorDocumentConfig {
|
|
61
|
+
statusCode: number;
|
|
62
|
+
target: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Header directive configuration
|
|
66
|
+
*/
|
|
67
|
+
interface HeaderConfig {
|
|
68
|
+
action: "set" | "append" | "unset";
|
|
69
|
+
name: string;
|
|
70
|
+
value?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Redirect configuration (legacy support)
|
|
74
|
+
*/
|
|
75
|
+
interface RedirectConfig {
|
|
76
|
+
type: "redirect";
|
|
77
|
+
code: number;
|
|
78
|
+
source: string;
|
|
79
|
+
target: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Authentication configuration
|
|
83
|
+
*/
|
|
84
|
+
interface AuthConfig {
|
|
85
|
+
authType?: "Basic" | "Digest";
|
|
86
|
+
authName?: string;
|
|
87
|
+
authUserFile?: string;
|
|
88
|
+
authGroupFile?: string;
|
|
89
|
+
authDigestProvider?: string;
|
|
90
|
+
require?: RequireConfig[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Require directive configuration
|
|
94
|
+
*/
|
|
95
|
+
interface RequireConfig {
|
|
96
|
+
type: "valid-user" | "user" | "group" | "ip" | "host" | "all";
|
|
97
|
+
value?: string | string[];
|
|
98
|
+
granted?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Access Control configuration (Apache 2.2 style - Order/Allow/Deny)
|
|
102
|
+
*/
|
|
103
|
+
interface AccessControlConfig {
|
|
104
|
+
order?: "allow,deny" | "deny,allow" | "mutual-failure";
|
|
105
|
+
allow: AccessRule[];
|
|
106
|
+
deny: AccessRule[];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Access rule for Allow/Deny directives
|
|
110
|
+
*/
|
|
111
|
+
interface AccessRule {
|
|
112
|
+
type: "all" | "ip" | "host" | "env";
|
|
113
|
+
value?: string | string[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Variable context for RewriteCond evaluation
|
|
117
|
+
*/
|
|
118
|
+
interface VariableContext {
|
|
119
|
+
HTTP_HOST: string;
|
|
120
|
+
HTTP_USER_AGENT: string;
|
|
121
|
+
REQUEST_URI: string;
|
|
122
|
+
QUERY_STRING: string;
|
|
123
|
+
HTTPS: string;
|
|
124
|
+
REMOTE_ADDR: string;
|
|
125
|
+
REQUEST_METHOD: string;
|
|
126
|
+
HTTP_REFERER: string;
|
|
127
|
+
HTTP_ACCEPT: string;
|
|
128
|
+
HTTP_COOKIE: string;
|
|
129
|
+
SERVER_NAME: string;
|
|
130
|
+
SERVER_PORT: string;
|
|
131
|
+
DOCUMENT_ROOT: string;
|
|
132
|
+
REQUEST_FILENAME: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Result of applying rewrite flags
|
|
136
|
+
*/
|
|
137
|
+
type RewriteResult = {
|
|
138
|
+
type: "continue";
|
|
139
|
+
} | {
|
|
140
|
+
type: "redirect";
|
|
141
|
+
url: string;
|
|
142
|
+
status: number;
|
|
143
|
+
} | {
|
|
144
|
+
type: "forbidden";
|
|
145
|
+
} | {
|
|
146
|
+
type: "gone";
|
|
147
|
+
} | {
|
|
148
|
+
type: "rewrite";
|
|
149
|
+
path: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Hono Context (for type compatibility)
|
|
153
|
+
*/
|
|
154
|
+
type HonoContext = Context;
|
|
155
|
+
|
|
156
|
+
export type { AccessControlConfig, AccessRule, AuthConfig, ConditionFlags, DirectoryConfig, ErrorDocumentConfig, HeaderConfig, HonoContext, HtaccessConfig, RedirectConfig, RequireConfig, RewriteCondition, RewriteFlags, RewriteResult, RewriteRuleConfig, VariableContext };
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { VariableContext, RewriteFlags, RewriteResult, RewriteCondition } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build variable context from Hono context
|
|
6
|
+
*/
|
|
7
|
+
declare function buildVariableContext(c: Context): VariableContext;
|
|
8
|
+
/**
|
|
9
|
+
* Expand variables like %{HTTP_HOST} in test strings
|
|
10
|
+
*/
|
|
11
|
+
declare function expandVariables(testString: string, context: VariableContext): string;
|
|
12
|
+
/**
|
|
13
|
+
* Test a single RewriteCond
|
|
14
|
+
*/
|
|
15
|
+
declare function testCondition(condition: RewriteCondition, context: VariableContext): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Apply rewrite flags to determine result
|
|
18
|
+
*/
|
|
19
|
+
declare function applyRewriteFlags(target: string, flags: RewriteFlags, context: Context): RewriteResult;
|
|
20
|
+
|
|
21
|
+
export { applyRewriteFlags, buildVariableContext, expandVariables, testCondition };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function buildVariableContext(c) {
|
|
2
|
+
const url = new URL(c.req.url);
|
|
3
|
+
return {
|
|
4
|
+
HTTP_HOST: c.req.header("host") || "",
|
|
5
|
+
HTTP_USER_AGENT: c.req.header("user-agent") || "",
|
|
6
|
+
REQUEST_URI: c.req.path,
|
|
7
|
+
QUERY_STRING: url.search.slice(1),
|
|
8
|
+
HTTPS: url.protocol === "https:" ? "on" : "off",
|
|
9
|
+
REMOTE_ADDR: c.req.header("x-forwarded-for")?.split(",")[0].trim() || "127.0.0.1",
|
|
10
|
+
REQUEST_METHOD: c.req.method,
|
|
11
|
+
HTTP_REFERER: c.req.header("referer") || "",
|
|
12
|
+
HTTP_ACCEPT: c.req.header("accept") || "",
|
|
13
|
+
HTTP_COOKIE: c.req.header("cookie") || "",
|
|
14
|
+
SERVER_NAME: c.req.header("host")?.split(":")[0] || "localhost",
|
|
15
|
+
SERVER_PORT: url.port || (url.protocol === "https:" ? "443" : "80"),
|
|
16
|
+
DOCUMENT_ROOT: "",
|
|
17
|
+
REQUEST_FILENAME: c.req.path
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function expandVariables(testString, context) {
|
|
21
|
+
return testString.replace(/%\{([^}]+)\}/g, (match, varName) => {
|
|
22
|
+
const value = context[varName];
|
|
23
|
+
return value !== void 0 ? String(value) : match;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function testCondition(condition, context) {
|
|
27
|
+
const testValue = expandVariables(condition.testString, context);
|
|
28
|
+
const pattern = new RegExp(condition.pattern, condition.flags.noCase ? "i" : "");
|
|
29
|
+
return pattern.test(testValue);
|
|
30
|
+
}
|
|
31
|
+
function applyRewriteFlags(target, flags, context) {
|
|
32
|
+
if (flags.forbidden) {
|
|
33
|
+
return { type: "forbidden" };
|
|
34
|
+
}
|
|
35
|
+
if (flags.gone) {
|
|
36
|
+
return { type: "gone" };
|
|
37
|
+
}
|
|
38
|
+
if (flags.redirect) {
|
|
39
|
+
let finalTarget = target;
|
|
40
|
+
if (flags.qsAppend) {
|
|
41
|
+
const currentQs = new URL(context.req.url).search.slice(1);
|
|
42
|
+
if (currentQs) {
|
|
43
|
+
const separator = finalTarget.includes("?") ? "&" : "?";
|
|
44
|
+
finalTarget += separator + currentQs;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (flags.qsDiscard) {
|
|
48
|
+
const qsIndex = finalTarget.indexOf("?");
|
|
49
|
+
if (qsIndex !== -1) {
|
|
50
|
+
finalTarget = finalTarget.slice(0, qsIndex);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
type: "redirect",
|
|
55
|
+
url: finalTarget,
|
|
56
|
+
status: flags.redirect
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
type: "rewrite",
|
|
61
|
+
path: target
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
applyRewriteFlags,
|
|
66
|
+
buildVariableContext,
|
|
67
|
+
expandVariables,
|
|
68
|
+
testCondition
|
|
69
|
+
};
|
package/dist/html.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as hono_utils_html from 'hono/utils/html';
|
|
|
2
2
|
import { CgiContext } from './cgi.js';
|
|
3
3
|
import 'hono/types';
|
|
4
4
|
import 'hono';
|
|
5
|
+
import './htaccess/types.js';
|
|
5
6
|
|
|
6
7
|
declare const generateCgiInfo: ({ $_SERVER, $_SESSION, $_REQUEST, config, }: Pick<CgiContext, "$_SERVER" | "$_SESSION" | "$_REQUEST" | "config">) => () => hono_utils_html.HtmlEscapedString | Promise<hono_utils_html.HtmlEscapedString>;
|
|
7
8
|
declare const generateCgiError: ({ error, $_SERVER, }: {
|