nitro5 1.0.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/.install.js +158 -0
- package/README.md +258 -0
- package/binding.gyp +25 -0
- package/index.js +54 -0
- package/native/nitro5.cc +742 -0
- package/nitro5.config.js +8 -0
- package/package.json +62 -0
- package/public/app.tsx +10 -0
- package/public/index.html +10 -0
- package/public/main.tsx +5 -0
- package/src/app.js +1162 -0
- package/src/cache.js +53 -0
- package/src/dashboard.js +215 -0
- package/src/deps-cache.js +37 -0
- package/src/disk-cache.js +37 -0
- package/src/file-worker.js +19 -0
- package/src/hmr.js +38 -0
- package/src/logger.js +51 -0
- package/src/mime.js +32 -0
- package/src/msg.js +16 -0
- package/src/native.js +75 -0
- package/src/router.js +85 -0
- package/src/stat.js +25 -0
- package/src/static.js +201 -0
- package/src/stats.js +25 -0
- package/src/supervisor.js +249 -0
- package/src/thread-pool.js +107 -0
- package/src/tsc.js +329 -0
- package/src/vite.js +15 -0
- package/src/watcher.js +63 -0
package/native/nitro5.cc
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
#include <napi.h>
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cctype>
|
|
5
|
+
#include <chrono>
|
|
6
|
+
#include <ctime>
|
|
7
|
+
#include <fstream>
|
|
8
|
+
#include <iomanip>
|
|
9
|
+
#include <mutex>
|
|
10
|
+
#include <sstream>
|
|
11
|
+
#include <stdexcept>
|
|
12
|
+
#include <string>
|
|
13
|
+
#include <string_view>
|
|
14
|
+
#include <unordered_map>
|
|
15
|
+
#include <vector>
|
|
16
|
+
|
|
17
|
+
#if !defined(_WIN32)
|
|
18
|
+
#include <unistd.h>
|
|
19
|
+
#include <sys/resource.h>
|
|
20
|
+
#endif
|
|
21
|
+
|
|
22
|
+
namespace nitro5 {
|
|
23
|
+
|
|
24
|
+
static std::mutex g_logMutex;
|
|
25
|
+
static std::string g_logFilePath;
|
|
26
|
+
|
|
27
|
+
static std::string NowString() {
|
|
28
|
+
using namespace std::chrono;
|
|
29
|
+
auto now = system_clock::now();
|
|
30
|
+
std::time_t t = system_clock::to_time_t(now);
|
|
31
|
+
|
|
32
|
+
std::tm tm{};
|
|
33
|
+
#if defined(_WIN32)
|
|
34
|
+
localtime_s(&tm, &t);
|
|
35
|
+
#else
|
|
36
|
+
localtime_r(&t, &tm);
|
|
37
|
+
#endif
|
|
38
|
+
|
|
39
|
+
std::ostringstream oss;
|
|
40
|
+
oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
|
|
41
|
+
return oss.str();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static void AppendLog(const std::string& level, const std::string& message) {
|
|
45
|
+
std::lock_guard<std::mutex> lock(g_logMutex);
|
|
46
|
+
if (g_logFilePath.empty()) return;
|
|
47
|
+
|
|
48
|
+
std::ofstream out(g_logFilePath, std::ios::app);
|
|
49
|
+
if (!out.is_open()) return;
|
|
50
|
+
|
|
51
|
+
out << "[" << NowString() << "]"
|
|
52
|
+
<< "[" << level << "] "
|
|
53
|
+
<< message << "\n";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static inline bool IsSpace(char c) {
|
|
57
|
+
return std::isspace(static_cast<unsigned char>(c)) != 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static inline std::string_view TrimView(std::string_view input) {
|
|
61
|
+
size_t start = 0;
|
|
62
|
+
while (start < input.size() && IsSpace(input[start])) start++;
|
|
63
|
+
|
|
64
|
+
size_t end = input.size();
|
|
65
|
+
while (end > start && IsSpace(input[end - 1])) end--;
|
|
66
|
+
|
|
67
|
+
return input.substr(start, end - start);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static inline std::string_view StripCR(std::string_view s) {
|
|
71
|
+
if (!s.empty() && s.back() == '\r') {
|
|
72
|
+
s.remove_suffix(1);
|
|
73
|
+
}
|
|
74
|
+
return s;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static std::string ToLowerAscii(std::string_view value) {
|
|
78
|
+
std::string out;
|
|
79
|
+
out.resize(value.size());
|
|
80
|
+
for (size_t i = 0; i < value.size(); ++i) {
|
|
81
|
+
out[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(value[i])));
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static bool IEqualsAscii(std::string_view a, std::string_view b) {
|
|
87
|
+
if (a.size() != b.size()) return false;
|
|
88
|
+
for (size_t i = 0; i < a.size(); ++i) {
|
|
89
|
+
if (std::tolower(static_cast<unsigned char>(a[i])) !=
|
|
90
|
+
std::tolower(static_cast<unsigned char>(b[i]))) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static bool IContainsTokenList(std::string_view csv, std::string_view token) {
|
|
98
|
+
if (token.empty()) return true;
|
|
99
|
+
|
|
100
|
+
size_t pos = 0;
|
|
101
|
+
while (pos <= csv.size()) {
|
|
102
|
+
size_t comma = csv.find(',', pos);
|
|
103
|
+
std::string_view part = (comma == std::string_view::npos)
|
|
104
|
+
? csv.substr(pos)
|
|
105
|
+
: csv.substr(pos, comma - pos);
|
|
106
|
+
|
|
107
|
+
part = TrimView(part);
|
|
108
|
+
if (IEqualsAscii(part, token)) return true;
|
|
109
|
+
|
|
110
|
+
if (comma == std::string_view::npos) break;
|
|
111
|
+
pos = comma + 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static int HexValue(char c) {
|
|
118
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
119
|
+
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
|
|
120
|
+
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
|
|
121
|
+
return -1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static std::string UrlDecode(std::string_view input) {
|
|
125
|
+
std::string out;
|
|
126
|
+
out.reserve(input.size());
|
|
127
|
+
|
|
128
|
+
for (size_t i = 0; i < input.size(); ++i) {
|
|
129
|
+
char c = input[i];
|
|
130
|
+
if (c == '+') {
|
|
131
|
+
out.push_back(' ');
|
|
132
|
+
} else if (c == '%' && i + 2 < input.size()) {
|
|
133
|
+
int hi = HexValue(input[i + 1]);
|
|
134
|
+
int lo = HexValue(input[i + 2]);
|
|
135
|
+
if (hi >= 0 && lo >= 0) {
|
|
136
|
+
out.push_back(static_cast<char>((hi << 4) | lo));
|
|
137
|
+
i += 2;
|
|
138
|
+
} else {
|
|
139
|
+
out.push_back(c);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
out.push_back(c);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static Napi::Value JsonParse(Napi::Env env, const std::string& body) {
|
|
150
|
+
Napi::Object global = env.Global();
|
|
151
|
+
Napi::Object JSON = global.Get("JSON").As<Napi::Object>();
|
|
152
|
+
Napi::Function parse = JSON.Get("parse").As<Napi::Function>();
|
|
153
|
+
return parse.Call(JSON, { Napi::String::New(env, body) });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static std::string JsonStringify(Napi::Env env, const Napi::Value& v) {
|
|
157
|
+
Napi::Object global = env.Global();
|
|
158
|
+
Napi::Object JSON = global.Get("JSON").As<Napi::Object>();
|
|
159
|
+
Napi::Function stringify = JSON.Get("stringify").As<Napi::Function>();
|
|
160
|
+
Napi::Value json = stringify.Call(JSON, { v });
|
|
161
|
+
return json.ToString().Utf8Value();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static std::string GetHeaderSingle(const Napi::Object& headers, const std::string& key) {
|
|
165
|
+
if (!headers.Has(key)) return "";
|
|
166
|
+
|
|
167
|
+
Napi::Value v = headers.Get(key);
|
|
168
|
+
if (v.IsString()) return v.As<Napi::String>().Utf8Value();
|
|
169
|
+
|
|
170
|
+
if (v.IsArray()) {
|
|
171
|
+
Napi::Array arr = v.As<Napi::Array>();
|
|
172
|
+
if (arr.Length() == 0) return "";
|
|
173
|
+
Napi::Value first = arr.Get((uint32_t)0);
|
|
174
|
+
if (first.IsString()) return first.As<Napi::String>().Utf8Value();
|
|
175
|
+
return first.ToString().Utf8Value();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return v.ToString().Utf8Value();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
static bool HeaderContains(const Napi::Object& headers, const std::string& key, const std::string& token) {
|
|
182
|
+
std::string value = ToLowerAscii(GetHeaderSingle(headers, key));
|
|
183
|
+
std::string needle = ToLowerAscii(token);
|
|
184
|
+
return IContainsTokenList(value, needle);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static void AddHeaderValue(Napi::Env env, Napi::Object headers, std::string_view key, std::string_view value) {
|
|
188
|
+
std::string k(key);
|
|
189
|
+
std::string v(value);
|
|
190
|
+
|
|
191
|
+
if (!headers.Has(k)) {
|
|
192
|
+
headers.Set(k, v);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Napi::Value existing = headers.Get(k);
|
|
197
|
+
if (existing.IsArray()) {
|
|
198
|
+
Napi::Array arr = existing.As<Napi::Array>();
|
|
199
|
+
arr.Set(arr.Length(), Napi::String::New(env, v));
|
|
200
|
+
headers.Set(k, arr);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
Napi::Array arr = Napi::Array::New(env);
|
|
205
|
+
arr.Set((uint32_t)0, existing);
|
|
206
|
+
arr.Set((uint32_t)1, Napi::String::New(env, v));
|
|
207
|
+
headers.Set(k, arr);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static Napi::Object ParseUrlEncodedToObject(Napi::Env env, const std::string& body) {
|
|
211
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
212
|
+
|
|
213
|
+
size_t start = 0;
|
|
214
|
+
while (start <= body.size()) {
|
|
215
|
+
size_t amp = body.find('&', start);
|
|
216
|
+
std::string pair = body.substr(start, amp == std::string::npos ? std::string::npos : amp - start);
|
|
217
|
+
|
|
218
|
+
if (!pair.empty()) {
|
|
219
|
+
size_t eq = pair.find('=');
|
|
220
|
+
std::string k = UrlDecode(eq == std::string::npos ? pair : pair.substr(0, eq));
|
|
221
|
+
std::string v = UrlDecode(eq == std::string::npos ? "" : pair.substr(eq + 1));
|
|
222
|
+
obj.Set(k, v);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (amp == std::string::npos) break;
|
|
226
|
+
start = amp + 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return obj;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static std::string NormalizeContentType(const std::string& ct) {
|
|
233
|
+
std::string lower = ToLowerAscii(ct);
|
|
234
|
+
size_t semi = lower.find(';');
|
|
235
|
+
if (semi != std::string::npos) lower = lower.substr(0, semi);
|
|
236
|
+
return std::string(TrimView(lower));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static std::string DecodeChunkedBody(std::string_view raw) {
|
|
240
|
+
std::string out;
|
|
241
|
+
out.reserve(raw.size());
|
|
242
|
+
|
|
243
|
+
size_t pos = 0;
|
|
244
|
+
while (pos < raw.size()) {
|
|
245
|
+
size_t lineEnd = raw.find("\r\n", pos);
|
|
246
|
+
if (lineEnd == std::string_view::npos) {
|
|
247
|
+
throw std::runtime_error("Invalid chunked body: missing chunk size line ending");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
std::string_view sizeLine = raw.substr(pos, lineEnd - pos);
|
|
251
|
+
sizeLine = TrimView(sizeLine);
|
|
252
|
+
|
|
253
|
+
size_t semi = sizeLine.find(';');
|
|
254
|
+
if (semi != std::string_view::npos) {
|
|
255
|
+
sizeLine = sizeLine.substr(0, semi);
|
|
256
|
+
sizeLine = TrimView(sizeLine);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (sizeLine.empty()) {
|
|
260
|
+
throw std::runtime_error("Invalid chunked body: empty chunk size");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
size_t chunkSize = 0;
|
|
264
|
+
try {
|
|
265
|
+
chunkSize = static_cast<size_t>(std::stoull(std::string(sizeLine), nullptr, 16));
|
|
266
|
+
} catch (...) {
|
|
267
|
+
throw std::runtime_error("Invalid chunked body: bad chunk size");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
pos = lineEnd + 2;
|
|
271
|
+
|
|
272
|
+
if (chunkSize == 0) {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (pos + chunkSize > raw.size()) {
|
|
277
|
+
throw std::runtime_error("Invalid chunked body: chunk overflows buffer");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
out.append(raw.data() + pos, chunkSize);
|
|
281
|
+
pos += chunkSize;
|
|
282
|
+
|
|
283
|
+
if (pos + 2 > raw.size() || raw.substr(pos, 2) != "\r\n") {
|
|
284
|
+
throw std::runtime_error("Invalid chunked body: missing chunk terminator");
|
|
285
|
+
}
|
|
286
|
+
pos += 2;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
static bool ShouldKeepAlive(const std::string& httpVersion, const Napi::Object& headers) {
|
|
293
|
+
std::string conn = ToLowerAscii(GetHeaderSingle(headers, "connection"));
|
|
294
|
+
|
|
295
|
+
if (httpVersion == "HTTP/1.1") {
|
|
296
|
+
return conn != "close";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (httpVersion == "HTTP/1.0") {
|
|
300
|
+
return conn == "keep-alive";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return conn == "keep-alive";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
static Napi::Value ParseBodyByContentType(Napi::Env env, const std::string& contentType, const std::string& body) {
|
|
307
|
+
std::string ct = NormalizeContentType(contentType);
|
|
308
|
+
|
|
309
|
+
if (ct == "application/json" || ct.find("+json") != std::string::npos) {
|
|
310
|
+
try {
|
|
311
|
+
return JsonParse(env, body);
|
|
312
|
+
} catch (...) {
|
|
313
|
+
return Napi::String::New(env, body);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (ct == "application/x-www-form-urlencoded") {
|
|
318
|
+
return ParseUrlEncodedToObject(env, body);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return Napi::String::New(env, body);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
static std::string StatusText(int status) {
|
|
325
|
+
switch (status) {
|
|
326
|
+
case 100: return "Continue";
|
|
327
|
+
case 101: return "Switching Protocols";
|
|
328
|
+
case 200: return "OK";
|
|
329
|
+
case 201: return "Created";
|
|
330
|
+
case 202: return "Accepted";
|
|
331
|
+
case 204: return "No Content";
|
|
332
|
+
case 206: return "Partial Content";
|
|
333
|
+
case 301: return "Moved Permanently";
|
|
334
|
+
case 302: return "Found";
|
|
335
|
+
case 304: return "Not Modified";
|
|
336
|
+
case 400: return "Bad Request";
|
|
337
|
+
case 401: return "Unauthorized";
|
|
338
|
+
case 403: return "Forbidden";
|
|
339
|
+
case 404: return "Not Found";
|
|
340
|
+
case 405: return "Method Not Allowed";
|
|
341
|
+
case 408: return "Request Timeout";
|
|
342
|
+
case 413: return "Payload Too Large";
|
|
343
|
+
case 414: return "URI Too Long";
|
|
344
|
+
case 415: return "Unsupported Media Type";
|
|
345
|
+
case 426: return "Upgrade Required";
|
|
346
|
+
case 500: return "Internal Server Error";
|
|
347
|
+
case 501: return "Not Implemented";
|
|
348
|
+
case 502: return "Bad Gateway";
|
|
349
|
+
case 503: return "Service Unavailable";
|
|
350
|
+
default: return "OK";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
static std::string ValueToStringLike(const Napi::Value& v, Napi::Env env, bool& jsonLike) {
|
|
355
|
+
jsonLike = false;
|
|
356
|
+
|
|
357
|
+
if (v.IsUndefined() || v.IsNull()) {
|
|
358
|
+
return "";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (v.IsString()) return v.As<Napi::String>().Utf8Value();
|
|
362
|
+
if (v.IsNumber()) return v.ToNumber().ToString().Utf8Value();
|
|
363
|
+
if (v.IsBoolean()) return v.ToBoolean().Value() ? "true" : "false";
|
|
364
|
+
|
|
365
|
+
if (v.IsBuffer()) {
|
|
366
|
+
auto buf = v.As<Napi::Buffer<uint8_t>>();
|
|
367
|
+
return std::string(reinterpret_cast<char*>(buf.Data()), buf.Length());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
jsonLike = true;
|
|
371
|
+
return JsonStringify(env, v);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
Napi::Value Hello(const Napi::CallbackInfo& info) {
|
|
375
|
+
Napi::Env env = info.Env();
|
|
376
|
+
return Napi::String::New(env, "Nitro5 native core loaded");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
Napi::Value SetLogFile(const Napi::CallbackInfo& info) {
|
|
380
|
+
Napi::Env env = info.Env();
|
|
381
|
+
|
|
382
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
383
|
+
Napi::TypeError::New(env, "setLogFile expects a string path").ThrowAsJavaScriptException();
|
|
384
|
+
return env.Null();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
g_logFilePath = info[0].As<Napi::String>().Utf8Value();
|
|
388
|
+
return Napi::Boolean::New(env, true);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
Napi::Value Log(const Napi::CallbackInfo& info) {
|
|
392
|
+
Napi::Env env = info.Env();
|
|
393
|
+
|
|
394
|
+
if (info.Length() < 1) {
|
|
395
|
+
Napi::TypeError::New(env, "log expects at least one argument").ThrowAsJavaScriptException();
|
|
396
|
+
return env.Null();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
std::string level = "INFO";
|
|
400
|
+
std::string message;
|
|
401
|
+
|
|
402
|
+
if (info.Length() >= 2 && info[0].IsString() && info[1].IsString()) {
|
|
403
|
+
level = info[0].As<Napi::String>().Utf8Value();
|
|
404
|
+
message = info[1].As<Napi::String>().Utf8Value();
|
|
405
|
+
} else {
|
|
406
|
+
message = info[0].ToString().Utf8Value();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
AppendLog(level, message);
|
|
410
|
+
return env.Undefined();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
Napi::Value GetMetrics(const Napi::CallbackInfo& info) {
|
|
414
|
+
Napi::Env env = info.Env();
|
|
415
|
+
|
|
416
|
+
#if !defined(_WIN32)
|
|
417
|
+
struct rusage usage;
|
|
418
|
+
getrusage(RUSAGE_SELF, &usage);
|
|
419
|
+
|
|
420
|
+
long memoryKB = usage.ru_maxrss;
|
|
421
|
+
double userCPU = usage.ru_utime.tv_sec + usage.ru_utime.tv_usec / 1e6;
|
|
422
|
+
double sysCPU = usage.ru_stime.tv_sec + usage.ru_stime.tv_usec / 1e6;
|
|
423
|
+
#else
|
|
424
|
+
long memoryKB = 0;
|
|
425
|
+
double userCPU = 0.0;
|
|
426
|
+
double sysCPU = 0.0;
|
|
427
|
+
#endif
|
|
428
|
+
|
|
429
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
430
|
+
obj.Set("memoryKB", memoryKB);
|
|
431
|
+
obj.Set("cpuUser", userCPU);
|
|
432
|
+
obj.Set("cpuSystem", sysCPU);
|
|
433
|
+
return obj;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
Napi::Value ParseHttpRequest(const Napi::CallbackInfo& info) {
|
|
437
|
+
Napi::Env env = info.Env();
|
|
438
|
+
|
|
439
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
440
|
+
Napi::TypeError::New(env, "parseHttpRequest expects a raw HTTP request string").ThrowAsJavaScriptException();
|
|
441
|
+
return env.Null();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
size_t maxSize = 1024 * 1024;
|
|
445
|
+
if (info.Length() >= 2 && info[1].IsNumber()) {
|
|
446
|
+
double n = info[1].As<Napi::Number>().DoubleValue();
|
|
447
|
+
if (n > 0) maxSize = static_cast<size_t>(n);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
std::string raw = info[0].As<Napi::String>().Utf8Value();
|
|
451
|
+
if (raw.size() > maxSize) {
|
|
452
|
+
Napi::Error::New(env, "Request too large").ThrowAsJavaScriptException();
|
|
453
|
+
return env.Null();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
std::string_view rawView(raw);
|
|
457
|
+
|
|
458
|
+
size_t headerEnd = rawView.find("\r\n\r\n");
|
|
459
|
+
if (headerEnd == std::string_view::npos) {
|
|
460
|
+
Napi::Error::New(env, "Invalid HTTP request: missing header terminator").ThrowAsJavaScriptException();
|
|
461
|
+
return env.Null();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
std::string_view head = rawView.substr(0, headerEnd);
|
|
465
|
+
std::string_view rawBodyView = rawView.substr(headerEnd + 4);
|
|
466
|
+
|
|
467
|
+
// Request line
|
|
468
|
+
size_t lineEnd = head.find("\r\n");
|
|
469
|
+
std::string_view requestLine = (lineEnd == std::string_view::npos)
|
|
470
|
+
? head
|
|
471
|
+
: head.substr(0, lineEnd);
|
|
472
|
+
|
|
473
|
+
requestLine = StripCR(requestLine);
|
|
474
|
+
requestLine = TrimView(requestLine);
|
|
475
|
+
|
|
476
|
+
size_t sp1 = requestLine.find(' ');
|
|
477
|
+
if (sp1 == std::string_view::npos) {
|
|
478
|
+
Napi::Error::New(env, "Invalid HTTP request line").ThrowAsJavaScriptException();
|
|
479
|
+
return env.Null();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
size_t sp2 = requestLine.find(' ', sp1 + 1);
|
|
483
|
+
if (sp2 == std::string_view::npos) {
|
|
484
|
+
Napi::Error::New(env, "Invalid HTTP request line").ThrowAsJavaScriptException();
|
|
485
|
+
return env.Null();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
std::string method = std::string(TrimView(requestLine.substr(0, sp1)));
|
|
489
|
+
std::string fullPath = std::string(TrimView(requestLine.substr(sp1 + 1, sp2 - sp1 - 1)));
|
|
490
|
+
std::string httpVersion = std::string(TrimView(requestLine.substr(sp2 + 1)));
|
|
491
|
+
|
|
492
|
+
if (method.empty() || fullPath.empty() || httpVersion.empty()) {
|
|
493
|
+
Napi::Error::New(env, "Invalid HTTP request line").ThrowAsJavaScriptException();
|
|
494
|
+
return env.Null();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
std::string pathname = fullPath;
|
|
498
|
+
std::string query = "";
|
|
499
|
+
|
|
500
|
+
size_t queryIndex = fullPath.find('?');
|
|
501
|
+
if (queryIndex != std::string::npos) {
|
|
502
|
+
pathname = fullPath.substr(0, queryIndex);
|
|
503
|
+
query = fullPath.substr(queryIndex + 1);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Headers
|
|
507
|
+
Napi::Object headers = Napi::Object::New(env);
|
|
508
|
+
|
|
509
|
+
size_t cursor = lineEnd == std::string_view::npos ? head.size() : lineEnd + 2;
|
|
510
|
+
while (cursor < head.size()) {
|
|
511
|
+
size_t next = head.find("\r\n", cursor);
|
|
512
|
+
std::string_view line = (next == std::string_view::npos)
|
|
513
|
+
? head.substr(cursor)
|
|
514
|
+
: head.substr(cursor, next - cursor);
|
|
515
|
+
|
|
516
|
+
cursor = (next == std::string_view::npos) ? head.size() : next + 2;
|
|
517
|
+
|
|
518
|
+
line = StripCR(line);
|
|
519
|
+
if (line.empty()) continue;
|
|
520
|
+
|
|
521
|
+
size_t colon = line.find(':');
|
|
522
|
+
if (colon == std::string_view::npos) {
|
|
523
|
+
Napi::Error::New(env, "Invalid HTTP header line").ThrowAsJavaScriptException();
|
|
524
|
+
return env.Null();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
std::string_view key = TrimView(line.substr(0, colon));
|
|
528
|
+
std::string_view value = TrimView(line.substr(colon + 1));
|
|
529
|
+
|
|
530
|
+
if (key.empty()) {
|
|
531
|
+
Napi::Error::New(env, "Invalid HTTP header key").ThrowAsJavaScriptException();
|
|
532
|
+
return env.Null();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
AddHeaderValue(env, headers, ToLowerAscii(key), value);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Body
|
|
539
|
+
bool chunked = HeaderContains(headers, "transfer-encoding", "chunked");
|
|
540
|
+
std::string decodedBody;
|
|
541
|
+
|
|
542
|
+
if (chunked) {
|
|
543
|
+
try {
|
|
544
|
+
decodedBody = DecodeChunkedBody(rawBodyView);
|
|
545
|
+
} catch (const std::exception& e) {
|
|
546
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
547
|
+
return env.Null();
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
decodedBody.assign(rawBodyView.data(), rawBodyView.size());
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
std::string contentType = GetHeaderSingle(headers, "content-type");
|
|
554
|
+
Napi::Value parsedBody = Napi::String::New(env, decodedBody);
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
if (!contentType.empty()) {
|
|
558
|
+
parsedBody = ParseBodyByContentType(env, contentType, decodedBody);
|
|
559
|
+
}
|
|
560
|
+
} catch (...) {
|
|
561
|
+
parsedBody = Napi::String::New(env, decodedBody);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Query params
|
|
565
|
+
Napi::Object queryParams = Napi::Object::New(env);
|
|
566
|
+
if (!query.empty()) {
|
|
567
|
+
std::string_view q(query);
|
|
568
|
+
size_t start = 0;
|
|
569
|
+
|
|
570
|
+
while (start <= q.size()) {
|
|
571
|
+
size_t amp = q.find('&', start);
|
|
572
|
+
std::string_view pair = (amp == std::string_view::npos)
|
|
573
|
+
? q.substr(start)
|
|
574
|
+
: q.substr(start, amp - start);
|
|
575
|
+
|
|
576
|
+
if (!pair.empty()) {
|
|
577
|
+
size_t eq = pair.find('=');
|
|
578
|
+
std::string key = UrlDecode(eq == std::string_view::npos ? pair : pair.substr(0, eq));
|
|
579
|
+
std::string value = UrlDecode(eq == std::string_view::npos ? "" : pair.substr(eq + 1));
|
|
580
|
+
queryParams.Set(key, value);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (amp == std::string_view::npos) break;
|
|
584
|
+
start = amp + 1;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
bool keepAlive = ShouldKeepAlive(httpVersion, headers);
|
|
589
|
+
|
|
590
|
+
Napi::Object req = Napi::Object::New(env);
|
|
591
|
+
req.Set("ok", true);
|
|
592
|
+
req.Set("method", method);
|
|
593
|
+
req.Set("fullPath", fullPath);
|
|
594
|
+
req.Set("pathname", pathname);
|
|
595
|
+
req.Set("query", query);
|
|
596
|
+
req.Set("queryParams", queryParams);
|
|
597
|
+
req.Set("httpVersion", httpVersion);
|
|
598
|
+
req.Set("headers", headers);
|
|
599
|
+
req.Set("rawBody", std::string(rawBodyView));
|
|
600
|
+
req.Set("body", decodedBody);
|
|
601
|
+
req.Set("parsedBody", parsedBody);
|
|
602
|
+
req.Set("chunked", chunked);
|
|
603
|
+
req.Set("keepAlive", keepAlive);
|
|
604
|
+
req.Set("contentType", contentType);
|
|
605
|
+
req.Set("contentLength", static_cast<double>(decodedBody.size()));
|
|
606
|
+
req.Set("raw", raw);
|
|
607
|
+
|
|
608
|
+
return req;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
Napi::Value BuildHttpResponse(const Napi::CallbackInfo& info) {
|
|
612
|
+
Napi::Env env = info.Env();
|
|
613
|
+
|
|
614
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
615
|
+
Napi::TypeError::New(env, "buildHttpResponse expects an object").ThrowAsJavaScriptException();
|
|
616
|
+
return env.Null();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
620
|
+
|
|
621
|
+
int status = 200;
|
|
622
|
+
if (opts.Has("status") && opts.Get("status").IsNumber()) {
|
|
623
|
+
status = opts.Get("status").As<Napi::Number>().Int32Value();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
std::string statusText = StatusText(status);
|
|
627
|
+
if (opts.Has("statusText") && opts.Get("statusText").IsString()) {
|
|
628
|
+
statusText = opts.Get("statusText").As<Napi::String>().Utf8Value();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
Napi::Object headers = Napi::Object::New(env);
|
|
632
|
+
if (opts.Has("headers") && opts.Get("headers").IsObject()) {
|
|
633
|
+
Napi::Object inHeaders = opts.Get("headers").As<Napi::Object>();
|
|
634
|
+
Napi::Array props = inHeaders.GetPropertyNames();
|
|
635
|
+
|
|
636
|
+
for (uint32_t i = 0; i < props.Length(); ++i) {
|
|
637
|
+
std::string key = ToLowerAscii(props.Get(i).ToString().Utf8Value());
|
|
638
|
+
Napi::Value val = inHeaders.Get(props.Get(i));
|
|
639
|
+
|
|
640
|
+
if (val.IsArray()) {
|
|
641
|
+
Napi::Array arr = val.As<Napi::Array>();
|
|
642
|
+
for (uint32_t j = 0; j < arr.Length(); ++j) {
|
|
643
|
+
AddHeaderValue(env, headers, key, arr.Get(j).ToString().Utf8Value());
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
AddHeaderValue(env, headers, key, val.ToString().Utf8Value());
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
std::string body;
|
|
652
|
+
bool jsonLike = false;
|
|
653
|
+
if (opts.Has("body")) {
|
|
654
|
+
body = ValueToStringLike(opts.Get("body"), env, jsonLike);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
bool keepAlive = true;
|
|
658
|
+
if (opts.Has("keepAlive") && opts.Get("keepAlive").IsBoolean()) {
|
|
659
|
+
keepAlive = opts.Get("keepAlive").ToBoolean().Value();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!headers.Has("content-type")) {
|
|
663
|
+
headers.Set("content-type", jsonLike ? "application/json; charset=utf-8" : "text/plain; charset=utf-8");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
headers.Set("content-length", std::to_string(body.size()));
|
|
667
|
+
headers.Set("connection", keepAlive ? "keep-alive" : "close");
|
|
668
|
+
|
|
669
|
+
std::ostringstream out;
|
|
670
|
+
out << "HTTP/1.1 " << status << " " << statusText << "\r\n";
|
|
671
|
+
|
|
672
|
+
Napi::Array names = headers.GetPropertyNames();
|
|
673
|
+
for (uint32_t i = 0; i < names.Length(); ++i) {
|
|
674
|
+
Napi::Value nameV = names.Get(i);
|
|
675
|
+
std::string key = nameV.ToString().Utf8Value();
|
|
676
|
+
Napi::Value val = headers.Get(nameV);
|
|
677
|
+
|
|
678
|
+
if (val.IsArray()) {
|
|
679
|
+
Napi::Array arr = val.As<Napi::Array>();
|
|
680
|
+
for (uint32_t j = 0; j < arr.Length(); ++j) {
|
|
681
|
+
out << key << ": " << arr.Get(j).ToString().Utf8Value() << "\r\n";
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
out << key << ": " << val.ToString().Utf8Value() << "\r\n";
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
out << "\r\n";
|
|
689
|
+
out << body;
|
|
690
|
+
|
|
691
|
+
return Napi::String::New(env, out.str());
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
Napi::Value IsKeepAlive(const Napi::CallbackInfo& info) {
|
|
695
|
+
Napi::Env env = info.Env();
|
|
696
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsObject()) {
|
|
697
|
+
Napi::TypeError::New(env, "isKeepAlive expects (httpVersion, headers)").ThrowAsJavaScriptException();
|
|
698
|
+
return env.Null();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
std::string httpVersion = info[0].As<Napi::String>().Utf8Value();
|
|
702
|
+
Napi::Object headers = info[1].As<Napi::Object>();
|
|
703
|
+
return Napi::Boolean::New(env, ShouldKeepAlive(httpVersion, headers));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
Napi::Value DecodeChunked(const Napi::CallbackInfo& info) {
|
|
707
|
+
Napi::Env env = info.Env();
|
|
708
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
709
|
+
Napi::TypeError::New(env, "decodeChunked expects a string").ThrowAsJavaScriptException();
|
|
710
|
+
return env.Null();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
std::string raw = info[0].As<Napi::String>().Utf8Value();
|
|
715
|
+
std::string_view view(raw);
|
|
716
|
+
std::string decoded = DecodeChunkedBody(view);
|
|
717
|
+
return Napi::String::New(env, decoded);
|
|
718
|
+
} catch (const std::exception& e) {
|
|
719
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
720
|
+
return env.Null();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
Napi::Object InitImpl(Napi::Env env, Napi::Object exports) {
|
|
725
|
+
exports.Set("hello", Napi::Function::New(env, Hello));
|
|
726
|
+
exports.Set("parseHttpRequest", Napi::Function::New(env, ParseHttpRequest));
|
|
727
|
+
exports.Set("buildHttpResponse", Napi::Function::New(env, BuildHttpResponse));
|
|
728
|
+
exports.Set("isKeepAlive", Napi::Function::New(env, IsKeepAlive));
|
|
729
|
+
exports.Set("decodeChunked", Napi::Function::New(env, DecodeChunked));
|
|
730
|
+
exports.Set("setLogFile", Napi::Function::New(env, SetLogFile));
|
|
731
|
+
exports.Set("log", Napi::Function::New(env, Log));
|
|
732
|
+
exports.Set("getMetrics", Napi::Function::New(env, GetMetrics));
|
|
733
|
+
return exports;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
} // namespace nitro5
|
|
737
|
+
|
|
738
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
739
|
+
return nitro5::InitImpl(env, exports);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
NODE_API_MODULE(nitro5, Init)
|