@tknf/matchbox 0.2.6 → 0.3.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 +212 -46
- 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
package/README.md
CHANGED
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
# Matchbox
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A modern CGI-style web framework built on [Hono](https://hono.dev). Brings Apache-style conventions (`.htaccess`, `.htpasswd`) into the modern TypeScript/JSX ecosystem with file-based routing and familiar CGI variables.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- File-based routing
|
|
8
|
-
- CGI-style context
|
|
9
|
-
- `.htaccess` rewrites/redirects
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- **File-based routing** - `.cgi.tsx`/`.cgi.jsx` files map directly to URL endpoints
|
|
8
|
+
- **CGI-style context** - Familiar `$_GET`, `$_POST`, `$_SESSION`, `$_SERVER`, `$_COOKIE`, etc.
|
|
9
|
+
- **Apache compatibility** - `.htaccess` for rewrites/redirects/headers, `.htpasswd` for Basic Auth
|
|
10
|
+
- **Modern tooling** - Full TypeScript support, Vite integration, JSX rendering
|
|
11
|
+
- **Flexible configuration** - Session management, custom middleware, security headers
|
|
12
|
+
- **Production ready** - Comprehensive test coverage, security best practices
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Installation
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
|
-
|
|
17
|
+
npm add @tknf/matchbox hono
|
|
18
|
+
# or
|
|
19
|
+
pnpm add @tknf/matchbox hono
|
|
20
|
+
# or
|
|
21
|
+
yarn add @tknf/matchbox hono
|
|
17
22
|
```
|
|
18
23
|
|
|
19
24
|
## Quick Start
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
### 1. Configure Vite
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
Create `vite.config.ts`:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
24
31
|
import devServer from "@hono/vite-dev-server";
|
|
25
32
|
import { defineConfig } from "vite";
|
|
26
|
-
import { MatchboxPlugin } from "matchbox/plugin";
|
|
33
|
+
import { MatchboxPlugin } from "@tknf/matchbox/plugin";
|
|
27
34
|
|
|
28
35
|
export default defineConfig({
|
|
29
36
|
plugins: [
|
|
@@ -36,98 +43,257 @@ export default defineConfig({
|
|
|
36
43
|
});
|
|
37
44
|
```
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
### 2. Create Server Entry
|
|
47
|
+
|
|
48
|
+
Create `server.ts`:
|
|
40
49
|
|
|
41
|
-
```
|
|
42
|
-
import { createCgi } from "matchbox";
|
|
50
|
+
```typescript
|
|
51
|
+
import { createCgi } from "@tknf/matchbox";
|
|
43
52
|
|
|
44
53
|
export default createCgi();
|
|
45
54
|
```
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
### 3. Create Your First Page
|
|
57
|
+
|
|
58
|
+
Create `public/index.cgi.tsx`:
|
|
48
59
|
|
|
49
60
|
```tsx
|
|
50
|
-
import type { CgiContext } from "matchbox";
|
|
61
|
+
import type { CgiContext } from "@tknf/matchbox";
|
|
51
62
|
|
|
52
|
-
export default function ({ $_SERVER }: CgiContext) {
|
|
63
|
+
export default function ({ $_SERVER, $_GET }: CgiContext) {
|
|
53
64
|
return (
|
|
54
65
|
<html>
|
|
55
66
|
<head>
|
|
56
67
|
<title>Matchbox</title>
|
|
57
68
|
</head>
|
|
58
69
|
<body>
|
|
59
|
-
<h1>Hello Matchbox
|
|
60
|
-
<p>Method: {$_SERVER.REQUEST_METHOD}</p>
|
|
70
|
+
<h1>Hello from Matchbox!</h1>
|
|
71
|
+
<p>Request Method: {$_SERVER.REQUEST_METHOD}</p>
|
|
72
|
+
<p>Query Params: {JSON.stringify($_GET)}</p>
|
|
61
73
|
</body>
|
|
62
74
|
</html>
|
|
63
75
|
);
|
|
64
76
|
}
|
|
65
77
|
```
|
|
66
78
|
|
|
67
|
-
Start
|
|
79
|
+
### 4. Start Development Server
|
|
68
80
|
|
|
69
81
|
```bash
|
|
70
|
-
|
|
82
|
+
npm run dev
|
|
71
83
|
```
|
|
72
84
|
|
|
85
|
+
Visit `http://localhost:5173` to see your page.
|
|
86
|
+
|
|
73
87
|
## CGI Context
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
Every page function receives a `CgiContext` object with:
|
|
90
|
+
|
|
91
|
+
### Request Data
|
|
92
|
+
|
|
93
|
+
- `$_GET` - Query parameters
|
|
94
|
+
- `$_POST` - Form data (POST/PUT)
|
|
95
|
+
- `$_FILES` - Uploaded files
|
|
96
|
+
- `$_REQUEST` - Combined `$_GET` + `$_POST` + `$_COOKIE`
|
|
97
|
+
- `$_COOKIE` - Cookie values
|
|
98
|
+
- `$_SERVER` - Server and request information
|
|
99
|
+
- `$_ENV` - Environment variables
|
|
100
|
+
- `$_SESSION` - Session data
|
|
101
|
+
|
|
102
|
+
### Helper Functions
|
|
76
103
|
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
- `
|
|
80
|
-
- `cgiinfo()`
|
|
104
|
+
- `header(name, value)` - Set response header
|
|
105
|
+
- `status(code)` - Set HTTP status code
|
|
106
|
+
- `redirect(url, status?)` - Redirect to another URL
|
|
107
|
+
- `cgiinfo()` - HTML debug info block
|
|
108
|
+
- `get_modules()` - List all available CGI modules
|
|
109
|
+
- `get_version()` - Get Matchbox version string
|
|
110
|
+
- `log(message)` - Custom logging
|
|
111
|
+
- `request_headers()` - Get all request headers
|
|
112
|
+
- `response_headers()` - Get current response headers
|
|
113
|
+
|
|
114
|
+
### Example Usage
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
export default function (ctx: CgiContext) {
|
|
118
|
+
const { $_GET, $_POST, $_SESSION, header, status, redirect } = ctx;
|
|
119
|
+
|
|
120
|
+
// Handle form submission
|
|
121
|
+
if ($_POST.username) {
|
|
122
|
+
$_SESSION.user = $_POST.username;
|
|
123
|
+
return redirect("/dashboard");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Set custom headers
|
|
127
|
+
header("X-Custom-Header", "value");
|
|
128
|
+
status(200);
|
|
129
|
+
|
|
130
|
+
return <div>Welcome</div>;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
81
133
|
|
|
82
134
|
## Configuration
|
|
83
135
|
|
|
84
|
-
###
|
|
136
|
+
### Plugin Options
|
|
85
137
|
|
|
86
|
-
```
|
|
138
|
+
```typescript
|
|
87
139
|
MatchboxPlugin({
|
|
88
|
-
publicDir: "public",
|
|
89
|
-
config: {
|
|
140
|
+
publicDir: "public", // Directory for .cgi files (default: "public")
|
|
141
|
+
config: { // Custom config object injected into pages
|
|
142
|
+
siteName: "My Site",
|
|
143
|
+
apiUrl: "https://api.example.com"
|
|
144
|
+
}
|
|
90
145
|
});
|
|
91
146
|
```
|
|
92
147
|
|
|
93
|
-
|
|
94
|
-
- `config`: Configuration object injected into pages
|
|
148
|
+
Access config in your pages via `context.config`.
|
|
95
149
|
|
|
96
|
-
###
|
|
150
|
+
### Runtime Options
|
|
97
151
|
|
|
98
|
-
```
|
|
152
|
+
```typescript
|
|
99
153
|
createCgi({
|
|
154
|
+
// Session cookie configuration
|
|
100
155
|
sessionCookie: {
|
|
101
|
-
name: "_SESSION_ID",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
156
|
+
name: "_SESSION_ID", // Cookie name (default: "_SESSION_ID")
|
|
157
|
+
path: "/", // Cookie path (default: "/")
|
|
158
|
+
domain: "example.com", // Cookie domain
|
|
159
|
+
secure: true, // HTTPS only (default: false)
|
|
160
|
+
sameSite: "Strict", // CSRF protection: "Strict" | "Lax" | "None"
|
|
161
|
+
maxAge: 3600, // Session timeout in seconds
|
|
105
162
|
},
|
|
163
|
+
|
|
164
|
+
// URL trailing slash enforcement
|
|
106
165
|
enforceTrailingSlash: true,
|
|
166
|
+
|
|
167
|
+
// Custom middleware (runs before page handlers)
|
|
107
168
|
middleware: [
|
|
108
169
|
async (c, next) => {
|
|
109
170
|
c.header("X-App", "matchbox");
|
|
110
171
|
await next();
|
|
111
172
|
},
|
|
112
173
|
],
|
|
174
|
+
|
|
175
|
+
// Custom logger
|
|
113
176
|
logger: (message, level) => {
|
|
114
177
|
console.log(`[${level ?? "info"}] ${message}`);
|
|
115
178
|
},
|
|
116
179
|
});
|
|
117
180
|
```
|
|
118
181
|
|
|
119
|
-
##
|
|
182
|
+
## Apache-Style Features
|
|
183
|
+
|
|
184
|
+
### Basic Authentication (.htpasswd)
|
|
185
|
+
|
|
186
|
+
Place a `.htpasswd` file in any directory under `public/` to protect it:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
# Generate with: htpasswd -c .htpasswd username
|
|
190
|
+
admin:$apr1$abc123$...
|
|
191
|
+
user:$apr1$xyz789$...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
All files in that directory and subdirectories will require authentication.
|
|
195
|
+
|
|
196
|
+
### URL Rewriting and Redirects (.htaccess)
|
|
197
|
+
|
|
198
|
+
Create `.htaccess` files to configure rewrites, redirects, headers, and error pages:
|
|
199
|
+
|
|
200
|
+
```apache
|
|
201
|
+
# Permanent redirect
|
|
202
|
+
Redirect 301 /old-page /new-page
|
|
120
203
|
|
|
121
|
-
|
|
204
|
+
# Conditional rewrite
|
|
205
|
+
RewriteCond %{HTTP_HOST} ^www\.example\.com$
|
|
206
|
+
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
|
|
122
207
|
|
|
123
|
-
|
|
124
|
-
|
|
208
|
+
# Pattern-based rewrite
|
|
209
|
+
RewriteRule ^blog/(.+)$ /posts.cgi?slug=$1 [QSA,L]
|
|
125
210
|
|
|
126
|
-
|
|
211
|
+
# Forbidden
|
|
212
|
+
RewriteRule ^private$ - [F]
|
|
127
213
|
|
|
128
|
-
|
|
129
|
-
-
|
|
214
|
+
# Security headers
|
|
215
|
+
Header set X-Frame-Options "SAMEORIGIN"
|
|
216
|
+
Header set X-Content-Type-Options "nosniff"
|
|
217
|
+
Header set Strict-Transport-Security "max-age=31536000"
|
|
218
|
+
|
|
219
|
+
# Custom error pages
|
|
220
|
+
ErrorDocument 404 /errors/404.html
|
|
221
|
+
ErrorDocument 500 /errors/500.html
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Supported RewriteRule Flags
|
|
225
|
+
|
|
226
|
+
- `[L]` - Last rule, stop processing
|
|
227
|
+
- `[R]` / `[R=301]` / `[R=302]` - Redirect with status code
|
|
228
|
+
- `[F]` - Forbidden (403)
|
|
229
|
+
- `[G]` - Gone (410)
|
|
230
|
+
- `[NC]` - No Case (case-insensitive)
|
|
231
|
+
- `[QSA]` - Query String Append
|
|
232
|
+
- `[QSD]` - Query String Discard
|
|
233
|
+
- `[NE]` - No Escape
|
|
234
|
+
|
|
235
|
+
#### Supported RewriteCond Variables
|
|
236
|
+
|
|
237
|
+
- `%{HTTP_HOST}` - Request host
|
|
238
|
+
- `%{HTTP_USER_AGENT}` - User agent
|
|
239
|
+
- `%{HTTP_REFERER}` - Referer header
|
|
240
|
+
- `%{REQUEST_URI}` - Request URI
|
|
241
|
+
- `%{REQUEST_METHOD}` - HTTP method
|
|
242
|
+
- `%{QUERY_STRING}` - Query string
|
|
243
|
+
- `%{REMOTE_ADDR}` - Client IP
|
|
244
|
+
- And more... (see [documentation](./docs/htaccess.md))
|
|
245
|
+
|
|
246
|
+
## Examples
|
|
247
|
+
|
|
248
|
+
Check out the [`examples/`](./examples) directory for complete working examples:
|
|
249
|
+
|
|
250
|
+
- **basic** - Minimal setup with a simple page
|
|
251
|
+
- **htaccess-auth** - Authentication and URL rewriting
|
|
252
|
+
- **custom-middleware** - Custom middleware and logging
|
|
253
|
+
|
|
254
|
+
## Documentation
|
|
255
|
+
|
|
256
|
+
- [Apache .htaccess Features](./docs/htaccess.md) - Complete `.htaccess` feature reference
|
|
257
|
+
- [Security Guide](./docs/security.md) - Security best practices
|
|
258
|
+
- [API Reference](./docs/api.md) - Complete API documentation
|
|
259
|
+
- [Roadmap](./docs/roadmap.md) - Planned features and improvements
|
|
260
|
+
|
|
261
|
+
## Migration from v0.2.x
|
|
262
|
+
|
|
263
|
+
Version 0.3.0 introduced enhanced `.htaccess` parsing. If you're upgrading:
|
|
264
|
+
|
|
265
|
+
- ✅ Existing `.htaccess` files work without changes
|
|
266
|
+
- ✅ Both `[R=301]` and `R=301` flag syntaxes are supported
|
|
267
|
+
- ⚠️ Malformed directives now throw errors (previously silently ignored)
|
|
268
|
+
|
|
269
|
+
See the [migration guide](./docs/htaccess.md#migration-from-v02x-to-v03) for details.
|
|
270
|
+
|
|
271
|
+
## Development
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# Install dependencies
|
|
275
|
+
pnpm install
|
|
276
|
+
|
|
277
|
+
# Run tests
|
|
278
|
+
pnpm test
|
|
279
|
+
|
|
280
|
+
# Build
|
|
281
|
+
pnpm run build
|
|
282
|
+
|
|
283
|
+
# Type check
|
|
284
|
+
pnpm run typecheck
|
|
285
|
+
```
|
|
130
286
|
|
|
131
287
|
## License
|
|
132
288
|
|
|
133
|
-
MIT License
|
|
289
|
+
MIT License - see [LICENSE](./LICENSE) for details.
|
|
290
|
+
|
|
291
|
+
## Contributing
|
|
292
|
+
|
|
293
|
+
Contributions are welcome! Please read the [contributing guidelines](./CONTRIBUTING.md) before submitting PRs.
|
|
294
|
+
|
|
295
|
+
## Links
|
|
296
|
+
|
|
297
|
+
- [GitHub Repository](https://github.com/tknf-labs/matchbox)
|
|
298
|
+
- [NPM Package](https://www.npmjs.com/package/@tknf/matchbox)
|
|
299
|
+
- [Hono Framework](https://hono.dev)
|
package/dist/cgi.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import { Context, Hono } from 'hono';
|
|
3
3
|
import { HtmlEscapedString } from 'hono/utils/html';
|
|
4
|
+
import { HtaccessConfig } from './htaccess/types.js';
|
|
4
5
|
|
|
5
|
-
type ConfigObject = Record<string,
|
|
6
|
+
type ConfigObject = Record<string, unknown>;
|
|
6
7
|
/**
|
|
7
8
|
* --- Session Cookie Configuration ---
|
|
8
9
|
*/
|
|
@@ -49,10 +50,10 @@ interface CgiContext<ConfigType = ConfigObject> {
|
|
|
49
50
|
$_GET: Record<string, string>;
|
|
50
51
|
$_POST: Record<string, string>;
|
|
51
52
|
$_FILES: Record<string, File | File[]>;
|
|
52
|
-
$_REQUEST: Record<string,
|
|
53
|
-
$_SESSION: Record<string,
|
|
53
|
+
$_REQUEST: Record<string, unknown>;
|
|
54
|
+
$_SESSION: Record<string, unknown>;
|
|
54
55
|
$_COOKIE: Record<string, string>;
|
|
55
|
-
$_ENV: Record<string,
|
|
56
|
+
$_ENV: Record<string, unknown>;
|
|
56
57
|
$_SERVER: {
|
|
57
58
|
REQUEST_METHOD: string;
|
|
58
59
|
REQUEST_URI: string;
|
|
@@ -61,7 +62,7 @@ interface CgiContext<ConfigType = ConfigObject> {
|
|
|
61
62
|
SCRIPT_NAME: string;
|
|
62
63
|
PATH_INFO: string;
|
|
63
64
|
QUERY_STRING: string;
|
|
64
|
-
[key: string]:
|
|
65
|
+
[key: string]: unknown;
|
|
65
66
|
};
|
|
66
67
|
config: ConfigType;
|
|
67
68
|
c: Context;
|
|
@@ -83,24 +84,12 @@ interface CgiContext<ConfigType = ConfigObject> {
|
|
|
83
84
|
type Page = {
|
|
84
85
|
urlPath: string;
|
|
85
86
|
dirPath: string | null;
|
|
86
|
-
component: (context: CgiContext) =>
|
|
87
|
+
component: (context: CgiContext) => unknown | Promise<unknown>;
|
|
87
88
|
};
|
|
88
|
-
|
|
89
|
-
type: "redirect";
|
|
90
|
-
code: string;
|
|
91
|
-
source: string;
|
|
92
|
-
target: string;
|
|
93
|
-
};
|
|
94
|
-
type RewriteRule = {
|
|
95
|
-
type: "rewrite";
|
|
96
|
-
pattern: string;
|
|
97
|
-
target: string;
|
|
98
|
-
flags: string;
|
|
99
|
-
};
|
|
100
|
-
type RewriteMap = Record<string, Array<RedirectRule | RewriteRule>>;
|
|
89
|
+
|
|
101
90
|
/**
|
|
102
91
|
* --- Matchbox Runtime Engine ---
|
|
103
92
|
*/
|
|
104
|
-
declare const createCgiWithPages: (pages: Page[], siteConfig?: ConfigObject, authMap?: Record<string, string>,
|
|
93
|
+
declare const createCgiWithPages: (pages: Page[], siteConfig?: ConfigObject, authMap?: Record<string, string>, htaccessConfig?: HtaccessConfig, options?: MatchboxOptions) => Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
105
94
|
|
|
106
|
-
export { type CgiContext, type ConfigObject, type MatchboxOptions, type ModuleInfo, type Page, type
|
|
95
|
+
export { type CgiContext, type ConfigObject, HtaccessConfig, type MatchboxOptions, type ModuleInfo, type Page, type SessionCookieOptions, createCgiWithPages };
|
package/dist/cgi.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
3
|
-
import { getCookie, setCookie } from "hono/cookie";
|
|
2
|
+
import { getCookie } from "hono/cookie";
|
|
4
3
|
import { generateCgiError, generateCgiInfo } from "./html.js";
|
|
4
|
+
import {
|
|
5
|
+
applyBasicAuth,
|
|
6
|
+
applyHtaccessMiddleware,
|
|
7
|
+
applyErrorDocumentMiddleware,
|
|
8
|
+
getSessionFromCookie,
|
|
9
|
+
saveSessionToCookie,
|
|
10
|
+
applyProtectedFilesMiddleware,
|
|
11
|
+
applyTrailingSlashMiddleware
|
|
12
|
+
} from "./middleware/index.js";
|
|
5
13
|
const isRedirectObject = (obj) => {
|
|
6
|
-
return obj && obj.__type === "redirect" && typeof obj.url === "string";
|
|
14
|
+
return typeof obj === "object" && obj !== null && "__type" in obj && obj.__type === "redirect" && "url" in obj && typeof obj.url === "string";
|
|
7
15
|
};
|
|
8
|
-
const createCgiWithPages = (pages, siteConfig = {}, authMap = {},
|
|
16
|
+
const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, htaccessConfig = {}, options = {}) => {
|
|
9
17
|
const app = new Hono();
|
|
10
|
-
const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
|
|
11
18
|
if (options.middleware && options.middleware.length > 0) {
|
|
12
19
|
for (const mw of options.middleware) {
|
|
13
20
|
app.use("*", async (c, next) => {
|
|
@@ -18,66 +25,12 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
|
|
|
18
25
|
});
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
|
-
|
|
22
|
-
app.use("*", async (c, next) => {
|
|
23
|
-
const path = c.req.path;
|
|
24
|
-
const lastSegment = path.slice(path.lastIndexOf("/") + 1);
|
|
25
|
-
if (protectedFiles.some((file) => lastSegment === file)) {
|
|
26
|
-
return c.text("Forbidden", 403);
|
|
27
|
-
}
|
|
28
|
-
await next();
|
|
29
|
-
});
|
|
28
|
+
applyProtectedFilesMiddleware(app);
|
|
30
29
|
if (options.enforceTrailingSlash) {
|
|
31
|
-
app
|
|
32
|
-
const path = c.req.path;
|
|
33
|
-
if (!path.endsWith("/") && !path.includes(".")) {
|
|
34
|
-
return c.redirect(`${path}/`, 301);
|
|
35
|
-
}
|
|
36
|
-
await next();
|
|
37
|
-
});
|
|
30
|
+
applyTrailingSlashMiddleware(app);
|
|
38
31
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
app.use(`${basePath}/*`, async (c, next) => {
|
|
42
|
-
const relPath = c.req.path.replace(basePath, "") || "/";
|
|
43
|
-
for (const rule of rules) {
|
|
44
|
-
if (rule.type === "redirect") {
|
|
45
|
-
if (relPath === rule.source) {
|
|
46
|
-
return c.redirect(
|
|
47
|
-
rule.target,
|
|
48
|
-
Number.parseInt(rule.code, 10) || 302
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
} else if (rule.type === "rewrite") {
|
|
52
|
-
const regex = new RegExp(rule.pattern);
|
|
53
|
-
if (regex.test(relPath)) {
|
|
54
|
-
const target = rule.target.startsWith("/") ? rule.target : `${basePath}/${rule.target}`;
|
|
55
|
-
if (rule.flags.includes("R")) {
|
|
56
|
-
const code = rule.flags.match(/R=(\d+)/)?.[1] || "302";
|
|
57
|
-
return c.redirect(target, Number.parseInt(code, 10));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
await next();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
Object.entries(authMap).forEach(([dir, content]) => {
|
|
66
|
-
const credentials = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
|
|
67
|
-
const [username, password] = line.split(":");
|
|
68
|
-
return { username, password };
|
|
69
|
-
});
|
|
70
|
-
if (credentials.length > 0) {
|
|
71
|
-
const authPath = dir === "/" ? "*" : `${dir.replace(/\/$/, "")}/*`;
|
|
72
|
-
app.use(authPath, async (c, next) => {
|
|
73
|
-
const handler = basicAuth({
|
|
74
|
-
verifyUser: (u, p) => credentials.some((cred) => cred.username === u && cred.password === p),
|
|
75
|
-
realm: "Restricted Area"
|
|
76
|
-
});
|
|
77
|
-
return handler(c, next);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
});
|
|
32
|
+
applyHtaccessMiddleware(app, htaccessConfig);
|
|
33
|
+
applyBasicAuth(app, authMap);
|
|
81
34
|
pages.forEach(({ urlPath, dirPath, component }) => {
|
|
82
35
|
const routes = [urlPath, `${urlPath}/*`];
|
|
83
36
|
if (dirPath) {
|
|
@@ -96,7 +49,7 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
|
|
|
96
49
|
if (value instanceof File || Array.isArray(value) && value[0] instanceof File) {
|
|
97
50
|
$_FILES[key] = value;
|
|
98
51
|
} else {
|
|
99
|
-
$_POST[key] = value;
|
|
52
|
+
$_POST[key] = String(value);
|
|
100
53
|
}
|
|
101
54
|
}
|
|
102
55
|
const $_COOKIE = getCookie(c);
|
|
@@ -106,14 +59,10 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
|
|
|
106
59
|
...$_POST
|
|
107
60
|
};
|
|
108
61
|
const $_ENV = typeof process !== "undefined" && process.env ? process.env : c.env || {};
|
|
109
|
-
let $_SESSION =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
$_SESSION = JSON.parse(decodeURIComponent(sRaw));
|
|
114
|
-
} catch {
|
|
115
|
-
}
|
|
116
|
-
}
|
|
62
|
+
let $_SESSION = getSessionFromCookie(
|
|
63
|
+
c,
|
|
64
|
+
options.sessionCookie?.name
|
|
65
|
+
);
|
|
117
66
|
let responseStatus = 200;
|
|
118
67
|
const responseHeaders = {
|
|
119
68
|
"Content-Type": "text/html; charset=utf-8"
|
|
@@ -169,7 +118,7 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
|
|
|
169
118
|
}
|
|
170
119
|
},
|
|
171
120
|
get_version: () => {
|
|
172
|
-
return `MatchboxCGI/v${"0.
|
|
121
|
+
return `MatchboxCGI/v${"0.3.1"}`;
|
|
173
122
|
},
|
|
174
123
|
/**
|
|
175
124
|
* Returns information about all loaded CGI modules
|
|
@@ -184,48 +133,28 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
|
|
|
184
133
|
};
|
|
185
134
|
try {
|
|
186
135
|
const result = await component(context);
|
|
187
|
-
|
|
188
|
-
const sessionOptions = {
|
|
189
|
-
path: options.sessionCookie?.path || "/",
|
|
190
|
-
httpOnly: true,
|
|
191
|
-
sameSite: options.sessionCookie?.sameSite || "Lax"
|
|
192
|
-
};
|
|
193
|
-
if (options.sessionCookie?.secure !== void 0) {
|
|
194
|
-
sessionOptions.secure = options.sessionCookie.secure;
|
|
195
|
-
}
|
|
196
|
-
if (options.sessionCookie?.domain) {
|
|
197
|
-
sessionOptions.domain = options.sessionCookie.domain;
|
|
198
|
-
}
|
|
199
|
-
if (options.sessionCookie?.maxAge) {
|
|
200
|
-
sessionOptions.maxAge = options.sessionCookie.maxAge;
|
|
201
|
-
}
|
|
136
|
+
saveSessionToCookie(c, $_SESSION, options.sessionCookie);
|
|
202
137
|
if (isRedirectObject(result)) {
|
|
203
|
-
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
204
138
|
return c.redirect(result.url, result.status);
|
|
205
139
|
}
|
|
206
140
|
if (result instanceof Response) {
|
|
207
|
-
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
208
141
|
return result;
|
|
209
142
|
}
|
|
210
|
-
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
211
143
|
Object.entries(responseHeaders).forEach(([key, value]) => {
|
|
212
144
|
c.header(key, value);
|
|
213
145
|
});
|
|
214
146
|
const contentType = responseHeaders["content-type"];
|
|
215
147
|
if (contentType?.includes("application/json")) {
|
|
216
|
-
return c.json(
|
|
217
|
-
// biome-ignore lint/suspicious/noExplicitAny: to JSON response
|
|
218
|
-
result ?? { success: true },
|
|
219
|
-
responseStatus
|
|
220
|
-
);
|
|
148
|
+
return c.json(result ?? { success: true }, responseStatus);
|
|
221
149
|
}
|
|
222
|
-
return c.html(result, responseStatus);
|
|
150
|
+
return c.html(String(result ?? ""), responseStatus);
|
|
223
151
|
} catch (error) {
|
|
224
152
|
return c.html(generateCgiError({ error, $_SERVER }), 500);
|
|
225
153
|
}
|
|
226
154
|
});
|
|
227
155
|
});
|
|
228
156
|
});
|
|
157
|
+
applyErrorDocumentMiddleware(app, htaccessConfig);
|
|
229
158
|
return app;
|
|
230
159
|
};
|
|
231
160
|
export {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { AccessControlConfig } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create middleware for access control (Order/Allow/Deny)
|
|
6
|
+
*/
|
|
7
|
+
declare function createAccessControlMiddleware(config: AccessControlConfig): (c: Context, next: () => Promise<void>) => Promise<Response | void>;
|
|
8
|
+
|
|
9
|
+
export { createAccessControlMiddleware };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function matchesIP(clientIP, ruleValue) {
|
|
2
|
+
const ips = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
|
3
|
+
for (const ip of ips) {
|
|
4
|
+
if (clientIP === ip) return true;
|
|
5
|
+
if (ip.includes("/")) {
|
|
6
|
+
const [network, bits] = ip.split("/");
|
|
7
|
+
const mask = parseInt(bits, 10);
|
|
8
|
+
if (isSameNetwork(clientIP, network, mask)) return true;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
function isSameNetwork(ip1, ip2, maskBits) {
|
|
14
|
+
const ip1Parts = ip1.split(".").map(Number);
|
|
15
|
+
const ip2Parts = ip2.split(".").map(Number);
|
|
16
|
+
let bits = maskBits;
|
|
17
|
+
for (let i = 0; i < 4; i++) {
|
|
18
|
+
if (bits <= 0) break;
|
|
19
|
+
const mask = bits >= 8 ? 255 : 256 - Math.pow(2, 8 - bits);
|
|
20
|
+
if ((ip1Parts[i] & mask) !== (ip2Parts[i] & mask)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
bits -= 8;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function matchesHost(clientHost, ruleValue) {
|
|
28
|
+
const hosts = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
|
29
|
+
for (const host of hosts) {
|
|
30
|
+
if (clientHost === host) return true;
|
|
31
|
+
if (host.startsWith("*.")) {
|
|
32
|
+
const domain = host.slice(2);
|
|
33
|
+
if (clientHost.endsWith(domain)) return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function matchesRule(rule, c) {
|
|
39
|
+
switch (rule.type) {
|
|
40
|
+
case "all":
|
|
41
|
+
return true;
|
|
42
|
+
case "ip": {
|
|
43
|
+
const clientIP = c.req.header("x-forwarded-for")?.split(",")[0].trim() || c.req.header("x-real-ip") || c.env?.REMOTE_ADDR || "127.0.0.1";
|
|
44
|
+
return rule.value ? matchesIP(clientIP, rule.value) : false;
|
|
45
|
+
}
|
|
46
|
+
case "host": {
|
|
47
|
+
const clientHost = c.req.header("host") || "";
|
|
48
|
+
return rule.value ? matchesHost(clientHost, rule.value) : false;
|
|
49
|
+
}
|
|
50
|
+
case "env": {
|
|
51
|
+
if (!rule.value) return false;
|
|
52
|
+
const envVar = typeof rule.value === "string" ? rule.value : rule.value[0];
|
|
53
|
+
return c.env?.[envVar] !== void 0;
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function evaluateAccessControl(config, c) {
|
|
60
|
+
const order = config.order || "allow,deny";
|
|
61
|
+
const allowMatches = config.allow.some((rule) => matchesRule(rule, c));
|
|
62
|
+
const denyMatches = config.deny.some((rule) => matchesRule(rule, c));
|
|
63
|
+
switch (order) {
|
|
64
|
+
case "allow,deny":
|
|
65
|
+
if (denyMatches) return false;
|
|
66
|
+
if (allowMatches) return true;
|
|
67
|
+
return false;
|
|
68
|
+
case "deny,allow":
|
|
69
|
+
if (allowMatches) return true;
|
|
70
|
+
if (denyMatches) return false;
|
|
71
|
+
return true;
|
|
72
|
+
case "mutual-failure":
|
|
73
|
+
if (denyMatches) return false;
|
|
74
|
+
if (allowMatches) return true;
|
|
75
|
+
return false;
|
|
76
|
+
default:
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function createAccessControlMiddleware(config) {
|
|
81
|
+
return async (c, next) => {
|
|
82
|
+
const allowed = evaluateAccessControl(config, c);
|
|
83
|
+
if (!allowed) {
|
|
84
|
+
return c.text("Forbidden", 403);
|
|
85
|
+
}
|
|
86
|
+
await next();
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
createAccessControlMiddleware
|
|
91
|
+
};
|