@tknf/matchbox 0.2.3 → 0.2.4
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.js +136 -5
- package/dist/html.js +3 -2
- package/dist/index.js +438 -1
- package/dist/plugin.js +4 -3
- package/dist/with-defaults.js +366 -3
- package/package.json +1 -1
package/dist/cgi.js
CHANGED
|
@@ -1,12 +1,143 @@
|
|
|
1
|
+
// src/cgi.ts
|
|
1
2
|
import { Hono } from "hono";
|
|
2
3
|
import { basicAuth } from "hono/basic-auth";
|
|
3
4
|
import { getCookie, setCookie } from "hono/cookie";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "@tknf/matchbox",
|
|
9
|
+
version: "0.2.4",
|
|
10
|
+
description: "A Simple Web Server Framework",
|
|
11
|
+
keywords: [
|
|
12
|
+
"cgi",
|
|
13
|
+
"framework",
|
|
14
|
+
"hono",
|
|
15
|
+
"server",
|
|
16
|
+
"web"
|
|
17
|
+
],
|
|
18
|
+
license: "MIT",
|
|
19
|
+
author: "tknf <dev@tknf.net>",
|
|
20
|
+
repository: {
|
|
21
|
+
url: "https://github.com/tknf/matchbox.git"
|
|
22
|
+
},
|
|
23
|
+
files: [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
type: "module",
|
|
27
|
+
module: "dist/index.js",
|
|
28
|
+
types: "dist/index.d.ts",
|
|
29
|
+
exports: {
|
|
30
|
+
".": {
|
|
31
|
+
import: {
|
|
32
|
+
types: "./dist/index.d.ts",
|
|
33
|
+
import: "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"./plugin": {
|
|
37
|
+
import: {
|
|
38
|
+
types: "./dist/plugin.d.ts",
|
|
39
|
+
import: "./dist/plugin.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
publishConfig: {
|
|
44
|
+
access: "public",
|
|
45
|
+
registry: "https://registry.npmjs.org/"
|
|
46
|
+
},
|
|
47
|
+
scripts: {
|
|
48
|
+
test: "vitest run",
|
|
49
|
+
"test:coverage": "vitest run --coverage",
|
|
50
|
+
"lint:check": "oxlint",
|
|
51
|
+
"lint:fix": "oxlint --fix",
|
|
52
|
+
"format:check": "oxfmt --check",
|
|
53
|
+
"format:write": "oxfmt",
|
|
54
|
+
watch: "tsup --watch",
|
|
55
|
+
build: "tsup"
|
|
56
|
+
},
|
|
57
|
+
dependencies: {
|
|
58
|
+
glob: "^13.0.0"
|
|
59
|
+
},
|
|
60
|
+
devDependencies: {
|
|
61
|
+
"@types/node": "^25.0.3",
|
|
62
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
63
|
+
hono: "^4.11.1",
|
|
64
|
+
oxfmt: "^0.20.0",
|
|
65
|
+
oxlint: "^1.35.0",
|
|
66
|
+
tsup: "^8.5.1",
|
|
67
|
+
typescript: "^5.9.3",
|
|
68
|
+
vite: "^7.3.0",
|
|
69
|
+
vitest: "^4.0.16"
|
|
70
|
+
},
|
|
71
|
+
peerDependencies: {
|
|
72
|
+
hono: "*"
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/html.ts
|
|
77
|
+
import { html } from "hono/html";
|
|
78
|
+
var generateCgiInfo = ({
|
|
79
|
+
$_SERVER,
|
|
80
|
+
$_SESSION,
|
|
81
|
+
$_REQUEST,
|
|
82
|
+
config
|
|
83
|
+
}) => {
|
|
84
|
+
return () => {
|
|
85
|
+
const infoSection = ({ title, data }) => {
|
|
86
|
+
return html`
|
|
87
|
+
<div style="margin-bottom: 20px; width: 100%; max-width: 900px;">
|
|
88
|
+
<h2 style="background: #ccccff; color: #000; padding: 5px 10px; margin: 0; font-size: 1.2em; border: 1px solid #000; font-family: 'MS PGothic', sans-serif;">${title}</h2>
|
|
89
|
+
<table style="width: 100%; border-collapse: collapse; border: 1px solid #000; table-layout: fixed; font-family: 'MS PGothic', sans-serif;">
|
|
90
|
+
${Object.entries(data).map(
|
|
91
|
+
([key, val], i) => html`
|
|
92
|
+
<tr style="background: ${i % 2 === 0 ? "#f0f0ff" : "#ffffff"}">
|
|
93
|
+
<td style="padding: 3px 10px; border: 1px solid #000; font-weight: bold; width: 30%; word-break: break-all;">${key}</td>
|
|
94
|
+
<td style="padding: 3px 10px; border: 1px solid #000; width: 70%; word-break: break-all;">
|
|
95
|
+
${typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)}
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
`
|
|
99
|
+
)}
|
|
100
|
+
</table>
|
|
101
|
+
</div>
|
|
102
|
+
`;
|
|
103
|
+
};
|
|
104
|
+
return html`
|
|
105
|
+
<div style="background: #ffffff; color: #333333; font-family: 'MS PGothic', sans-serif; padding: 20px; display: flex; flex-direction: column; align-items: center;">
|
|
106
|
+
<h1 style="color: #000000; border-bottom: 3px double #000000; padding-bottom: 10px; margin-bottom: 30px;">Matchbox CGI Version Information</h1>
|
|
107
|
+
<div style="width: 100%; max-width: 900px; padding: 15px; background: #ffffcc; border: 1px dashed #000000; margin-bottom: 20px; text-align: center;">
|
|
108
|
+
<strong>Server Software:</strong> Matchbox Engine on ${typeof process !== "undefined" ? "Node.js/Bun" : "Edge"}
|
|
109
|
+
</div>
|
|
110
|
+
${infoSection({ title: "$_SERVER (Environment)", data: $_SERVER })}
|
|
111
|
+
${infoSection({ title: "$_SESSION", data: $_SESSION })}
|
|
112
|
+
${infoSection({ title: "$_REQUEST", data: $_REQUEST })}
|
|
113
|
+
${infoSection({ title: "Site Configuration", data: config })}
|
|
114
|
+
<div style="margin-top: 40px; font-size: 0.9em; font-style: italic; border-top: 1px solid #ccc; width: 100%; max-width: 900px; text-align: right; padding-top: 10px;">
|
|
115
|
+
Generated by Matchbox Framework - Ignition for Hono
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
`;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
var generateCgiError = ({
|
|
122
|
+
error,
|
|
123
|
+
$_SERVER
|
|
124
|
+
}) => {
|
|
125
|
+
return html`
|
|
126
|
+
<div style="padding:2rem; background:#fffafa; border:5px double #cc0000; font-family: 'MS PGothic', sans-serif;">
|
|
127
|
+
<h1 style="color:#cc0000; border-bottom: 2px solid #cc0000; padding-bottom: 5px;">Matchbox: Runtime Exception</h1>
|
|
128
|
+
<p><strong>Fatal Error:</strong> ${error.message}</p>
|
|
129
|
+
<pre style="background:#f0f0f0; padding:1rem; border:1px inset #ccc; overflow: auto;">${error.stack}</pre>
|
|
130
|
+
<hr style="border: 0; border-top: 1px solid #cc0000;" />
|
|
131
|
+
<div style="text-align: right; font-size: 0.8em;">Matchbox CGI Engine Server at ${$_SERVER.REMOTE_ADDR}</div>
|
|
132
|
+
</div>
|
|
133
|
+
`;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/cgi.ts
|
|
137
|
+
var isRedirectObject = (obj) => {
|
|
7
138
|
return obj && obj.__type === "redirect" && typeof obj.url === "string";
|
|
8
139
|
};
|
|
9
|
-
|
|
140
|
+
var createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
|
|
10
141
|
const app = new Hono();
|
|
11
142
|
const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
|
|
12
143
|
if (options.middleware && options.middleware.length > 0) {
|
|
@@ -170,7 +301,7 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
|
|
|
170
301
|
}
|
|
171
302
|
},
|
|
172
303
|
get_version: () => {
|
|
173
|
-
return `MatchboxCGI/v${
|
|
304
|
+
return `MatchboxCGI/v${package_default.version}`;
|
|
174
305
|
},
|
|
175
306
|
/**
|
|
176
307
|
* Returns information about all loaded CGI modules
|
package/dist/html.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
// src/html.ts
|
|
1
2
|
import { html } from "hono/html";
|
|
2
|
-
|
|
3
|
+
var generateCgiInfo = ({
|
|
3
4
|
$_SERVER,
|
|
4
5
|
$_SESSION,
|
|
5
6
|
$_REQUEST,
|
|
@@ -42,7 +43,7 @@ const generateCgiInfo = ({
|
|
|
42
43
|
`;
|
|
43
44
|
};
|
|
44
45
|
};
|
|
45
|
-
|
|
46
|
+
var generateCgiError = ({
|
|
46
47
|
error,
|
|
47
48
|
$_SERVER
|
|
48
49
|
}) => {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,441 @@
|
|
|
1
|
-
|
|
1
|
+
// src/cgi.ts
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { basicAuth } from "hono/basic-auth";
|
|
4
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "@tknf/matchbox",
|
|
9
|
+
version: "0.2.4",
|
|
10
|
+
description: "A Simple Web Server Framework",
|
|
11
|
+
keywords: [
|
|
12
|
+
"cgi",
|
|
13
|
+
"framework",
|
|
14
|
+
"hono",
|
|
15
|
+
"server",
|
|
16
|
+
"web"
|
|
17
|
+
],
|
|
18
|
+
license: "MIT",
|
|
19
|
+
author: "tknf <dev@tknf.net>",
|
|
20
|
+
repository: {
|
|
21
|
+
url: "https://github.com/tknf/matchbox.git"
|
|
22
|
+
},
|
|
23
|
+
files: [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
type: "module",
|
|
27
|
+
module: "dist/index.js",
|
|
28
|
+
types: "dist/index.d.ts",
|
|
29
|
+
exports: {
|
|
30
|
+
".": {
|
|
31
|
+
import: {
|
|
32
|
+
types: "./dist/index.d.ts",
|
|
33
|
+
import: "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"./plugin": {
|
|
37
|
+
import: {
|
|
38
|
+
types: "./dist/plugin.d.ts",
|
|
39
|
+
import: "./dist/plugin.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
publishConfig: {
|
|
44
|
+
access: "public",
|
|
45
|
+
registry: "https://registry.npmjs.org/"
|
|
46
|
+
},
|
|
47
|
+
scripts: {
|
|
48
|
+
test: "vitest run",
|
|
49
|
+
"test:coverage": "vitest run --coverage",
|
|
50
|
+
"lint:check": "oxlint",
|
|
51
|
+
"lint:fix": "oxlint --fix",
|
|
52
|
+
"format:check": "oxfmt --check",
|
|
53
|
+
"format:write": "oxfmt",
|
|
54
|
+
watch: "tsup --watch",
|
|
55
|
+
build: "tsup"
|
|
56
|
+
},
|
|
57
|
+
dependencies: {
|
|
58
|
+
glob: "^13.0.0"
|
|
59
|
+
},
|
|
60
|
+
devDependencies: {
|
|
61
|
+
"@types/node": "^25.0.3",
|
|
62
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
63
|
+
hono: "^4.11.1",
|
|
64
|
+
oxfmt: "^0.20.0",
|
|
65
|
+
oxlint: "^1.35.0",
|
|
66
|
+
tsup: "^8.5.1",
|
|
67
|
+
typescript: "^5.9.3",
|
|
68
|
+
vite: "^7.3.0",
|
|
69
|
+
vitest: "^4.0.16"
|
|
70
|
+
},
|
|
71
|
+
peerDependencies: {
|
|
72
|
+
hono: "*"
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/html.ts
|
|
77
|
+
import { html } from "hono/html";
|
|
78
|
+
var generateCgiInfo = ({
|
|
79
|
+
$_SERVER,
|
|
80
|
+
$_SESSION,
|
|
81
|
+
$_REQUEST,
|
|
82
|
+
config
|
|
83
|
+
}) => {
|
|
84
|
+
return () => {
|
|
85
|
+
const infoSection = ({ title, data }) => {
|
|
86
|
+
return html`
|
|
87
|
+
<div style="margin-bottom: 20px; width: 100%; max-width: 900px;">
|
|
88
|
+
<h2 style="background: #ccccff; color: #000; padding: 5px 10px; margin: 0; font-size: 1.2em; border: 1px solid #000; font-family: 'MS PGothic', sans-serif;">${title}</h2>
|
|
89
|
+
<table style="width: 100%; border-collapse: collapse; border: 1px solid #000; table-layout: fixed; font-family: 'MS PGothic', sans-serif;">
|
|
90
|
+
${Object.entries(data).map(
|
|
91
|
+
([key, val], i) => html`
|
|
92
|
+
<tr style="background: ${i % 2 === 0 ? "#f0f0ff" : "#ffffff"}">
|
|
93
|
+
<td style="padding: 3px 10px; border: 1px solid #000; font-weight: bold; width: 30%; word-break: break-all;">${key}</td>
|
|
94
|
+
<td style="padding: 3px 10px; border: 1px solid #000; width: 70%; word-break: break-all;">
|
|
95
|
+
${typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)}
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
`
|
|
99
|
+
)}
|
|
100
|
+
</table>
|
|
101
|
+
</div>
|
|
102
|
+
`;
|
|
103
|
+
};
|
|
104
|
+
return html`
|
|
105
|
+
<div style="background: #ffffff; color: #333333; font-family: 'MS PGothic', sans-serif; padding: 20px; display: flex; flex-direction: column; align-items: center;">
|
|
106
|
+
<h1 style="color: #000000; border-bottom: 3px double #000000; padding-bottom: 10px; margin-bottom: 30px;">Matchbox CGI Version Information</h1>
|
|
107
|
+
<div style="width: 100%; max-width: 900px; padding: 15px; background: #ffffcc; border: 1px dashed #000000; margin-bottom: 20px; text-align: center;">
|
|
108
|
+
<strong>Server Software:</strong> Matchbox Engine on ${typeof process !== "undefined" ? "Node.js/Bun" : "Edge"}
|
|
109
|
+
</div>
|
|
110
|
+
${infoSection({ title: "$_SERVER (Environment)", data: $_SERVER })}
|
|
111
|
+
${infoSection({ title: "$_SESSION", data: $_SESSION })}
|
|
112
|
+
${infoSection({ title: "$_REQUEST", data: $_REQUEST })}
|
|
113
|
+
${infoSection({ title: "Site Configuration", data: config })}
|
|
114
|
+
<div style="margin-top: 40px; font-size: 0.9em; font-style: italic; border-top: 1px solid #ccc; width: 100%; max-width: 900px; text-align: right; padding-top: 10px;">
|
|
115
|
+
Generated by Matchbox Framework - Ignition for Hono
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
`;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
var generateCgiError = ({
|
|
122
|
+
error,
|
|
123
|
+
$_SERVER
|
|
124
|
+
}) => {
|
|
125
|
+
return html`
|
|
126
|
+
<div style="padding:2rem; background:#fffafa; border:5px double #cc0000; font-family: 'MS PGothic', sans-serif;">
|
|
127
|
+
<h1 style="color:#cc0000; border-bottom: 2px solid #cc0000; padding-bottom: 5px;">Matchbox: Runtime Exception</h1>
|
|
128
|
+
<p><strong>Fatal Error:</strong> ${error.message}</p>
|
|
129
|
+
<pre style="background:#f0f0f0; padding:1rem; border:1px inset #ccc; overflow: auto;">${error.stack}</pre>
|
|
130
|
+
<hr style="border: 0; border-top: 1px solid #cc0000;" />
|
|
131
|
+
<div style="text-align: right; font-size: 0.8em;">Matchbox CGI Engine Server at ${$_SERVER.REMOTE_ADDR}</div>
|
|
132
|
+
</div>
|
|
133
|
+
`;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/cgi.ts
|
|
137
|
+
var isRedirectObject = (obj) => {
|
|
138
|
+
return obj && obj.__type === "redirect" && typeof obj.url === "string";
|
|
139
|
+
};
|
|
140
|
+
var createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
|
|
141
|
+
const app = new Hono();
|
|
142
|
+
const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
|
|
143
|
+
if (options.middleware && options.middleware.length > 0) {
|
|
144
|
+
for (const mw of options.middleware) {
|
|
145
|
+
app.use("*", async (c, next) => {
|
|
146
|
+
const result = await mw(c, next);
|
|
147
|
+
if (result instanceof Response) {
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const protectedFiles = [".htaccess", ".htpasswd", ".htdigest", ".htgroup"];
|
|
154
|
+
app.use("*", async (c, next) => {
|
|
155
|
+
const path = c.req.path;
|
|
156
|
+
const lastSegment = path.slice(path.lastIndexOf("/") + 1);
|
|
157
|
+
if (protectedFiles.some((file) => lastSegment === file)) {
|
|
158
|
+
return c.text("Forbidden", 403);
|
|
159
|
+
}
|
|
160
|
+
await next();
|
|
161
|
+
});
|
|
162
|
+
if (options.enforceTrailingSlash) {
|
|
163
|
+
app.use("*", async (c, next) => {
|
|
164
|
+
const path = c.req.path;
|
|
165
|
+
if (!path.endsWith("/") && !path.includes(".")) {
|
|
166
|
+
return c.redirect(`${path}/`, 301);
|
|
167
|
+
}
|
|
168
|
+
await next();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
Object.entries(rewriteMap).forEach(([dir, rules]) => {
|
|
172
|
+
const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
|
|
173
|
+
app.use(`${basePath}/*`, async (c, next) => {
|
|
174
|
+
const relPath = c.req.path.replace(basePath, "") || "/";
|
|
175
|
+
for (const rule of rules) {
|
|
176
|
+
if (rule.type === "redirect") {
|
|
177
|
+
if (relPath === rule.source) {
|
|
178
|
+
return c.redirect(
|
|
179
|
+
rule.target,
|
|
180
|
+
Number.parseInt(rule.code, 10) || 302
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
} else if (rule.type === "rewrite") {
|
|
184
|
+
const regex = new RegExp(rule.pattern);
|
|
185
|
+
if (regex.test(relPath)) {
|
|
186
|
+
const target = rule.target.startsWith("/") ? rule.target : `${basePath}/${rule.target}`;
|
|
187
|
+
if (rule.flags.includes("R")) {
|
|
188
|
+
const code = rule.flags.match(/R=(\d+)/)?.[1] || "302";
|
|
189
|
+
return c.redirect(target, Number.parseInt(code, 10));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await next();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
Object.entries(authMap).forEach(([dir, content]) => {
|
|
198
|
+
const credentials = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
|
|
199
|
+
const [username, password] = line.split(":");
|
|
200
|
+
return { username, password };
|
|
201
|
+
});
|
|
202
|
+
if (credentials.length > 0) {
|
|
203
|
+
const authPath = dir === "/" ? "*" : `${dir.replace(/\/$/, "")}/*`;
|
|
204
|
+
app.use(authPath, async (c, next) => {
|
|
205
|
+
const handler = basicAuth({
|
|
206
|
+
verifyUser: (u, p) => credentials.some((cred) => cred.username === u && cred.password === p),
|
|
207
|
+
realm: "Restricted Area"
|
|
208
|
+
});
|
|
209
|
+
return handler(c, next);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
pages.forEach(({ urlPath, dirPath, component }) => {
|
|
214
|
+
const routes = [urlPath, `${urlPath}/*`];
|
|
215
|
+
if (dirPath) {
|
|
216
|
+
routes.push(dirPath);
|
|
217
|
+
if (dirPath !== "/") {
|
|
218
|
+
routes.push(`${dirPath}/*`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
routes.forEach((route) => {
|
|
222
|
+
app.all(route, async (c) => {
|
|
223
|
+
const $_GET = c.req.query();
|
|
224
|
+
const body = await c.req.parseBody({ all: true }).catch(() => ({}));
|
|
225
|
+
const $_POST = {};
|
|
226
|
+
const $_FILES = {};
|
|
227
|
+
for (const [key, value] of Object.entries(body)) {
|
|
228
|
+
if (value instanceof File || Array.isArray(value) && value[0] instanceof File) {
|
|
229
|
+
$_FILES[key] = value;
|
|
230
|
+
} else {
|
|
231
|
+
$_POST[key] = value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const $_COOKIE = getCookie(c);
|
|
235
|
+
const $_REQUEST = {
|
|
236
|
+
...$_COOKIE,
|
|
237
|
+
...$_GET,
|
|
238
|
+
...$_POST
|
|
239
|
+
};
|
|
240
|
+
const $_ENV = typeof process !== "undefined" && process.env ? process.env : c.env || {};
|
|
241
|
+
let $_SESSION = {};
|
|
242
|
+
const sRaw = getCookie(c, SESS_KEY);
|
|
243
|
+
if (sRaw) {
|
|
244
|
+
try {
|
|
245
|
+
$_SESSION = JSON.parse(decodeURIComponent(sRaw));
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
let responseStatus = 200;
|
|
250
|
+
const responseHeaders = {
|
|
251
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
252
|
+
};
|
|
253
|
+
const $_SERVER = {
|
|
254
|
+
...$_ENV,
|
|
255
|
+
REQUEST_METHOD: c.req.method,
|
|
256
|
+
REQUEST_URI: c.req.url,
|
|
257
|
+
REMOTE_ADDR: c.req.header("x-forwarded-for") || "127.0.0.1",
|
|
258
|
+
USER_AGENT: c.req.header("user-agent") || "",
|
|
259
|
+
SCRIPT_NAME: urlPath,
|
|
260
|
+
PATH_INFO: c.req.path.replace(urlPath, "") || "/",
|
|
261
|
+
QUERY_STRING: new URL(c.req.url).search.slice(1)
|
|
262
|
+
};
|
|
263
|
+
const cgiinfo = generateCgiInfo({
|
|
264
|
+
$_SERVER,
|
|
265
|
+
$_REQUEST,
|
|
266
|
+
$_SESSION,
|
|
267
|
+
config: siteConfig
|
|
268
|
+
});
|
|
269
|
+
const context = {
|
|
270
|
+
$_GET,
|
|
271
|
+
$_POST,
|
|
272
|
+
$_FILES,
|
|
273
|
+
$_REQUEST,
|
|
274
|
+
$_COOKIE,
|
|
275
|
+
$_ENV,
|
|
276
|
+
$_SERVER,
|
|
277
|
+
$_SESSION,
|
|
278
|
+
config: siteConfig,
|
|
279
|
+
c,
|
|
280
|
+
header: (name, value) => {
|
|
281
|
+
responseHeaders[name.toLocaleLowerCase()] = value;
|
|
282
|
+
},
|
|
283
|
+
status: (code) => {
|
|
284
|
+
responseStatus = code;
|
|
285
|
+
},
|
|
286
|
+
redirect: (url, status = 302) => {
|
|
287
|
+
return { __type: "redirect", url, status };
|
|
288
|
+
},
|
|
289
|
+
cgiinfo,
|
|
290
|
+
request_headers: () => {
|
|
291
|
+
return Object.fromEntries(c.req.raw.headers.entries());
|
|
292
|
+
},
|
|
293
|
+
response_headers: () => {
|
|
294
|
+
return responseHeaders;
|
|
295
|
+
},
|
|
296
|
+
log: (message) => {
|
|
297
|
+
if (options.logger) {
|
|
298
|
+
options.logger(message, "info");
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`[CGI LOG] ${message}`);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
get_version: () => {
|
|
304
|
+
return `MatchboxCGI/v${package_default.version}`;
|
|
305
|
+
},
|
|
306
|
+
/**
|
|
307
|
+
* Returns information about all loaded CGI modules
|
|
308
|
+
* @returns Array of module information containing urlPath and dirPath
|
|
309
|
+
*/
|
|
310
|
+
get_modules: () => {
|
|
311
|
+
return pages.map((page) => ({
|
|
312
|
+
urlPath: page.urlPath,
|
|
313
|
+
dirPath: page.dirPath
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
const result = await component(context);
|
|
319
|
+
const sessionValue = encodeURIComponent(JSON.stringify($_SESSION));
|
|
320
|
+
const sessionOptions = {
|
|
321
|
+
path: options.sessionCookie?.path || "/",
|
|
322
|
+
httpOnly: true,
|
|
323
|
+
sameSite: options.sessionCookie?.sameSite || "Lax"
|
|
324
|
+
};
|
|
325
|
+
if (options.sessionCookie?.secure !== void 0) {
|
|
326
|
+
sessionOptions.secure = options.sessionCookie.secure;
|
|
327
|
+
}
|
|
328
|
+
if (options.sessionCookie?.domain) {
|
|
329
|
+
sessionOptions.domain = options.sessionCookie.domain;
|
|
330
|
+
}
|
|
331
|
+
if (options.sessionCookie?.maxAge) {
|
|
332
|
+
sessionOptions.maxAge = options.sessionCookie.maxAge;
|
|
333
|
+
}
|
|
334
|
+
if (isRedirectObject(result)) {
|
|
335
|
+
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
336
|
+
return c.redirect(result.url, result.status);
|
|
337
|
+
}
|
|
338
|
+
if (result instanceof Response) {
|
|
339
|
+
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
343
|
+
Object.entries(responseHeaders).forEach(([key, value]) => {
|
|
344
|
+
c.header(key, value);
|
|
345
|
+
});
|
|
346
|
+
const contentType = responseHeaders["content-type"];
|
|
347
|
+
if (contentType?.includes("application/json")) {
|
|
348
|
+
return c.json(
|
|
349
|
+
// biome-ignore lint/suspicious/noExplicitAny: to JSON response
|
|
350
|
+
result ?? { success: true },
|
|
351
|
+
responseStatus
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return c.html(result, responseStatus);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return c.html(generateCgiError({ error, $_SERVER }), 500);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
return app;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// src/with-defaults.ts
|
|
365
|
+
var loadPagesFromPublic = () => {
|
|
366
|
+
const modules = import.meta.glob("/public/**/*.cgi.{tsx,jsx}", {
|
|
367
|
+
eager: true
|
|
368
|
+
});
|
|
369
|
+
const htpasswds = import.meta.glob("/public/**/.htpasswd", {
|
|
370
|
+
eager: true,
|
|
371
|
+
query: "?raw",
|
|
372
|
+
import: "default"
|
|
373
|
+
});
|
|
374
|
+
const htaccessFiles = import.meta.glob("/public/**/.htaccess", {
|
|
375
|
+
eager: true,
|
|
376
|
+
query: "?raw",
|
|
377
|
+
import: "default"
|
|
378
|
+
});
|
|
379
|
+
void import.meta.glob("/public/**/.htdigest", {
|
|
380
|
+
eager: true,
|
|
381
|
+
query: "?raw",
|
|
382
|
+
import: "default"
|
|
383
|
+
});
|
|
384
|
+
void import.meta.glob("/public/**/.htgroup", {
|
|
385
|
+
eager: true,
|
|
386
|
+
query: "?raw",
|
|
387
|
+
import: "default"
|
|
388
|
+
});
|
|
389
|
+
const basePathRegex = /^\/public/;
|
|
390
|
+
const pages = Object.keys(modules).map((key) => {
|
|
391
|
+
const urlPath = key.replace(basePathRegex, "").replace(/.tsx$/, "").replace(/.jsx$/, "");
|
|
392
|
+
const isIndex = urlPath.endsWith("/index.cgi") || urlPath === "/index.cgi";
|
|
393
|
+
const dirPath = isIndex ? urlPath.replace(/\/index\.cgi$/, "/") : null;
|
|
394
|
+
return { urlPath, dirPath, component: modules[key].default };
|
|
395
|
+
});
|
|
396
|
+
const authMap = Object.keys(htpasswds).reduce(
|
|
397
|
+
(acc, key) => {
|
|
398
|
+
const dir = key.replace(basePathRegex, "").replace(/\.htpasswd$/, "") || "/";
|
|
399
|
+
acc[dir] = htpasswds[key];
|
|
400
|
+
return acc;
|
|
401
|
+
},
|
|
402
|
+
{}
|
|
403
|
+
);
|
|
404
|
+
const rewriteMap = Object.keys(htaccessFiles).reduce((acc, key) => {
|
|
405
|
+
const dir = key.replace(basePathRegex, "").replace(/\.htaccess$/, "") || "/";
|
|
406
|
+
const lines = htaccessFiles[key].split("\n");
|
|
407
|
+
const rules = lines.map((line) => {
|
|
408
|
+
const l = line.trim();
|
|
409
|
+
if (!l || l.startsWith("#")) return null;
|
|
410
|
+
const parts = l.split(/\s+/);
|
|
411
|
+
if (parts[0] === "RewriteRule") {
|
|
412
|
+
return {
|
|
413
|
+
type: "rewrite",
|
|
414
|
+
pattern: parts[1],
|
|
415
|
+
target: parts[2],
|
|
416
|
+
flags: parts[3] || ""
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (parts[0] === "Redirect") {
|
|
420
|
+
return {
|
|
421
|
+
type: "redirect",
|
|
422
|
+
code: parts[1],
|
|
423
|
+
source: parts[2],
|
|
424
|
+
target: parts[3]
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}).filter(Boolean);
|
|
429
|
+
acc[dir] = rules;
|
|
430
|
+
return acc;
|
|
431
|
+
}, {});
|
|
432
|
+
return { pages, authMap, rewriteMap };
|
|
433
|
+
};
|
|
434
|
+
var createCgi = (options) => {
|
|
435
|
+
const resolvedConfig = typeof __MATCHBOX_CONFIG__ === "undefined" ? {} : __MATCHBOX_CONFIG__;
|
|
436
|
+
const { pages, authMap, rewriteMap } = loadPagesFromPublic();
|
|
437
|
+
return createCgiWithPages(pages, resolvedConfig, authMap, rewriteMap, options);
|
|
438
|
+
};
|
|
2
439
|
export {
|
|
3
440
|
createCgi
|
|
4
441
|
};
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var MatchboxPlugin = (options = {}) => {
|
|
4
5
|
let viteConfig;
|
|
5
6
|
const siteConfig = options.config || {};
|
|
6
7
|
const publicDir = options.publicDir || "public";
|
package/dist/with-defaults.js
CHANGED
|
@@ -1,5 +1,368 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// src/cgi.ts
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { basicAuth } from "hono/basic-auth";
|
|
4
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "@tknf/matchbox",
|
|
9
|
+
version: "0.2.4",
|
|
10
|
+
description: "A Simple Web Server Framework",
|
|
11
|
+
keywords: [
|
|
12
|
+
"cgi",
|
|
13
|
+
"framework",
|
|
14
|
+
"hono",
|
|
15
|
+
"server",
|
|
16
|
+
"web"
|
|
17
|
+
],
|
|
18
|
+
license: "MIT",
|
|
19
|
+
author: "tknf <dev@tknf.net>",
|
|
20
|
+
repository: {
|
|
21
|
+
url: "https://github.com/tknf/matchbox.git"
|
|
22
|
+
},
|
|
23
|
+
files: [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
type: "module",
|
|
27
|
+
module: "dist/index.js",
|
|
28
|
+
types: "dist/index.d.ts",
|
|
29
|
+
exports: {
|
|
30
|
+
".": {
|
|
31
|
+
import: {
|
|
32
|
+
types: "./dist/index.d.ts",
|
|
33
|
+
import: "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"./plugin": {
|
|
37
|
+
import: {
|
|
38
|
+
types: "./dist/plugin.d.ts",
|
|
39
|
+
import: "./dist/plugin.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
publishConfig: {
|
|
44
|
+
access: "public",
|
|
45
|
+
registry: "https://registry.npmjs.org/"
|
|
46
|
+
},
|
|
47
|
+
scripts: {
|
|
48
|
+
test: "vitest run",
|
|
49
|
+
"test:coverage": "vitest run --coverage",
|
|
50
|
+
"lint:check": "oxlint",
|
|
51
|
+
"lint:fix": "oxlint --fix",
|
|
52
|
+
"format:check": "oxfmt --check",
|
|
53
|
+
"format:write": "oxfmt",
|
|
54
|
+
watch: "tsup --watch",
|
|
55
|
+
build: "tsup"
|
|
56
|
+
},
|
|
57
|
+
dependencies: {
|
|
58
|
+
glob: "^13.0.0"
|
|
59
|
+
},
|
|
60
|
+
devDependencies: {
|
|
61
|
+
"@types/node": "^25.0.3",
|
|
62
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
63
|
+
hono: "^4.11.1",
|
|
64
|
+
oxfmt: "^0.20.0",
|
|
65
|
+
oxlint: "^1.35.0",
|
|
66
|
+
tsup: "^8.5.1",
|
|
67
|
+
typescript: "^5.9.3",
|
|
68
|
+
vite: "^7.3.0",
|
|
69
|
+
vitest: "^4.0.16"
|
|
70
|
+
},
|
|
71
|
+
peerDependencies: {
|
|
72
|
+
hono: "*"
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/html.ts
|
|
77
|
+
import { html } from "hono/html";
|
|
78
|
+
var generateCgiInfo = ({
|
|
79
|
+
$_SERVER,
|
|
80
|
+
$_SESSION,
|
|
81
|
+
$_REQUEST,
|
|
82
|
+
config
|
|
83
|
+
}) => {
|
|
84
|
+
return () => {
|
|
85
|
+
const infoSection = ({ title, data }) => {
|
|
86
|
+
return html`
|
|
87
|
+
<div style="margin-bottom: 20px; width: 100%; max-width: 900px;">
|
|
88
|
+
<h2 style="background: #ccccff; color: #000; padding: 5px 10px; margin: 0; font-size: 1.2em; border: 1px solid #000; font-family: 'MS PGothic', sans-serif;">${title}</h2>
|
|
89
|
+
<table style="width: 100%; border-collapse: collapse; border: 1px solid #000; table-layout: fixed; font-family: 'MS PGothic', sans-serif;">
|
|
90
|
+
${Object.entries(data).map(
|
|
91
|
+
([key, val], i) => html`
|
|
92
|
+
<tr style="background: ${i % 2 === 0 ? "#f0f0ff" : "#ffffff"}">
|
|
93
|
+
<td style="padding: 3px 10px; border: 1px solid #000; font-weight: bold; width: 30%; word-break: break-all;">${key}</td>
|
|
94
|
+
<td style="padding: 3px 10px; border: 1px solid #000; width: 70%; word-break: break-all;">
|
|
95
|
+
${typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)}
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
`
|
|
99
|
+
)}
|
|
100
|
+
</table>
|
|
101
|
+
</div>
|
|
102
|
+
`;
|
|
103
|
+
};
|
|
104
|
+
return html`
|
|
105
|
+
<div style="background: #ffffff; color: #333333; font-family: 'MS PGothic', sans-serif; padding: 20px; display: flex; flex-direction: column; align-items: center;">
|
|
106
|
+
<h1 style="color: #000000; border-bottom: 3px double #000000; padding-bottom: 10px; margin-bottom: 30px;">Matchbox CGI Version Information</h1>
|
|
107
|
+
<div style="width: 100%; max-width: 900px; padding: 15px; background: #ffffcc; border: 1px dashed #000000; margin-bottom: 20px; text-align: center;">
|
|
108
|
+
<strong>Server Software:</strong> Matchbox Engine on ${typeof process !== "undefined" ? "Node.js/Bun" : "Edge"}
|
|
109
|
+
</div>
|
|
110
|
+
${infoSection({ title: "$_SERVER (Environment)", data: $_SERVER })}
|
|
111
|
+
${infoSection({ title: "$_SESSION", data: $_SESSION })}
|
|
112
|
+
${infoSection({ title: "$_REQUEST", data: $_REQUEST })}
|
|
113
|
+
${infoSection({ title: "Site Configuration", data: config })}
|
|
114
|
+
<div style="margin-top: 40px; font-size: 0.9em; font-style: italic; border-top: 1px solid #ccc; width: 100%; max-width: 900px; text-align: right; padding-top: 10px;">
|
|
115
|
+
Generated by Matchbox Framework - Ignition for Hono
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
`;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
var generateCgiError = ({
|
|
122
|
+
error,
|
|
123
|
+
$_SERVER
|
|
124
|
+
}) => {
|
|
125
|
+
return html`
|
|
126
|
+
<div style="padding:2rem; background:#fffafa; border:5px double #cc0000; font-family: 'MS PGothic', sans-serif;">
|
|
127
|
+
<h1 style="color:#cc0000; border-bottom: 2px solid #cc0000; padding-bottom: 5px;">Matchbox: Runtime Exception</h1>
|
|
128
|
+
<p><strong>Fatal Error:</strong> ${error.message}</p>
|
|
129
|
+
<pre style="background:#f0f0f0; padding:1rem; border:1px inset #ccc; overflow: auto;">${error.stack}</pre>
|
|
130
|
+
<hr style="border: 0; border-top: 1px solid #cc0000;" />
|
|
131
|
+
<div style="text-align: right; font-size: 0.8em;">Matchbox CGI Engine Server at ${$_SERVER.REMOTE_ADDR}</div>
|
|
132
|
+
</div>
|
|
133
|
+
`;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/cgi.ts
|
|
137
|
+
var isRedirectObject = (obj) => {
|
|
138
|
+
return obj && obj.__type === "redirect" && typeof obj.url === "string";
|
|
139
|
+
};
|
|
140
|
+
var createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
|
|
141
|
+
const app = new Hono();
|
|
142
|
+
const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
|
|
143
|
+
if (options.middleware && options.middleware.length > 0) {
|
|
144
|
+
for (const mw of options.middleware) {
|
|
145
|
+
app.use("*", async (c, next) => {
|
|
146
|
+
const result = await mw(c, next);
|
|
147
|
+
if (result instanceof Response) {
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const protectedFiles = [".htaccess", ".htpasswd", ".htdigest", ".htgroup"];
|
|
154
|
+
app.use("*", async (c, next) => {
|
|
155
|
+
const path = c.req.path;
|
|
156
|
+
const lastSegment = path.slice(path.lastIndexOf("/") + 1);
|
|
157
|
+
if (protectedFiles.some((file) => lastSegment === file)) {
|
|
158
|
+
return c.text("Forbidden", 403);
|
|
159
|
+
}
|
|
160
|
+
await next();
|
|
161
|
+
});
|
|
162
|
+
if (options.enforceTrailingSlash) {
|
|
163
|
+
app.use("*", async (c, next) => {
|
|
164
|
+
const path = c.req.path;
|
|
165
|
+
if (!path.endsWith("/") && !path.includes(".")) {
|
|
166
|
+
return c.redirect(`${path}/`, 301);
|
|
167
|
+
}
|
|
168
|
+
await next();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
Object.entries(rewriteMap).forEach(([dir, rules]) => {
|
|
172
|
+
const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
|
|
173
|
+
app.use(`${basePath}/*`, async (c, next) => {
|
|
174
|
+
const relPath = c.req.path.replace(basePath, "") || "/";
|
|
175
|
+
for (const rule of rules) {
|
|
176
|
+
if (rule.type === "redirect") {
|
|
177
|
+
if (relPath === rule.source) {
|
|
178
|
+
return c.redirect(
|
|
179
|
+
rule.target,
|
|
180
|
+
Number.parseInt(rule.code, 10) || 302
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
} else if (rule.type === "rewrite") {
|
|
184
|
+
const regex = new RegExp(rule.pattern);
|
|
185
|
+
if (regex.test(relPath)) {
|
|
186
|
+
const target = rule.target.startsWith("/") ? rule.target : `${basePath}/${rule.target}`;
|
|
187
|
+
if (rule.flags.includes("R")) {
|
|
188
|
+
const code = rule.flags.match(/R=(\d+)/)?.[1] || "302";
|
|
189
|
+
return c.redirect(target, Number.parseInt(code, 10));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await next();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
Object.entries(authMap).forEach(([dir, content]) => {
|
|
198
|
+
const credentials = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
|
|
199
|
+
const [username, password] = line.split(":");
|
|
200
|
+
return { username, password };
|
|
201
|
+
});
|
|
202
|
+
if (credentials.length > 0) {
|
|
203
|
+
const authPath = dir === "/" ? "*" : `${dir.replace(/\/$/, "")}/*`;
|
|
204
|
+
app.use(authPath, async (c, next) => {
|
|
205
|
+
const handler = basicAuth({
|
|
206
|
+
verifyUser: (u, p) => credentials.some((cred) => cred.username === u && cred.password === p),
|
|
207
|
+
realm: "Restricted Area"
|
|
208
|
+
});
|
|
209
|
+
return handler(c, next);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
pages.forEach(({ urlPath, dirPath, component }) => {
|
|
214
|
+
const routes = [urlPath, `${urlPath}/*`];
|
|
215
|
+
if (dirPath) {
|
|
216
|
+
routes.push(dirPath);
|
|
217
|
+
if (dirPath !== "/") {
|
|
218
|
+
routes.push(`${dirPath}/*`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
routes.forEach((route) => {
|
|
222
|
+
app.all(route, async (c) => {
|
|
223
|
+
const $_GET = c.req.query();
|
|
224
|
+
const body = await c.req.parseBody({ all: true }).catch(() => ({}));
|
|
225
|
+
const $_POST = {};
|
|
226
|
+
const $_FILES = {};
|
|
227
|
+
for (const [key, value] of Object.entries(body)) {
|
|
228
|
+
if (value instanceof File || Array.isArray(value) && value[0] instanceof File) {
|
|
229
|
+
$_FILES[key] = value;
|
|
230
|
+
} else {
|
|
231
|
+
$_POST[key] = value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const $_COOKIE = getCookie(c);
|
|
235
|
+
const $_REQUEST = {
|
|
236
|
+
...$_COOKIE,
|
|
237
|
+
...$_GET,
|
|
238
|
+
...$_POST
|
|
239
|
+
};
|
|
240
|
+
const $_ENV = typeof process !== "undefined" && process.env ? process.env : c.env || {};
|
|
241
|
+
let $_SESSION = {};
|
|
242
|
+
const sRaw = getCookie(c, SESS_KEY);
|
|
243
|
+
if (sRaw) {
|
|
244
|
+
try {
|
|
245
|
+
$_SESSION = JSON.parse(decodeURIComponent(sRaw));
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
let responseStatus = 200;
|
|
250
|
+
const responseHeaders = {
|
|
251
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
252
|
+
};
|
|
253
|
+
const $_SERVER = {
|
|
254
|
+
...$_ENV,
|
|
255
|
+
REQUEST_METHOD: c.req.method,
|
|
256
|
+
REQUEST_URI: c.req.url,
|
|
257
|
+
REMOTE_ADDR: c.req.header("x-forwarded-for") || "127.0.0.1",
|
|
258
|
+
USER_AGENT: c.req.header("user-agent") || "",
|
|
259
|
+
SCRIPT_NAME: urlPath,
|
|
260
|
+
PATH_INFO: c.req.path.replace(urlPath, "") || "/",
|
|
261
|
+
QUERY_STRING: new URL(c.req.url).search.slice(1)
|
|
262
|
+
};
|
|
263
|
+
const cgiinfo = generateCgiInfo({
|
|
264
|
+
$_SERVER,
|
|
265
|
+
$_REQUEST,
|
|
266
|
+
$_SESSION,
|
|
267
|
+
config: siteConfig
|
|
268
|
+
});
|
|
269
|
+
const context = {
|
|
270
|
+
$_GET,
|
|
271
|
+
$_POST,
|
|
272
|
+
$_FILES,
|
|
273
|
+
$_REQUEST,
|
|
274
|
+
$_COOKIE,
|
|
275
|
+
$_ENV,
|
|
276
|
+
$_SERVER,
|
|
277
|
+
$_SESSION,
|
|
278
|
+
config: siteConfig,
|
|
279
|
+
c,
|
|
280
|
+
header: (name, value) => {
|
|
281
|
+
responseHeaders[name.toLocaleLowerCase()] = value;
|
|
282
|
+
},
|
|
283
|
+
status: (code) => {
|
|
284
|
+
responseStatus = code;
|
|
285
|
+
},
|
|
286
|
+
redirect: (url, status = 302) => {
|
|
287
|
+
return { __type: "redirect", url, status };
|
|
288
|
+
},
|
|
289
|
+
cgiinfo,
|
|
290
|
+
request_headers: () => {
|
|
291
|
+
return Object.fromEntries(c.req.raw.headers.entries());
|
|
292
|
+
},
|
|
293
|
+
response_headers: () => {
|
|
294
|
+
return responseHeaders;
|
|
295
|
+
},
|
|
296
|
+
log: (message) => {
|
|
297
|
+
if (options.logger) {
|
|
298
|
+
options.logger(message, "info");
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`[CGI LOG] ${message}`);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
get_version: () => {
|
|
304
|
+
return `MatchboxCGI/v${package_default.version}`;
|
|
305
|
+
},
|
|
306
|
+
/**
|
|
307
|
+
* Returns information about all loaded CGI modules
|
|
308
|
+
* @returns Array of module information containing urlPath and dirPath
|
|
309
|
+
*/
|
|
310
|
+
get_modules: () => {
|
|
311
|
+
return pages.map((page) => ({
|
|
312
|
+
urlPath: page.urlPath,
|
|
313
|
+
dirPath: page.dirPath
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
const result = await component(context);
|
|
319
|
+
const sessionValue = encodeURIComponent(JSON.stringify($_SESSION));
|
|
320
|
+
const sessionOptions = {
|
|
321
|
+
path: options.sessionCookie?.path || "/",
|
|
322
|
+
httpOnly: true,
|
|
323
|
+
sameSite: options.sessionCookie?.sameSite || "Lax"
|
|
324
|
+
};
|
|
325
|
+
if (options.sessionCookie?.secure !== void 0) {
|
|
326
|
+
sessionOptions.secure = options.sessionCookie.secure;
|
|
327
|
+
}
|
|
328
|
+
if (options.sessionCookie?.domain) {
|
|
329
|
+
sessionOptions.domain = options.sessionCookie.domain;
|
|
330
|
+
}
|
|
331
|
+
if (options.sessionCookie?.maxAge) {
|
|
332
|
+
sessionOptions.maxAge = options.sessionCookie.maxAge;
|
|
333
|
+
}
|
|
334
|
+
if (isRedirectObject(result)) {
|
|
335
|
+
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
336
|
+
return c.redirect(result.url, result.status);
|
|
337
|
+
}
|
|
338
|
+
if (result instanceof Response) {
|
|
339
|
+
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
setCookie(c, SESS_KEY, sessionValue, sessionOptions);
|
|
343
|
+
Object.entries(responseHeaders).forEach(([key, value]) => {
|
|
344
|
+
c.header(key, value);
|
|
345
|
+
});
|
|
346
|
+
const contentType = responseHeaders["content-type"];
|
|
347
|
+
if (contentType?.includes("application/json")) {
|
|
348
|
+
return c.json(
|
|
349
|
+
// biome-ignore lint/suspicious/noExplicitAny: to JSON response
|
|
350
|
+
result ?? { success: true },
|
|
351
|
+
responseStatus
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return c.html(result, responseStatus);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return c.html(generateCgiError({ error, $_SERVER }), 500);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
return app;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// src/with-defaults.ts
|
|
365
|
+
var loadPagesFromPublic = () => {
|
|
3
366
|
const modules = import.meta.glob("/public/**/*.cgi.{tsx,jsx}", {
|
|
4
367
|
eager: true
|
|
5
368
|
});
|
|
@@ -68,7 +431,7 @@ const loadPagesFromPublic = () => {
|
|
|
68
431
|
}, {});
|
|
69
432
|
return { pages, authMap, rewriteMap };
|
|
70
433
|
};
|
|
71
|
-
|
|
434
|
+
var createCgi = (options) => {
|
|
72
435
|
const resolvedConfig = typeof __MATCHBOX_CONFIG__ === "undefined" ? {} : __MATCHBOX_CONFIG__;
|
|
73
436
|
const { pages, authMap, rewriteMap } = loadPagesFromPublic();
|
|
74
437
|
return createCgiWithPages(pages, resolvedConfig, authMap, rewriteMap, options);
|