nibula 2.5.0 → 3.0.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/.eleventy.js +0 -7
- package/CHANGELOG.md +53 -3
- package/bin/create.js +43 -45
- package/bin/nibula.js +2 -8
- package/nginx.conf +1 -1
- package/package.json +3 -7
- package/src/backend/.htaccess +2 -2
- package/src/backend/core/index.js +329 -0
- package/src/backend/core/index.php +207 -0
- package/src/backend/core/init.js +37 -0
- package/src/backend/core/init.php +54 -0
- package/src/backend/core/modules/RateLimiter.js +55 -0
- package/src/backend/core/modules/RateLimiter.php +39 -0
- package/src/backend/{_core → core}/modules/Response.js +15 -10
- package/src/backend/{_core → core}/modules/Response.php +14 -17
- package/src/backend/core/modules/Validate.js +56 -0
- package/src/backend/core/modules/Validate.php +52 -0
- package/src/backend/endpoints/protected/example-protected.js +39 -0
- package/src/backend/endpoints/protected/example-protected.php +47 -0
- package/src/backend/endpoints/public/example-public.js +39 -0
- package/src/backend/endpoints/public/example-public.php +47 -0
- package/src/backend/example.config.js +7 -6
- package/src/backend/example.config.php +8 -7
- package/src/backend/package.json +3 -3
- package/src/backend/web.config +2 -2
- package/src/frontend/hosting/.htaccess +1 -1
- package/src/frontend/hosting/web.config +1 -1
- package/src/frontend/layouts/base.njk +3 -7
- package/src/frontend/scss/{_global.scss → global.scss} +0 -2
- package/src/frontend/scss/modules/frameworks/_bootstrap.scss +0 -2
- package/src/frontend/scss/modules/frameworks/_bulma.scss +0 -2
- package/src/frontend/scss/pages/404.scss +0 -3
- package/src/frontend/scss/pages/homepage.scss +0 -3
- package/tools/res/templates/template.json +13 -0
- package/tools/res/templates/template.scss +0 -3
- package/src/backend/_core/index.js +0 -230
- package/src/backend/_core/index.php +0 -118
- package/src/backend/_core/init.js +0 -32
- package/src/backend/_core/init.php +0 -30
- package/src/backend/_core/modules/RateLimiter.js +0 -47
- package/src/backend/_core/modules/RateLimiter.php +0 -31
- package/src/backend/api/protected/example-protected.js +0 -16
- package/src/backend/api/protected/example-protected.php +0 -17
- package/src/backend/api/public/example-public.js +0 -16
- package/src/backend/api/public/example-public.php +0 -17
- package/src/frontend/scss/modules/frameworks/_foundation.scss +0 -138
- package/src/frontend/scss/modules/frameworks/_uikit.scss +0 -110
- package/tools/buildJs.js +0 -39
- /package/src/backend/{_core → core}/composer.json +0 -0
- /package/src/backend/{_core → core}/composer.lock +0 -0
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const http = require("http");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const path = require("path");
|
|
6
|
-
const crypto = require("crypto");
|
|
7
|
-
|
|
8
|
-
const { createResponse, HALT } = require("./modules/Response");
|
|
9
|
-
const RateLimiter = require("./modules/RateLimiter");
|
|
10
|
-
const { loadConfig, handleException } = require("./init");
|
|
11
|
-
|
|
12
|
-
const CORE_PATH = __dirname;
|
|
13
|
-
|
|
14
|
-
const config = loadConfig();
|
|
15
|
-
|
|
16
|
-
const APP_ENV = config.APP_ENV || "production";
|
|
17
|
-
|
|
18
|
-
const DOCUMENT_ROOT =
|
|
19
|
-
process.env.DOCUMENT_ROOT || path.join(CORE_PATH, "..", "..");
|
|
20
|
-
|
|
21
|
-
const basePublic = path.join(CORE_PATH, "..", "api", "public");
|
|
22
|
-
const baseProtected = path.join(CORE_PATH, "..", "api", "protected");
|
|
23
|
-
|
|
24
|
-
function hashEquals(known, given) {
|
|
25
|
-
const a = Buffer.from(String(known));
|
|
26
|
-
const b = Buffer.from(String(given));
|
|
27
|
-
if (a.length !== b.length) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return crypto.timingSafeEqual(a, b);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function send404(res) {
|
|
34
|
-
res.statusCode = 404;
|
|
35
|
-
const errorPage = DOCUMENT_ROOT ? path.join(DOCUMENT_ROOT, "404.html") : "";
|
|
36
|
-
if (errorPage && fs.existsSync(errorPage)) {
|
|
37
|
-
res.setHeader("Content-Type", "text/html; charset=UTF-8");
|
|
38
|
-
res.end(fs.readFileSync(errorPage));
|
|
39
|
-
} else {
|
|
40
|
-
res.setHeader("Content-Type", "text/html; charset=UTF-8");
|
|
41
|
-
res.end(
|
|
42
|
-
"<h1>404 Not Found</h1>The requested URL was not found on this server.",
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function readBody(req) {
|
|
48
|
-
return new Promise((resolve) => {
|
|
49
|
-
const chunks = [];
|
|
50
|
-
req.on("data", (c) => chunks.push(c));
|
|
51
|
-
req.on("end", () => {
|
|
52
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
53
|
-
if (!raw) return resolve({ raw: "", json: null });
|
|
54
|
-
let json = null;
|
|
55
|
-
try {
|
|
56
|
-
json = JSON.parse(raw);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
json = null;
|
|
59
|
-
}
|
|
60
|
-
resolve({ raw, json });
|
|
61
|
-
});
|
|
62
|
-
req.on("error", () => resolve({ raw: "", json: null }));
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function handleRequest(req, res) {
|
|
67
|
-
const Response = createResponse(res);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const method = req.method;
|
|
71
|
-
const parsedUrl = new URL(req.url, "http://localhost");
|
|
72
|
-
let uri = parsedUrl.pathname;
|
|
73
|
-
|
|
74
|
-
uri = uri.replace(/^\/api/, "");
|
|
75
|
-
uri = uri.replace(/\/+$/, "");
|
|
76
|
-
if (uri === "") uri = "/";
|
|
77
|
-
|
|
78
|
-
const parts = uri.split("/").filter((p) => p !== "");
|
|
79
|
-
|
|
80
|
-
for (const part of parts) {
|
|
81
|
-
if (part === ".." || part === ".") {
|
|
82
|
-
Response.error("Bad Request", 400);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let endpointFile = null;
|
|
87
|
-
let isProtected = false;
|
|
88
|
-
|
|
89
|
-
const checkParts = parts.slice();
|
|
90
|
-
const params = [];
|
|
91
|
-
|
|
92
|
-
while (checkParts.length > 0) {
|
|
93
|
-
const relativePath = checkParts.join("/") + ".js";
|
|
94
|
-
|
|
95
|
-
const publicCandidate = path.join(basePublic, relativePath);
|
|
96
|
-
if (fs.existsSync(publicCandidate)) {
|
|
97
|
-
endpointFile = publicCandidate;
|
|
98
|
-
isProtected = false;
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const protectedCandidate = path.join(baseProtected, relativePath);
|
|
103
|
-
if (fs.existsSync(protectedCandidate)) {
|
|
104
|
-
endpointFile = protectedCandidate;
|
|
105
|
-
isProtected = true;
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
params.unshift(checkParts.pop());
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!endpointFile) {
|
|
113
|
-
send404(res);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const base = isProtected ? baseProtected : basePublic;
|
|
118
|
-
let endpointPath = endpointFile
|
|
119
|
-
.split(path.sep)
|
|
120
|
-
.join("/")
|
|
121
|
-
.replace(base.split(path.sep).join("/"), "")
|
|
122
|
-
.replace(/\.js$/, "")
|
|
123
|
-
.replace(/^\//, "");
|
|
124
|
-
|
|
125
|
-
res.setHeader("Content-Type", "application/json; charset=UTF-8");
|
|
126
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
127
|
-
res.setHeader("X-Frame-Options", "DENY");
|
|
128
|
-
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
129
|
-
res.setHeader(
|
|
130
|
-
"Access-Control-Allow-Methods",
|
|
131
|
-
"GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
132
|
-
);
|
|
133
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Api-Key");
|
|
134
|
-
|
|
135
|
-
const originsForEndpoint =
|
|
136
|
-
(config.CUSTOM_ENDPOINT_ORIGINS &&
|
|
137
|
-
config.CUSTOM_ENDPOINT_ORIGINS[endpointPath]) ||
|
|
138
|
-
config.GENERAL_ALLOWED_ORIGINS ||
|
|
139
|
-
[];
|
|
140
|
-
|
|
141
|
-
const origin = req.headers["origin"] || "";
|
|
142
|
-
|
|
143
|
-
if (origin !== "" && originsForEndpoint.includes(origin)) {
|
|
144
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
145
|
-
res.setHeader("Vary", "Origin");
|
|
146
|
-
} else if (originsForEndpoint.includes("*")) {
|
|
147
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (method === "OPTIONS") {
|
|
151
|
-
res.statusCode = 204;
|
|
152
|
-
res.end();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Rate limit: 60 requests / 60 seconds per IP
|
|
157
|
-
const ip =
|
|
158
|
-
(req.headers["x-forwarded-for"] || "").split(",")[0].trim() ||
|
|
159
|
-
req.socket.remoteAddress ||
|
|
160
|
-
"";
|
|
161
|
-
RateLimiter.check(ip, 60, 60, Response, res);
|
|
162
|
-
|
|
163
|
-
if (isProtected) {
|
|
164
|
-
const validKey =
|
|
165
|
-
(config.CUSTOM_ENDPOINT_KEYS &&
|
|
166
|
-
config.CUSTOM_ENDPOINT_KEYS[endpointPath]) ||
|
|
167
|
-
config.GENERAL_API_KEY ||
|
|
168
|
-
"";
|
|
169
|
-
const apiKey = req.headers["x-api-key"] || "";
|
|
170
|
-
|
|
171
|
-
if (validKey === "" || !hashEquals(validKey, apiKey)) {
|
|
172
|
-
Response.error("Unauthorized. X_API_KEY is incorrect or missing", 403);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const handler = require(endpointFile);
|
|
177
|
-
|
|
178
|
-
const context = {
|
|
179
|
-
method, // HTTP method
|
|
180
|
-
requestParams: params, // leftover URL segments (route params)
|
|
181
|
-
Response, // request-bound Response helper
|
|
182
|
-
query: Object.fromEntries(parsedUrl.searchParams), // parsed query string
|
|
183
|
-
body: body.json, // parsed JSON body (or null)
|
|
184
|
-
rawBody: body.raw, // raw body string
|
|
185
|
-
headers: req.headers,
|
|
186
|
-
req,
|
|
187
|
-
res,
|
|
188
|
-
config,
|
|
189
|
-
CORE_PATH,
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
await handler(context);
|
|
193
|
-
|
|
194
|
-
if (!res.writableEnded) {
|
|
195
|
-
res.end();
|
|
196
|
-
}
|
|
197
|
-
} catch (err) {
|
|
198
|
-
if (err === HALT) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
try {
|
|
202
|
-
handleException(err, config, Response);
|
|
203
|
-
} catch (inner) {
|
|
204
|
-
if (inner === HALT) return;
|
|
205
|
-
if (!res.writableEnded) {
|
|
206
|
-
res.statusCode = 500;
|
|
207
|
-
res.end(
|
|
208
|
-
JSON.stringify({
|
|
209
|
-
status: "error",
|
|
210
|
-
message: "Internal server error",
|
|
211
|
-
code: 500,
|
|
212
|
-
}),
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const HOST = process.env.HOST || "127.0.0.1";
|
|
220
|
-
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
221
|
-
|
|
222
|
-
const server = http.createServer(handleRequest);
|
|
223
|
-
|
|
224
|
-
server.listen(PORT, HOST, () => {
|
|
225
|
-
console.log(
|
|
226
|
-
`[backend-node] listening on http://${HOST}:${PORT} (env: ${APP_ENV})`,
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
module.exports = server;
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
define('CORE_ACCESS', true);
|
|
6
|
-
define('CORE_PATH', __DIR__);
|
|
7
|
-
|
|
8
|
-
require_once __DIR__ . '/init.php';
|
|
9
|
-
require_once __DIR__ . '/modules/Response.php';
|
|
10
|
-
|
|
11
|
-
$method = $_SERVER['REQUEST_METHOD'];
|
|
12
|
-
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
|
13
|
-
|
|
14
|
-
$uri = rtrim(preg_replace('#^/api#', '', $uri), '/') ?: '/';
|
|
15
|
-
$parts = array_values(array_filter(explode('/', $uri)));
|
|
16
|
-
|
|
17
|
-
foreach ($parts as $part) {
|
|
18
|
-
if ($part === '..' || $part === '.') {
|
|
19
|
-
Response::error('Bad Request', 400);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
$basePublic = __DIR__ . '/../api/public/';
|
|
24
|
-
$baseProtected = __DIR__ . '/../api/protected/';
|
|
25
|
-
|
|
26
|
-
$endpointFile = null;
|
|
27
|
-
$isProtected = false;
|
|
28
|
-
$requestParams = [];
|
|
29
|
-
$checkParts = $parts;
|
|
30
|
-
$params = [];
|
|
31
|
-
|
|
32
|
-
while (count($checkParts) > 0) {
|
|
33
|
-
$relativePath = implode('/', $checkParts) . '.php';
|
|
34
|
-
|
|
35
|
-
// Check first whether this is a public route
|
|
36
|
-
if (file_exists($basePublic . $relativePath)) {
|
|
37
|
-
$endpointFile = $basePublic . $relativePath;
|
|
38
|
-
$isProtected = false;
|
|
39
|
-
break;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (file_exists($baseProtected . $relativePath)) {
|
|
43
|
-
$endpointFile = $baseProtected . $relativePath;
|
|
44
|
-
$isProtected = true;
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
array_unshift($params, array_pop($checkParts));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!$endpointFile) {
|
|
52
|
-
http_response_code(404);
|
|
53
|
-
header_remove('WWW-Authenticate');
|
|
54
|
-
|
|
55
|
-
$documentRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
|
|
56
|
-
$errorPage = $documentRoot . '/404.html';
|
|
57
|
-
|
|
58
|
-
if (!file_exists($errorPage)) {
|
|
59
|
-
$errorPage = __DIR__ . '/../../404.html';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
header('Content-Type: text/html; charset=UTF-8');
|
|
63
|
-
|
|
64
|
-
if (file_exists($errorPage)) {
|
|
65
|
-
echo file_get_contents($errorPage);
|
|
66
|
-
} else {
|
|
67
|
-
echo "<h1>404 Not Found</h1>";
|
|
68
|
-
echo "The requested URL was not found on this server.";
|
|
69
|
-
}
|
|
70
|
-
exit;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
$base = $isProtected ? $baseProtected : $basePublic;
|
|
74
|
-
$base = str_replace('\\', '/', $base);
|
|
75
|
-
$endpointPath = str_replace('\\', '/', $endpointFile);
|
|
76
|
-
$endpointPath = preg_replace('#\.php$#', '', str_replace($base, '', $endpointPath));
|
|
77
|
-
|
|
78
|
-
header('Content-Type: application/json; charset=UTF-8');
|
|
79
|
-
header_remove('WWW-Authenticate');
|
|
80
|
-
header('X-Content-Type-Options: nosniff');
|
|
81
|
-
header('X-Frame-Options: DENY');
|
|
82
|
-
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
83
|
-
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
84
|
-
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
|
|
85
|
-
|
|
86
|
-
$originsForEndpoint = $config['CUSTOM_ENDPOINT_ORIGINS'][$endpointPath]
|
|
87
|
-
?? $config['GENERAL_ALLOWED_ORIGINS']
|
|
88
|
-
?? [];
|
|
89
|
-
|
|
90
|
-
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
91
|
-
|
|
92
|
-
if ($origin !== '' && in_array($origin, $originsForEndpoint, true)) {
|
|
93
|
-
header('Access-Control-Allow-Origin: ' . $origin);
|
|
94
|
-
header('Vary: Origin');
|
|
95
|
-
} elseif (in_array('*', $originsForEndpoint, true)) {
|
|
96
|
-
header('Access-Control-Allow-Origin: *');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if ($method === 'OPTIONS') {
|
|
100
|
-
http_response_code(204);
|
|
101
|
-
exit;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
require_once __DIR__ . '/modules/RateLimiter.php';
|
|
105
|
-
RateLimiter::check($_SERVER['REMOTE_ADDR'] ?? '', 60, 60);
|
|
106
|
-
|
|
107
|
-
if ($isProtected) {
|
|
108
|
-
$validKey = $config['CUSTOM_ENDPOINT_KEYS'][$endpointPath] ?? $config['GENERAL_API_KEY'] ?? '';
|
|
109
|
-
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
|
110
|
-
|
|
111
|
-
if ($validKey === '' || !hash_equals($validKey, $apiKey)) {
|
|
112
|
-
Response::error('Unauthorized. X_API_KEY is incorrect or missing', 403);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
$requestParams = $params;
|
|
117
|
-
|
|
118
|
-
require $endpointFile;
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
|
|
6
|
-
function loadConfig() {
|
|
7
|
-
const backendRoot = path.join(__dirname, "..");
|
|
8
|
-
const configPath = path.join(backendRoot, "config.js");
|
|
9
|
-
const examplePath = path.join(backendRoot, "example.config.js");
|
|
10
|
-
|
|
11
|
-
if (fs.existsSync(configPath)) {
|
|
12
|
-
return require(configPath);
|
|
13
|
-
}
|
|
14
|
-
return require(examplePath);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Turn any thrown error into a 500 response, mirroring set_exception_handler.
|
|
19
|
-
* @param {Error} exception
|
|
20
|
-
* @param {object} config
|
|
21
|
-
* @param {object} Response request-bound helper
|
|
22
|
-
*/
|
|
23
|
-
function handleException(exception, config, Response) {
|
|
24
|
-
const isDebug = (config.APP_ENV || "production") !== "production";
|
|
25
|
-
Response.error(
|
|
26
|
-
isDebug ? exception.message : "Internal server error",
|
|
27
|
-
500,
|
|
28
|
-
isDebug ? { stack: exception.stack } : null,
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
module.exports = { loadConfig, handleException };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
require_once __DIR__ . '/vendor/autoload.php';
|
|
6
|
-
require_once __DIR__ . '/modules/Response.php';
|
|
7
|
-
|
|
8
|
-
set_exception_handler(function ($exception) use (&$config) {
|
|
9
|
-
$isDebug = ($config['APP_ENV'] ?? 'production') !== 'production';
|
|
10
|
-
Response::error(
|
|
11
|
-
$isDebug ? $exception->getMessage() : 'Internal server error',
|
|
12
|
-
500,
|
|
13
|
-
$isDebug ? ['file' => $exception->getFile(), 'line' => $exception->getLine()] : null
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
set_error_handler(function ($severity, $message, $file, $line) {
|
|
18
|
-
if (!(error_reporting() & $severity)) return;
|
|
19
|
-
throw new ErrorException($message, 0, $severity, $file, $line);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
$config = require dirname(__DIR__) . '/config.php';
|
|
23
|
-
|
|
24
|
-
if (($config['APP_ENV'] ?? 'production') === 'production') {
|
|
25
|
-
ini_set('display_errors', '0');
|
|
26
|
-
error_reporting(0);
|
|
27
|
-
} else {
|
|
28
|
-
ini_set('display_errors', '1');
|
|
29
|
-
error_reporting(E_ALL);
|
|
30
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const crypto = require("crypto");
|
|
6
|
-
|
|
7
|
-
class RateLimiter {
|
|
8
|
-
/**
|
|
9
|
-
* @param {string} ip
|
|
10
|
-
* @param {number} maxRequests
|
|
11
|
-
* @param {number} windowSeconds
|
|
12
|
-
* @param {object} Response request-bound helper from createResponse(res)
|
|
13
|
-
* @param {object} res raw http response (to set Retry-After)
|
|
14
|
-
*/
|
|
15
|
-
static check(ip, maxRequests = 60, windowSeconds = 60, Response, res) {
|
|
16
|
-
const cacheDir = path.join(__dirname, "..", "..", "cache");
|
|
17
|
-
|
|
18
|
-
if (!fs.existsSync(cacheDir)) {
|
|
19
|
-
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o755 });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const hash = crypto.createHash("md5").update(ip).digest("hex");
|
|
23
|
-
const cacheFile = path.join(cacheDir, "rl_" + hash + ".json");
|
|
24
|
-
const now = Math.floor(Date.now() / 1000);
|
|
25
|
-
let data = [];
|
|
26
|
-
|
|
27
|
-
if (fs.existsSync(cacheFile)) {
|
|
28
|
-
try {
|
|
29
|
-
data = JSON.parse(fs.readFileSync(cacheFile, "utf8")) || [];
|
|
30
|
-
} catch (e) {
|
|
31
|
-
data = [];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
data = data.filter((ts) => ts > now - windowSeconds);
|
|
36
|
-
data.push(now);
|
|
37
|
-
|
|
38
|
-
fs.writeFileSync(cacheFile, JSON.stringify(data));
|
|
39
|
-
|
|
40
|
-
if (data.length > maxRequests) {
|
|
41
|
-
res.setHeader("Retry-After", String(windowSeconds));
|
|
42
|
-
Response.error("Too Many Requests", 429);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
module.exports = RateLimiter;
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
class RateLimiter {
|
|
6
|
-
public static function check(string $ip, int $maxRequests = 60, int $windowSeconds = 60): void {
|
|
7
|
-
$cacheDir = __DIR__ . '/../../cache';
|
|
8
|
-
|
|
9
|
-
if (!is_dir($cacheDir)) {
|
|
10
|
-
mkdir($cacheDir, 0755, true);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
$cacheFile = $cacheDir . '/rl_' . md5($ip) . '.json';
|
|
14
|
-
$now = time();
|
|
15
|
-
$data = [];
|
|
16
|
-
|
|
17
|
-
if (file_exists($cacheFile)) {
|
|
18
|
-
$data = json_decode(file_get_contents($cacheFile), true) ?: [];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
$data = array_filter($data, fn($ts) => $ts > ($now - $windowSeconds));
|
|
22
|
-
$data[] = $now;
|
|
23
|
-
|
|
24
|
-
file_put_contents($cacheFile, json_encode(array_values($data)), LOCK_EX);
|
|
25
|
-
|
|
26
|
-
if (count($data) > $maxRequests) {
|
|
27
|
-
header('Retry-After: ' . $windowSeconds);
|
|
28
|
-
Response::error('Too Many Requests', 429);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
module.exports = ({ method, requestParams, Response }) => {
|
|
4
|
-
if (method !== "GET") {
|
|
5
|
-
Response.error("Method not allowed", 405);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// Your endpoint logic here. You can access route parameters in requestParams
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
Response.success({
|
|
13
|
-
message: "Protected endpoint is working",
|
|
14
|
-
params: requestParams,
|
|
15
|
-
});
|
|
16
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
declare(strict_types=1);
|
|
3
|
-
|
|
4
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
5
|
-
|
|
6
|
-
if ($method !== 'GET') {
|
|
7
|
-
Response::error('Method not allowed', 405);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
// Your endpoint logic here. You can access route parameters in $requestParams array
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
Response::success([
|
|
15
|
-
'message' => 'Protected endpoint is working',
|
|
16
|
-
'params' => $requestParams,
|
|
17
|
-
]);
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
module.exports = ({ method, requestParams, Response }) => {
|
|
4
|
-
if (method !== "GET") {
|
|
5
|
-
Response.error("Method not allowed", 405);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// Your endpoint logic here. You can access route parameters in requestParams
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
Response.success({
|
|
13
|
-
message: "Public endpoint is working",
|
|
14
|
-
params: requestParams,
|
|
15
|
-
});
|
|
16
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
declare(strict_types=1);
|
|
3
|
-
|
|
4
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
5
|
-
|
|
6
|
-
if ($method !== 'GET') {
|
|
7
|
-
Response::error('Method not allowed', 405);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
// Your endpoint logic here. You can access route parameters in $requestParams array
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
Response::success([
|
|
15
|
-
'message' => 'Public endpoint is working',
|
|
16
|
-
'params' => $requestParams,
|
|
17
|
-
]);
|