rtjscomp 0.9.2 → 0.9.3
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/package.json +6 -6
- package/rtjscomp.js +39 -1676
- package/changes_before_git.txt +0 -58
package/rtjscomp.js
CHANGED
|
@@ -1,1678 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
"use strict";/*
|
|
3
|
+
RTJSCOMP by L3P3, 2017-2025
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const WATCH_OPTIONS = {persistent: true, interval: 1000};
|
|
43
|
-
|
|
44
|
-
// config
|
|
45
|
-
const log_verbose_flag = process.argv.includes('-v');
|
|
46
|
-
let log_verbose = log_verbose_flag;
|
|
47
|
-
let port_http = 0;
|
|
48
|
-
let port_https = 0;
|
|
49
|
-
let gz_enabled = true;
|
|
50
|
-
let exiting = false;
|
|
51
|
-
/// any path -> file
|
|
52
|
-
const path_aliases = new Map([
|
|
53
|
-
['', 'index.html'],
|
|
54
|
-
]);
|
|
55
|
-
const path_aliases_reverse = new Map([
|
|
56
|
-
['index.html', '/'],
|
|
57
|
-
]);
|
|
58
|
-
const path_aliases_templates = new Map;
|
|
59
|
-
/// files where requests should be totally ignored
|
|
60
|
-
const path_ghosts = new Set;
|
|
61
|
-
/// hidden files
|
|
62
|
-
const path_hiddens = new Set;
|
|
63
|
-
/// forced static files
|
|
64
|
-
const path_statics = new Set;
|
|
65
|
-
const type_dynamics = new Set([
|
|
66
|
-
'events',
|
|
67
|
-
'html',
|
|
68
|
-
'json',
|
|
69
|
-
'txt',
|
|
70
|
-
]);
|
|
71
|
-
const type_mimes = new Map([
|
|
72
|
-
['apk', 'application/zip'],
|
|
73
|
-
['bpg', 'image/bpg'],
|
|
74
|
-
['css', 'text/css; charset=utf-8'],
|
|
75
|
-
['events', 'text/event-stream'],
|
|
76
|
-
['flac', 'audio/flac'],
|
|
77
|
-
['gz', 'application/gzip'],
|
|
78
|
-
['hta', 'application/hta'],
|
|
79
|
-
['html', 'text/html; charset=utf-8'],
|
|
80
|
-
['ico', 'image/x-icon'],
|
|
81
|
-
['jpg', 'image/jpeg'],
|
|
82
|
-
['js', 'text/javascript; charset=utf-8'],
|
|
83
|
-
['json', 'application/json; charset=utf-8'],
|
|
84
|
-
['mid', 'audio/midi'],
|
|
85
|
-
['mp3', 'audio/mpeg3'],
|
|
86
|
-
['pdf', 'application/pdf'],
|
|
87
|
-
['png', 'image/png'],
|
|
88
|
-
['rss', 'application/rss+xml; charset=utf-8'],
|
|
89
|
-
['txt', 'text/plain; charset=utf-8'],
|
|
90
|
-
['xml', 'application/xml; charset=utf-8'],
|
|
91
|
-
['xz', 'application/x-xz'],
|
|
92
|
-
['zip', 'application/zip'],
|
|
93
|
-
]);
|
|
94
|
-
const type_raws = new Set([
|
|
95
|
-
'apk',
|
|
96
|
-
'bpg',
|
|
97
|
-
'flac',
|
|
98
|
-
'gz',
|
|
99
|
-
'jpg',
|
|
100
|
-
'mp3',
|
|
101
|
-
'pdf',
|
|
102
|
-
'png',
|
|
103
|
-
'xz',
|
|
104
|
-
'zip',
|
|
105
|
-
]);
|
|
106
|
-
|
|
107
|
-
/// compiled file handlers
|
|
108
|
-
const file_cache_functions = new Map;
|
|
109
|
-
|
|
110
|
-
const actions = {};
|
|
111
|
-
const rtjscomp = global.rtjscomp = {
|
|
112
|
-
actions,
|
|
113
|
-
version: VERSION,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// polyfills
|
|
117
|
-
if (!Object.fromEntries) {
|
|
118
|
-
Object.fromEntries = entries => {
|
|
119
|
-
const object = {};
|
|
120
|
-
for (const entry of entries) object[entry[0]] = entry[1];
|
|
121
|
-
return object;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// workaround for bun: https://github.com/oven-sh/bun/issues/18919
|
|
126
|
-
let fs_watch = fs.watch;
|
|
127
|
-
if (IS_BUN) {
|
|
128
|
-
const fs_watch_original = fs_watch;
|
|
129
|
-
const watch_callbacks = new Map;
|
|
130
|
-
fs_watch = (path, options, callback) => {
|
|
131
|
-
if (!watch_callbacks.has(path)) {
|
|
132
|
-
fs_watch_original(path, options, () => {
|
|
133
|
-
const callback = watch_callbacks.get(path);
|
|
134
|
-
if (callback) {
|
|
135
|
-
callback();
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
watch_callbacks.set(path, callback);
|
|
140
|
-
return {
|
|
141
|
-
close: () => (
|
|
142
|
-
watch_callbacks.set(path, null)
|
|
143
|
-
),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// legacy, will be removed soon!
|
|
149
|
-
global.globals = rtjscomp;
|
|
150
|
-
global.actions = rtjscomp.actions;
|
|
151
|
-
global.data_load = name => {
|
|
152
|
-
log('[deprecated] synchronous load file: ' + PATH_DATA + name);
|
|
153
|
-
try {
|
|
154
|
-
return fs.readFileSync(PATH_DATA + name, 'utf8');
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
global.data_save = (name, data) => (
|
|
161
|
-
log('[deprecated] synchronous save file: ' + PATH_DATA + name),
|
|
162
|
-
fs.writeFileSync(PATH_DATA + name, data, 'utf8')
|
|
163
|
-
)
|
|
164
|
-
global.number_check_int = number => (
|
|
165
|
-
Math.floor(number) === number
|
|
166
|
-
)
|
|
167
|
-
global.number_check_uint = number => (
|
|
168
|
-
number >= 0 && number_check_int(number)
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
rtjscomp.data_load = async name => {
|
|
172
|
-
if (log_verbose) log('load file: ' + PATH_DATA + name);
|
|
173
|
-
const data = await fsp.readFile(PATH_DATA + name, 'utf8').catch(() => null);
|
|
174
|
-
return (
|
|
175
|
-
name.endsWith('.json')
|
|
176
|
-
? JSON.parse(data || null)
|
|
177
|
-
: data
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
rtjscomp.data_load_watch = (name, callback) => (
|
|
181
|
-
file_keep_new(PATH_DATA + name, data => (
|
|
182
|
-
callback(
|
|
183
|
-
name.endsWith('.json')
|
|
184
|
-
? JSON.parse(data || null)
|
|
185
|
-
: data
|
|
186
|
-
)
|
|
187
|
-
))
|
|
188
|
-
)
|
|
189
|
-
rtjscomp.data_save = (name, data) => (
|
|
190
|
-
log_verbose && log('save file: ' + PATH_DATA + name),
|
|
191
|
-
fsp.writeFile(
|
|
192
|
-
PATH_DATA + name,
|
|
193
|
-
(
|
|
194
|
-
name.endsWith('.json')
|
|
195
|
-
? JSON.stringify(data)
|
|
196
|
-
: data
|
|
197
|
-
),
|
|
198
|
-
'utf8'
|
|
199
|
-
)
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
hack to guess the line number of an error
|
|
204
|
-
*/
|
|
205
|
-
const linenumber_try = err => {
|
|
206
|
-
try {
|
|
207
|
-
return `:${
|
|
208
|
-
err.stack
|
|
209
|
-
.split('\n', 2)[1]
|
|
210
|
-
.split(',').pop()
|
|
211
|
-
.match(LINENUMBER_REG)[1]
|
|
212
|
-
- 2
|
|
213
|
-
}`;
|
|
214
|
-
}
|
|
215
|
-
catch (_) {
|
|
216
|
-
return '';
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const custom_require_paths = new Set;
|
|
221
|
-
const custom_require_cache = new Map;
|
|
222
|
-
const custom_import_cache = new Map;
|
|
223
|
-
const custom_require = path => {
|
|
224
|
-
let result = custom_require_cache.get(path);
|
|
225
|
-
if (result != null) return result;
|
|
226
|
-
|
|
227
|
-
log_verbose && log('require module: ' + path);
|
|
228
|
-
const path_real = require.resolve(path, RESOLVE_OPTIONS);
|
|
229
|
-
custom_require_paths.add(path_real);
|
|
230
|
-
custom_require_cache.set(
|
|
231
|
-
path,
|
|
232
|
-
result = require(path_real)
|
|
233
|
-
);
|
|
234
|
-
return result;
|
|
235
|
-
}
|
|
236
|
-
const custom_import = async path => {
|
|
237
|
-
let result = custom_import_cache.get(path);
|
|
238
|
-
if (result != null) return result;
|
|
239
|
-
|
|
240
|
-
log_verbose && log('import module: ' + path);
|
|
241
|
-
custom_import_cache.set(
|
|
242
|
-
path,
|
|
243
|
-
result = await import(
|
|
244
|
-
'file://' + require.resolve(path, RESOLVE_OPTIONS)
|
|
245
|
-
)
|
|
246
|
-
);
|
|
247
|
-
return result;
|
|
248
|
-
}
|
|
249
|
-
actions.module_cache_clear = () => {
|
|
250
|
-
for (const path of custom_require_paths) {
|
|
251
|
-
delete require.cache[path];
|
|
252
|
-
}
|
|
253
|
-
custom_require_cache.clear();
|
|
254
|
-
custom_import_cache.clear();
|
|
255
|
-
}
|
|
256
|
-
const AsyncFunction = custom_import.constructor;
|
|
257
|
-
|
|
258
|
-
const services = new Map;
|
|
259
|
-
let services_loaded_promise = null;
|
|
260
|
-
let services_loaded_promise_resolve = null;
|
|
261
|
-
/**
|
|
262
|
-
stop/start services according to list
|
|
263
|
-
*/
|
|
264
|
-
const services_list_react = async list => {
|
|
265
|
-
// stop all services not in list
|
|
266
|
-
await Promise.all(
|
|
267
|
-
[...services.values()]
|
|
268
|
-
.filter(service_object => (
|
|
269
|
-
service_object.status < SERVICE_STATUS_STOPPING &&
|
|
270
|
-
!list.includes(service_object.path)
|
|
271
|
-
))
|
|
272
|
-
// so they will not be restarted
|
|
273
|
-
.map(service_object => (
|
|
274
|
-
service_object.dependencies = null,
|
|
275
|
-
service_object
|
|
276
|
-
))
|
|
277
|
-
.map(service_stop)
|
|
278
|
-
);
|
|
279
|
-
// start all services in list
|
|
280
|
-
const start_queue = [];
|
|
281
|
-
for (const path of list) {
|
|
282
|
-
let service_object = services.get(path);
|
|
283
|
-
if (service_object == null) {
|
|
284
|
-
services.set(path, service_object = {
|
|
285
|
-
content: null,
|
|
286
|
-
dependencies: null,
|
|
287
|
-
dependencies_paths: null,
|
|
288
|
-
file_function: null,
|
|
289
|
-
handler_stop: null,
|
|
290
|
-
path,
|
|
291
|
-
promise_deps: null,
|
|
292
|
-
promise_deps_resolve: null,
|
|
293
|
-
promise_stopped: null,
|
|
294
|
-
promise_stopped_resolve: null,
|
|
295
|
-
status: SERVICE_STATUS_PENDING,
|
|
296
|
-
watcher: null,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
else if (service_object.status < SERVICE_STATUS_STOPPING) continue;
|
|
300
|
-
start_queue.push(service_object);
|
|
301
|
-
}
|
|
302
|
-
await Promise.all(
|
|
303
|
-
start_queue.map(service_start)
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
const services_shutdown = () => (
|
|
307
|
-
Promise.all(
|
|
308
|
-
[...services.values()]
|
|
309
|
-
// so they will not be restarted
|
|
310
|
-
.map(service_object => (
|
|
311
|
-
service_object.dependencies = null,
|
|
312
|
-
service_object
|
|
313
|
-
))
|
|
314
|
-
.map(service_stop)
|
|
315
|
-
)
|
|
316
|
-
)
|
|
317
|
-
/**
|
|
318
|
-
(re)start service
|
|
319
|
-
*/
|
|
320
|
-
const service_start = async service_object => {
|
|
321
|
-
if (!services_loaded_promise) {
|
|
322
|
-
services_loaded_promise = new Promise(resolve => {
|
|
323
|
-
services_loaded_promise_resolve = resolve;
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
const {path} = service_object;
|
|
327
|
-
if (log_verbose) log(path + ': prepare for (re)start');
|
|
328
|
-
let start_interval = 0;
|
|
329
|
-
try {
|
|
330
|
-
// if service is running, stop it
|
|
331
|
-
if (
|
|
332
|
-
service_object.status === SERVICE_STATUS_WAITING ||
|
|
333
|
-
service_object.status === SERVICE_STATUS_STARTING
|
|
334
|
-
) {
|
|
335
|
-
if (log_verbose) log(path + ': abort previous start');
|
|
336
|
-
service_object.status = SERVICE_STATUS_PENDING;
|
|
337
|
-
if (service_object.promise_deps_resolve) {
|
|
338
|
-
service_object.promise_deps_resolve();
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
else if (service_object.status === SERVICE_STATUS_ACTIVE) {
|
|
342
|
-
service_object.status = SERVICE_STATUS_PENDING;
|
|
343
|
-
// restart all depending services
|
|
344
|
-
for (const other of services.values())
|
|
345
|
-
if (
|
|
346
|
-
(
|
|
347
|
-
other.status === SERVICE_STATUS_STARTING ||
|
|
348
|
-
other.status === SERVICE_STATUS_ACTIVE
|
|
349
|
-
) &&
|
|
350
|
-
other.dependencies &&
|
|
351
|
-
other.dependencies.includes(service_object)
|
|
352
|
-
) {
|
|
353
|
-
other.dependencies = null;
|
|
354
|
-
service_start(other);
|
|
355
|
-
}
|
|
356
|
-
service_stop_inner(service_object);
|
|
357
|
-
}
|
|
358
|
-
await service_object.promise_stopped;
|
|
359
|
-
// service is not running, now start it
|
|
360
|
-
if (service_object.status > SERVICE_STATUS_ACTIVE) {
|
|
361
|
-
service_object.status = SERVICE_STATUS_PENDING;
|
|
362
|
-
}
|
|
363
|
-
service_object.promise_stopped = new Promise(resolve => {
|
|
364
|
-
service_object.promise_stopped_resolve = resolve;
|
|
365
|
-
});
|
|
366
|
-
if (!service_object.file_function) {
|
|
367
|
-
const path_real = PATH_PUBLIC + path + '.service.js';
|
|
368
|
-
const file_content = await fsp.readFile(path_real, 'utf8');
|
|
369
|
-
if (service_object.status !== SERVICE_STATUS_PENDING) return;
|
|
370
|
-
if (!service_object.watcher) {
|
|
371
|
-
let timeout = 0;
|
|
372
|
-
service_object.watcher = fs_watch(path_real, WATCH_OPTIONS, () => (
|
|
373
|
-
clearTimeout(timeout),
|
|
374
|
-
timeout = setTimeout(() => (
|
|
375
|
-
log_verbose && log('file updated: ' + path),
|
|
376
|
-
service_object.file_function = null,
|
|
377
|
-
service_start(service_object)
|
|
378
|
-
), 50)
|
|
379
|
-
));
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (file_content.includes('globals.')) {
|
|
383
|
-
log(`[deprecated] ${path}: uses globals object`);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
service_object.dependencies_paths = [];
|
|
387
|
-
for (let [, dep] of file_content.matchAll(SERVICE_REQUIRE_REG)) {
|
|
388
|
-
const first_char = dep.charCodeAt(0);
|
|
389
|
-
const last_char = dep.charCodeAt(dep.length - 1);
|
|
390
|
-
if (
|
|
391
|
-
dep.length <3 ||
|
|
392
|
-
(first_char !== 34 || last_char !== 34) && // "
|
|
393
|
-
(first_char !== 39 || last_char !== 39) // '
|
|
394
|
-
) {
|
|
395
|
-
throw new Error('service_require() needs inline string');
|
|
396
|
-
}
|
|
397
|
-
if (
|
|
398
|
-
!service_object.dependencies_paths.includes(
|
|
399
|
-
dep = dep.slice(1, -1)
|
|
400
|
-
)
|
|
401
|
-
) {
|
|
402
|
-
service_object.dependencies_paths.push(dep);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
service_object.file_function = new AsyncFunction(
|
|
407
|
-
'require',
|
|
408
|
-
'custom_import',
|
|
409
|
-
'service_require',
|
|
410
|
-
'service_require_try',
|
|
411
|
-
`const log=a=>rtjscomp.log(${
|
|
412
|
-
JSON.stringify(path + ': ')
|
|
413
|
-
}+a);${
|
|
414
|
-
file_content
|
|
415
|
-
.replace(IMPORT_REG, 'custom_import(')
|
|
416
|
-
}`
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
service_object.dependencies = [];
|
|
421
|
-
let waiting_needed = false;
|
|
422
|
-
for (const dep_path of service_object.dependencies_paths) {
|
|
423
|
-
const dep = services.get(dep_path);
|
|
424
|
-
if (dep == null) {
|
|
425
|
-
throw new Error('unknown required service: ' + dep_path);
|
|
426
|
-
}
|
|
427
|
-
service_object.dependencies.push(dep);
|
|
428
|
-
waiting_needed = (
|
|
429
|
-
waiting_needed ||
|
|
430
|
-
dep.status !== SERVICE_STATUS_ACTIVE
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
if (waiting_needed) {
|
|
434
|
-
if (log_verbose) log(path + ': wait for dependencies');
|
|
435
|
-
service_object.status = SERVICE_STATUS_WAITING;
|
|
436
|
-
service_object.promise_deps = new Promise(resolve => {
|
|
437
|
-
service_object.promise_deps_resolve = resolve;
|
|
438
|
-
});
|
|
439
|
-
await service_object.promise_deps;
|
|
440
|
-
if (service_object.status !== SERVICE_STATUS_WAITING) return;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
log('start service: ' + path);
|
|
444
|
-
service_object.status = SERVICE_STATUS_STARTING;
|
|
445
|
-
start_interval = setInterval(() => {
|
|
446
|
-
log(`[warning] ${path}: still starting`);
|
|
447
|
-
}, 5e3);
|
|
448
|
-
const content_object = service_object.content = {};
|
|
449
|
-
const result = await service_object.file_function.call(
|
|
450
|
-
content_object,
|
|
451
|
-
custom_require,
|
|
452
|
-
custom_import,
|
|
453
|
-
service_require,
|
|
454
|
-
service_require_try,
|
|
455
|
-
);
|
|
456
|
-
if (service_object.status !== SERVICE_STATUS_STARTING) return;
|
|
457
|
-
if (typeof result === 'function') {
|
|
458
|
-
service_object.handler_stop = result;
|
|
459
|
-
}
|
|
460
|
-
const handler_start = content_object.start;
|
|
461
|
-
if (handler_start) {
|
|
462
|
-
log(`[deprecated] ${path}: has start method`);
|
|
463
|
-
delete content_object.start;
|
|
464
|
-
await handler_start();
|
|
465
|
-
if (service_object.status !== SERVICE_STATUS_STARTING) return;
|
|
466
|
-
}
|
|
467
|
-
if (content_object.stop) {
|
|
468
|
-
log(`[deprecated] ${path}: has stop method`);
|
|
469
|
-
service_object.handler_stop = content_object.stop;
|
|
470
|
-
delete content_object.stop;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (log_verbose) log('started service: ' + path);
|
|
474
|
-
service_object.status = SERVICE_STATUS_ACTIVE;
|
|
475
|
-
}
|
|
476
|
-
catch (err) {
|
|
477
|
-
if (!err instanceof Error) {
|
|
478
|
-
err = new Error(err + '?! wtf');
|
|
479
|
-
}
|
|
480
|
-
log(`[error] ${
|
|
481
|
-
path
|
|
482
|
-
}${
|
|
483
|
-
linenumber_try(err)
|
|
484
|
-
}: ${
|
|
485
|
-
err.message
|
|
486
|
-
}`);
|
|
487
|
-
service_object.status = SERVICE_STATUS_FAILED;
|
|
488
|
-
service_object.dependencies = null;
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
finally {
|
|
492
|
-
clearInterval(start_interval);
|
|
493
|
-
service_object.promise_deps =
|
|
494
|
-
service_object.promise_deps_resolve = null;
|
|
495
|
-
if (service_object.status !== SERVICE_STATUS_ACTIVE) {
|
|
496
|
-
service_object.promise_stopped_resolve();
|
|
497
|
-
services_loaded_promise_try();
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// restart waiting services
|
|
502
|
-
other: for (const other of services.values())
|
|
503
|
-
if (other.status === SERVICE_STATUS_WAITING) {
|
|
504
|
-
for (const dep of other.dependencies)
|
|
505
|
-
if (dep.status !== SERVICE_STATUS_ACTIVE) {
|
|
506
|
-
continue other;
|
|
507
|
-
}
|
|
508
|
-
other.promise_deps_resolve();
|
|
509
|
-
}
|
|
510
|
-
services_loaded_promise_try();
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
check if any loading services are left
|
|
514
|
-
*/
|
|
515
|
-
const services_loaded_promise_try = () => {
|
|
516
|
-
if (!services_loaded_promise) return;
|
|
517
|
-
for (const service_object of services.values())
|
|
518
|
-
if (
|
|
519
|
-
service_object.status < SERVICE_STATUS_ACTIVE
|
|
520
|
-
) return;
|
|
521
|
-
services_loaded_promise_resolve();
|
|
522
|
-
services_loaded_promise =
|
|
523
|
-
services_loaded_promise_resolve = null;
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
stop and forget service
|
|
527
|
-
*/
|
|
528
|
-
const service_stop = async service_object => {
|
|
529
|
-
log('stop service: ' + service_object.path);
|
|
530
|
-
service_object.status = SERVICE_STATUS_STOPPING;
|
|
531
|
-
service_object.file_function = null;
|
|
532
|
-
if (service_object.watcher) {
|
|
533
|
-
service_object.watcher.close();
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
for (const other of services.values())
|
|
537
|
-
if (
|
|
538
|
-
other.dependencies &&
|
|
539
|
-
other.dependencies.includes(service_object)
|
|
540
|
-
) {
|
|
541
|
-
other.dependencies = null;
|
|
542
|
-
if (other.status !== SERVICE_STATUS_STOPPING) {
|
|
543
|
-
service_start(other);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
if (service_object.promise_deps_resolve) {
|
|
548
|
-
service_object.promise_deps_resolve();
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
await service_stop_inner(service_object);
|
|
552
|
-
}
|
|
553
|
-
services.delete(service_object.path);
|
|
554
|
-
if (log_verbose) log('stopped service: ' + service_object.path);
|
|
555
|
-
}
|
|
556
|
-
/**
|
|
557
|
-
stop service so it can be forgot or restarted
|
|
558
|
-
*/
|
|
559
|
-
const service_stop_inner = async service_object => {
|
|
560
|
-
const {
|
|
561
|
-
handler_stop,
|
|
562
|
-
path,
|
|
563
|
-
} = service_object;
|
|
564
|
-
if (handler_stop) {
|
|
565
|
-
service_object.handler_stop = null;
|
|
566
|
-
const stop_interval = setInterval(() => {
|
|
567
|
-
log(`[warning] ${path}: still stopping`);
|
|
568
|
-
}, 1e3);
|
|
569
|
-
try {
|
|
570
|
-
await handler_stop();
|
|
571
|
-
}
|
|
572
|
-
catch (err) {
|
|
573
|
-
log(`[error] ${
|
|
574
|
-
path
|
|
575
|
-
}${
|
|
576
|
-
linenumber_try(err)
|
|
577
|
-
} stop handler: ${
|
|
578
|
-
err.message
|
|
579
|
-
}`);
|
|
580
|
-
service_object.status = SERVICE_STATUS_FAILED;
|
|
581
|
-
}
|
|
582
|
-
clearInterval(stop_interval);
|
|
583
|
-
}
|
|
584
|
-
service_object.promise_stopped_resolve();
|
|
585
|
-
}
|
|
586
|
-
const service_require = path => {
|
|
587
|
-
const service_object = services.get(path);
|
|
588
|
-
if (
|
|
589
|
-
service_object != null &&
|
|
590
|
-
service_object.status === SERVICE_STATUS_ACTIVE
|
|
591
|
-
) return service_object.content;
|
|
592
|
-
throw new Error('service required: ' + path);
|
|
593
|
-
}
|
|
594
|
-
const service_require_try = path => {
|
|
595
|
-
const service_object = services.get(path);
|
|
596
|
-
return (
|
|
597
|
-
service_object != null &&
|
|
598
|
-
service_object.status === SERVICE_STATUS_ACTIVE
|
|
599
|
-
? service_object.content
|
|
600
|
-
: null
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const file_watch_once = (path, callback) => {
|
|
605
|
-
const watcher = fs_watch(path, WATCH_OPTIONS, () => (
|
|
606
|
-
watcher.close(),
|
|
607
|
-
log_verbose && log('file updated: ' + path),
|
|
608
|
-
callback()
|
|
609
|
-
));
|
|
610
|
-
};
|
|
611
|
-
const file_keep_new = async (path, callback) => {
|
|
612
|
-
try {
|
|
613
|
-
const data = await fsp.readFile(path, 'utf8');
|
|
614
|
-
if (log_verbose) log('load file: ' + path);
|
|
615
|
-
await callback(data);
|
|
616
|
-
}
|
|
617
|
-
catch (err) {
|
|
618
|
-
await callback(null);
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
let timeout = 0;
|
|
623
|
-
return fs_watch(path, WATCH_OPTIONS, () => (
|
|
624
|
-
clearTimeout(timeout),
|
|
625
|
-
timeout = setTimeout(() => exiting || (
|
|
626
|
-
log_verbose && log('file updated: ' + path),
|
|
627
|
-
fsp.readFile(path, 'utf8')
|
|
628
|
-
.catch(() => null)
|
|
629
|
-
.then(callback)
|
|
630
|
-
), 50)
|
|
631
|
-
));
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const get_prop_bool = (obj, prop, fallback) => {
|
|
635
|
-
if (
|
|
636
|
-
obj === null ||
|
|
637
|
-
!(prop in obj)
|
|
638
|
-
) return fallback;
|
|
639
|
-
const value = obj[prop];
|
|
640
|
-
if (typeof value !== 'boolean') {
|
|
641
|
-
throw prop + ' must be boolean';
|
|
642
|
-
}
|
|
643
|
-
delete obj[prop];
|
|
644
|
-
return value;
|
|
645
|
-
}
|
|
646
|
-
const get_prop_uint = (obj, prop, fallback) => {
|
|
647
|
-
if (
|
|
648
|
-
obj === null ||
|
|
649
|
-
!(prop in obj)
|
|
650
|
-
) return fallback;
|
|
651
|
-
const value = obj[prop];
|
|
652
|
-
if (
|
|
653
|
-
typeof value !== 'number' ||
|
|
654
|
-
value < 0 ||
|
|
655
|
-
value % 1 > 0
|
|
656
|
-
) {
|
|
657
|
-
throw prop + ' must be positive integer';
|
|
658
|
-
}
|
|
659
|
-
delete obj[prop];
|
|
660
|
-
return value;
|
|
661
|
-
}
|
|
662
|
-
const get_prop_list = (obj, prop) => {
|
|
663
|
-
if (
|
|
664
|
-
obj === null ||
|
|
665
|
-
!(prop in obj)
|
|
666
|
-
) return null;
|
|
667
|
-
const value = obj[prop];
|
|
668
|
-
if (
|
|
669
|
-
typeof value !== 'object' ||
|
|
670
|
-
value.length == null ||
|
|
671
|
-
value.some(item => typeof item !== 'string')
|
|
672
|
-
) {
|
|
673
|
-
throw prop + ' must be array of strings';
|
|
674
|
-
}
|
|
675
|
-
delete obj[prop];
|
|
676
|
-
return value;
|
|
677
|
-
}
|
|
678
|
-
const get_prop_map = (obj, prop) => {
|
|
679
|
-
if (
|
|
680
|
-
obj === null ||
|
|
681
|
-
!(prop in obj)
|
|
682
|
-
) return null;
|
|
683
|
-
let value = obj[prop];
|
|
684
|
-
if (
|
|
685
|
-
typeof value !== 'object' ||
|
|
686
|
-
(
|
|
687
|
-
value = Object.entries(value)
|
|
688
|
-
).some(([_, item]) => typeof item !== 'string')
|
|
689
|
-
) {
|
|
690
|
-
throw prop + ' must be object of strings';
|
|
691
|
-
}
|
|
692
|
-
delete obj[prop];
|
|
693
|
-
return value;
|
|
694
|
-
}
|
|
695
|
-
const parse_old_list = data => (
|
|
696
|
-
data
|
|
697
|
-
.split('\n')
|
|
698
|
-
.filter(entry =>
|
|
699
|
-
entry.length > 0 &&
|
|
700
|
-
entry.charCodeAt(0) !== 35
|
|
701
|
-
)
|
|
702
|
-
)
|
|
703
|
-
const parse_old_map = data => (
|
|
704
|
-
Object.fromEntries(
|
|
705
|
-
data
|
|
706
|
-
.split('\n')
|
|
707
|
-
.filter(entry =>
|
|
708
|
-
entry.length > 0 &&
|
|
709
|
-
entry.charCodeAt(0) !== 35
|
|
710
|
-
)
|
|
711
|
-
.map(entry => entry.split(':'))
|
|
712
|
-
)
|
|
713
|
-
)
|
|
714
|
-
const config_path_check = (path, allow_empty = false) => {
|
|
715
|
-
if (!allow_empty && !path) {
|
|
716
|
-
throw 'path is empty';
|
|
717
|
-
}
|
|
718
|
-
if (path.charCodeAt(0) === 47) {
|
|
719
|
-
throw 'path must not start with /';
|
|
720
|
-
}
|
|
721
|
-
if (path.charCodeAt(path.length - 1) === 47) {
|
|
722
|
-
throw 'path must not end with /';
|
|
723
|
-
}
|
|
724
|
-
if (path.includes('..')) {
|
|
725
|
-
throw 'path must not contain ..';
|
|
726
|
-
}
|
|
727
|
-
if (path.includes('~')) {
|
|
728
|
-
throw 'path must not contain ~';
|
|
729
|
-
}
|
|
730
|
-
if (path.includes('//')) {
|
|
731
|
-
throw 'path must not contain //';
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
let log_history = rtjscomp.log_history = [];
|
|
736
|
-
actions.log_clear = () => {
|
|
737
|
-
log_history = rtjscomp.log_history = [];
|
|
738
|
-
}
|
|
739
|
-
const log = rtjscomp.log = msg => (
|
|
740
|
-
console.log(msg),
|
|
741
|
-
log_history.push(msg),
|
|
742
|
-
spam_enabled ? spam('log', [msg]) : undefined
|
|
743
|
-
)
|
|
744
|
-
|
|
745
|
-
const spam_enabled = fs.existsSync('spam.csv');
|
|
746
|
-
rtjscomp.spam_history = '';
|
|
747
|
-
actions.spam_save = async (muted = false) => {
|
|
748
|
-
if (!spam_enabled) return;
|
|
749
|
-
|
|
750
|
-
try {
|
|
751
|
-
const tmp = rtjscomp.spam_history;
|
|
752
|
-
rtjscomp.spam_history = '';
|
|
753
|
-
await fsp.appendFile('spam.csv', tmp, 'utf8');
|
|
754
|
-
if (log_verbose && !muted) log('spam.csv saved');
|
|
755
|
-
}
|
|
756
|
-
catch (err) {
|
|
757
|
-
log('[error] cannot save spam.csv: ' + err.message);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
const spam = (type, data) => {
|
|
761
|
-
rtjscomp.spam_history += (
|
|
762
|
-
Date.now() +
|
|
763
|
-
',' +
|
|
764
|
-
type +
|
|
765
|
-
',' +
|
|
766
|
-
JSON.stringify(data) +
|
|
767
|
-
'\n'
|
|
768
|
-
);
|
|
769
|
-
|
|
770
|
-
if (rtjscomp.spam_history.length >= 1e5) {
|
|
771
|
-
actions.spam_save();
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const request_handle = async (request, response, https) => {
|
|
776
|
-
const request_method = request.method;
|
|
777
|
-
const request_headers = request.headers;
|
|
778
|
-
const request_ip = request_ip_get(request).clientIp;
|
|
779
|
-
|
|
780
|
-
if ('x-forwarded-proto' in request_headers) {
|
|
781
|
-
https = request_headers['x-forwarded-proto'] === 'https';
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (spam_enabled) spam('request', [https, request.url, request_ip]);
|
|
785
|
-
|
|
786
|
-
try {
|
|
787
|
-
const request_url_parsed = url.parse(request.url, false);
|
|
788
|
-
|
|
789
|
-
let path = request_url_parsed.pathname || '/';
|
|
790
|
-
if (
|
|
791
|
-
path.charCodeAt(0) !== 47 ||
|
|
792
|
-
path.includes('//')
|
|
793
|
-
) throw 404;
|
|
794
|
-
if (path.length > 1 && path.endsWith('/')) {
|
|
795
|
-
path = path.slice(0, -1);
|
|
796
|
-
// is file with extension?
|
|
797
|
-
if (path.lastIndexOf('/') < path.lastIndexOf('.')) {
|
|
798
|
-
response.setHeader('Location', path);
|
|
799
|
-
throw 301;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
path = path.slice(1);
|
|
803
|
-
|
|
804
|
-
// ignore (timeout) many hack attempts
|
|
805
|
-
if (
|
|
806
|
-
path.includes('php') ||
|
|
807
|
-
path.includes('sql') ||
|
|
808
|
-
path.includes('.git/') ||
|
|
809
|
-
path_ghosts.has(path)
|
|
810
|
-
) return;
|
|
811
|
-
|
|
812
|
-
response.setHeader('Server', 'l3p3 rtjscomp v' + VERSION);
|
|
813
|
-
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
814
|
-
|
|
815
|
-
let path_params = null;
|
|
816
|
-
let request_body_promise = null;
|
|
817
|
-
|
|
818
|
-
if (path_aliases_reverse.has(path)) {
|
|
819
|
-
response.setHeader('Location', path_aliases_reverse.get(path));
|
|
820
|
-
throw 301;
|
|
821
|
-
}
|
|
822
|
-
if (path_aliases.has(path)) {
|
|
823
|
-
path = path_aliases.get(path)
|
|
824
|
-
}
|
|
825
|
-
else { // aliases with *
|
|
826
|
-
const path_split = path.split('/');
|
|
827
|
-
const templates = path_aliases_templates.get(
|
|
828
|
-
path_split.shift()
|
|
829
|
-
);
|
|
830
|
-
if (templates != null)
|
|
831
|
-
template: for (const template of templates) {
|
|
832
|
-
const template_path = template.path_split;
|
|
833
|
-
const template_path_length = template_path.length;
|
|
834
|
-
if (template_path_length !== path_split.length) continue;
|
|
835
|
-
const params = {};
|
|
836
|
-
for (let i = 0; i < template_path_length; ++i) {
|
|
837
|
-
if (template_path[i].charCodeAt(0) === 42) {
|
|
838
|
-
if (template_path[i].length > 1) {
|
|
839
|
-
params[
|
|
840
|
-
template_path[i].slice(1)
|
|
841
|
-
] = path_split[i];
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
else if (template_path[i] !== path_split[i]) {
|
|
845
|
-
continue template;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
path = template.value;
|
|
849
|
-
path_params = params;
|
|
850
|
-
break;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
const file_type_index = path.lastIndexOf('.');
|
|
855
|
-
if (
|
|
856
|
-
path.includes('..') ||
|
|
857
|
-
path.includes('~') ||
|
|
858
|
-
// no type ending -> dir?
|
|
859
|
-
file_type_index <= path.lastIndexOf('/')
|
|
860
|
-
) throw 404;
|
|
861
|
-
const file_type = path.slice(
|
|
862
|
-
file_type_index + 1
|
|
863
|
-
).toLowerCase();
|
|
864
|
-
|
|
865
|
-
let file_gz_enabled = (
|
|
866
|
-
gz_enabled &&
|
|
867
|
-
'accept-encoding' in request_headers &&
|
|
868
|
-
!type_raws.has(file_type) &&
|
|
869
|
-
request_headers['accept-encoding'].split(HTTP_LIST_REG).includes('gzip')
|
|
870
|
-
);
|
|
871
|
-
|
|
872
|
-
const file_dyn_enabled = (
|
|
873
|
-
type_dynamics.has(file_type) &&
|
|
874
|
-
!path_statics.has(path)
|
|
875
|
-
);
|
|
876
|
-
|
|
877
|
-
if (request_method !== 'GET') {
|
|
878
|
-
if (!file_dyn_enabled) throw 405;
|
|
879
|
-
if ('content-length' in request_headers) {
|
|
880
|
-
request_body_promise = new Promise(resolve => {
|
|
881
|
-
const request_body_chunks = [];
|
|
882
|
-
request.on('data', chunk => {
|
|
883
|
-
request_body_chunks.push(chunk);
|
|
884
|
-
});
|
|
885
|
-
request.on('end', chunk => {
|
|
886
|
-
chunk && request_body_chunks.push(chunk);
|
|
887
|
-
resolve(Buffer.concat(request_body_chunks));
|
|
888
|
-
});
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
let file_function = null;
|
|
894
|
-
let file_stat = null;
|
|
895
|
-
const path_real = PATH_PUBLIC + path;
|
|
896
|
-
|
|
897
|
-
if (
|
|
898
|
-
file_dyn_enabled &&
|
|
899
|
-
file_cache_functions.has(path)
|
|
900
|
-
) {
|
|
901
|
-
file_function = file_cache_functions.get(path);
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
if (log_verbose) log(`load ${
|
|
905
|
-
file_dyn_enabled
|
|
906
|
-
? 'dynam'
|
|
907
|
-
: 'stat'
|
|
908
|
-
}ic file: ${path}`);
|
|
909
|
-
|
|
910
|
-
if (
|
|
911
|
-
path_hiddens.has(path) ||
|
|
912
|
-
path.endsWith('.service.js')
|
|
913
|
-
) throw 403;
|
|
914
|
-
if (!fs.existsSync(path_real)) throw 404;
|
|
915
|
-
file_stat = fs.statSync(path_real);
|
|
916
|
-
if (file_stat.isDirectory()) throw 403;
|
|
917
|
-
|
|
918
|
-
if (file_dyn_enabled) { // compile file
|
|
919
|
-
const file_content = await fsp.readFile(path_real, 'utf8');
|
|
920
|
-
try {
|
|
921
|
-
if (file_content.includes('\r')) {
|
|
922
|
-
throw 'illegal line break, must be unix';
|
|
923
|
-
}
|
|
924
|
-
if (file_content.includes('globals.')) {
|
|
925
|
-
log(`[deprecated] ${path}: uses globals object`);
|
|
926
|
-
}
|
|
927
|
-
const file_content_length = file_content.length;
|
|
928
|
-
|
|
929
|
-
let code = `const log=a=>rtjscomp.log(${
|
|
930
|
-
JSON.stringify(path + ': ')
|
|
931
|
-
}+a);`;
|
|
932
|
-
|
|
933
|
-
let section_dynamic = false;
|
|
934
|
-
let index_start = 0;
|
|
935
|
-
let index_end = 0;
|
|
936
|
-
|
|
937
|
-
while (index_end < file_content_length) {
|
|
938
|
-
if (section_dynamic) {
|
|
939
|
-
if (
|
|
940
|
-
(
|
|
941
|
-
index_end = file_content.indexOf(
|
|
942
|
-
'?>',
|
|
943
|
-
// skip `<?`
|
|
944
|
-
index_start = index_end + 2
|
|
945
|
-
)
|
|
946
|
-
) < 0
|
|
947
|
-
) throw '"?>" missing';
|
|
948
|
-
section_dynamic = false;
|
|
949
|
-
// section not empty?
|
|
950
|
-
if (index_start < index_end) {
|
|
951
|
-
// `<?`?
|
|
952
|
-
if (file_content.charCodeAt(index_start) !== 61) {
|
|
953
|
-
code += (
|
|
954
|
-
file_content
|
|
955
|
-
.slice(
|
|
956
|
-
index_start,
|
|
957
|
-
index_end
|
|
958
|
-
)
|
|
959
|
-
.replace(IMPORT_REG, 'custom_import(') +
|
|
960
|
-
';'
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
else { // `<?=`?
|
|
964
|
-
code += `output.write(''+(${
|
|
965
|
-
file_content
|
|
966
|
-
.slice(
|
|
967
|
-
++index_start,
|
|
968
|
-
index_end
|
|
969
|
-
)
|
|
970
|
-
.replace(IMPORT_REG, 'custom_import(')
|
|
971
|
-
}));`;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
// skip `?>`
|
|
975
|
-
index_end += 2;
|
|
976
|
-
}
|
|
977
|
-
else { // static section
|
|
978
|
-
// still something dynamic coming?
|
|
979
|
-
if (
|
|
980
|
-
(
|
|
981
|
-
index_end = file_content.indexOf(
|
|
982
|
-
'<?',
|
|
983
|
-
index_start = index_end
|
|
984
|
-
)
|
|
985
|
-
) > -1
|
|
986
|
-
) {
|
|
987
|
-
section_dynamic = true;
|
|
988
|
-
}
|
|
989
|
-
else {
|
|
990
|
-
index_end = file_content_length;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// section not empty?
|
|
994
|
-
if (index_start < index_end) {
|
|
995
|
-
code += `output.write(${
|
|
996
|
-
JSON.stringify(
|
|
997
|
-
file_content.slice(index_start, index_end)
|
|
998
|
-
)
|
|
999
|
-
});`;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
try {
|
|
1005
|
-
file_function = new AsyncFunction(
|
|
1006
|
-
'input',
|
|
1007
|
-
'output',
|
|
1008
|
-
'request',
|
|
1009
|
-
'response',
|
|
1010
|
-
'require',
|
|
1011
|
-
'custom_import',
|
|
1012
|
-
'service_require',
|
|
1013
|
-
'service_require_try',
|
|
1014
|
-
code
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
catch (err) {
|
|
1018
|
-
throw err.message;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
catch (err) {
|
|
1022
|
-
log(`[error] ${path} compile: ${err}`);
|
|
1023
|
-
throw 500;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
file_cache_functions.set(path, file_function);
|
|
1027
|
-
file_watch_once(path_real, () => (
|
|
1028
|
-
file_cache_functions.delete(path)
|
|
1029
|
-
));
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
response.statusCode = 200;
|
|
1034
|
-
response.setHeader(
|
|
1035
|
-
'Content-Type',
|
|
1036
|
-
type_mimes.get(file_type) || type_mimes.get('txt')
|
|
1037
|
-
);
|
|
1038
|
-
|
|
1039
|
-
if (file_dyn_enabled) { // dynamic file
|
|
1040
|
-
const file_function_input = path_params || {};
|
|
1041
|
-
|
|
1042
|
-
if (request_headers['cookie'])
|
|
1043
|
-
for (let cookie of request_headers['cookie'].split(';')) {
|
|
1044
|
-
cookie = cookie.trim();
|
|
1045
|
-
const index_equ = cookie.indexOf('=');
|
|
1046
|
-
if (index_equ > 0) {
|
|
1047
|
-
file_function_input[
|
|
1048
|
-
cookie
|
|
1049
|
-
.slice(0, index_equ)
|
|
1050
|
-
.trimRight()
|
|
1051
|
-
] = decodeURI(
|
|
1052
|
-
cookie
|
|
1053
|
-
.slice(index_equ + 1)
|
|
1054
|
-
.trimLeft()
|
|
1055
|
-
);
|
|
1056
|
-
}
|
|
1057
|
-
else if (index_equ < 0) {
|
|
1058
|
-
file_function_input[cookie] = undefined;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (request_headers['x-input']) {
|
|
1063
|
-
Object.assign(
|
|
1064
|
-
file_function_input,
|
|
1065
|
-
querystring_parse(request_headers['x-input'])
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
if (request_url_parsed.query) {
|
|
1070
|
-
try {
|
|
1071
|
-
Object.assign(
|
|
1072
|
-
file_function_input,
|
|
1073
|
-
querystring_parse(request_url_parsed.query)
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
catch (err) {
|
|
1077
|
-
log(`[error] ${path} request query: ${err.message}`);
|
|
1078
|
-
throw 400;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
if (request_body_promise) {
|
|
1083
|
-
try {
|
|
1084
|
-
const content_type = request.headers['content-type'] || '';
|
|
1085
|
-
const body_raw = file_function_input['body'] = await request_body_promise;
|
|
1086
|
-
let body = null;
|
|
1087
|
-
switch (content_type.split(';')[0]) {
|
|
1088
|
-
case 'application/x-www-form-urlencoded':
|
|
1089
|
-
body = querystring_parse(body_raw.toString());
|
|
1090
|
-
break;
|
|
1091
|
-
case 'application/json':
|
|
1092
|
-
body = JSON.parse(body_raw.toString());
|
|
1093
|
-
break;
|
|
1094
|
-
case 'multipart/form-data': {
|
|
1095
|
-
body = Object.fromEntries(
|
|
1096
|
-
multipart_parse(
|
|
1097
|
-
body_raw,
|
|
1098
|
-
content_type.split('boundary=')[1].split(';')[0]
|
|
1099
|
-
)
|
|
1100
|
-
.map(({ name, ...value }) => [
|
|
1101
|
-
name,
|
|
1102
|
-
value.type ? value : value.data.toString()
|
|
1103
|
-
])
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
if (body) {
|
|
1108
|
-
Object.assign(file_function_input, body);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
catch (err) {
|
|
1112
|
-
log(`[error] ${path} request body: ${err.message}`);
|
|
1113
|
-
throw 400;
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const request_headers_user_agent = file_function_input['user_agent'] = request_headers['user-agent'];
|
|
1118
|
-
file_function_input['bot'] = AGENT_CHECK_BOT.test(request_headers_user_agent);
|
|
1119
|
-
file_function_input['mobil'] = AGENT_CHECK_MOBIL.test(request_headers_user_agent);
|
|
1120
|
-
|
|
1121
|
-
file_function_input['https'] = https;
|
|
1122
|
-
file_function_input['ip'] = request_ip;
|
|
1123
|
-
file_function_input['method'] = request_method.toLowerCase();
|
|
1124
|
-
file_function_input['path'] = request_url_parsed.pathname;
|
|
1125
|
-
|
|
1126
|
-
let file_function_output;
|
|
1127
|
-
response.setHeader('Cache-Control', 'no-cache, no-store');
|
|
1128
|
-
|
|
1129
|
-
if (file_gz_enabled) {
|
|
1130
|
-
response.setHeader('Content-Encoding', 'gzip');
|
|
1131
|
-
|
|
1132
|
-
(
|
|
1133
|
-
file_function_output = zlib.createGzip(GZIP_OPTIONS)
|
|
1134
|
-
).pipe(response);
|
|
1135
|
-
}
|
|
1136
|
-
else {
|
|
1137
|
-
file_function_output = response;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if (spam_enabled) spam('execute', [
|
|
1141
|
-
path,
|
|
1142
|
-
Object.fromEntries(
|
|
1143
|
-
Object.entries(file_function_input)
|
|
1144
|
-
.filter(e => e[0] !== 'body')
|
|
1145
|
-
.map(e => e[0] === 'password' ? [e[0], '***'] : e)
|
|
1146
|
-
.map(e => e[0] === 'file' ? [e[0], '...'] : e)
|
|
1147
|
-
.map(e => (typeof e[1] === 'object' && !e[1].length) ? [e[0], Object.keys(e[1]).slice(0, 20)] : e)
|
|
1148
|
-
.map(e => (e[0] !== 'user_agent' && typeof e[1] === 'string' && e[1].length > 20) ? [e[0], e[1].slice(0, 20) + '...'] : e)
|
|
1149
|
-
)
|
|
1150
|
-
]);
|
|
1151
|
-
|
|
1152
|
-
await services_loaded_promise;
|
|
1153
|
-
|
|
1154
|
-
let returned;
|
|
1155
|
-
try {
|
|
1156
|
-
returned = await file_function(
|
|
1157
|
-
file_function_input,
|
|
1158
|
-
file_function_output,
|
|
1159
|
-
request,
|
|
1160
|
-
response,
|
|
1161
|
-
custom_require,
|
|
1162
|
-
custom_import,
|
|
1163
|
-
service_require,
|
|
1164
|
-
service_require_try,
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1167
|
-
catch (err) {
|
|
1168
|
-
if (err instanceof Error) {
|
|
1169
|
-
log(`[error] ${path}: ${err.message}`);
|
|
1170
|
-
returned = (
|
|
1171
|
-
err.message.startsWith('service required: ')
|
|
1172
|
-
? 503
|
|
1173
|
-
: 500
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
else if (typeof err === 'number') {
|
|
1177
|
-
log(`[deprecated] ${path}: status code thrown, use return instead`);
|
|
1178
|
-
returned = err;
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
log(`[error] ${path}: invalid throw type: ${typeof err}`);
|
|
1182
|
-
returned = 500;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
if (returned != null) {
|
|
1186
|
-
if (response.headersSent) {
|
|
1187
|
-
if (
|
|
1188
|
-
response.writableEnded != null
|
|
1189
|
-
? !response.writableEnded
|
|
1190
|
-
: !response.finished
|
|
1191
|
-
) {
|
|
1192
|
-
file_function_output.end(`${
|
|
1193
|
-
file_type === 'html'
|
|
1194
|
-
? '<hr>'
|
|
1195
|
-
: '\n\n---\n'
|
|
1196
|
-
}ERROR!`);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
else if (file_gz_enabled) {
|
|
1200
|
-
response.removeHeader('Content-Encoding');
|
|
1201
|
-
}
|
|
1202
|
-
if (typeof returned !== 'number') {
|
|
1203
|
-
log(`[error] ${path}: invalid return type: ${typeof returned}`);
|
|
1204
|
-
throw 500;
|
|
1205
|
-
}
|
|
1206
|
-
if (response.headersSent) {
|
|
1207
|
-
if (returned < 500) {
|
|
1208
|
-
log(`[error] ${path}: status code after content`);
|
|
1209
|
-
throw 500;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
throw returned;
|
|
1213
|
-
}
|
|
1214
|
-
file_function_output.end();
|
|
1215
|
-
}
|
|
1216
|
-
else { // static file
|
|
1217
|
-
let file_data = null;
|
|
1218
|
-
|
|
1219
|
-
if (
|
|
1220
|
-
file_gz_enabled &&
|
|
1221
|
-
file_stat.size > 80 &&
|
|
1222
|
-
fs.existsSync(path_real + '.gz')
|
|
1223
|
-
) {
|
|
1224
|
-
file_data = fs.createReadStream(path_real + '.gz');
|
|
1225
|
-
}
|
|
1226
|
-
else {
|
|
1227
|
-
file_gz_enabled = false;
|
|
1228
|
-
file_data = fs.createReadStream(path_real);
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
if (spam_enabled) spam('static_send', [path, file_gz_enabled]);
|
|
1232
|
-
response.setHeader('Cache-Control', 'public, max-age=600');
|
|
1233
|
-
|
|
1234
|
-
if (file_gz_enabled) {
|
|
1235
|
-
response.setHeader('Content-Encoding', 'gzip');
|
|
1236
|
-
}
|
|
1237
|
-
else {
|
|
1238
|
-
response.setHeader('Content-Length', file_stat.size);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
file_data.pipe(response);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
catch (err) {
|
|
1245
|
-
// catch internal errors
|
|
1246
|
-
if (typeof err !== 'number') {
|
|
1247
|
-
console.error(err);
|
|
1248
|
-
err = 500;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
if (
|
|
1252
|
-
err >= 500 ||
|
|
1253
|
-
log_verbose && err >= 400
|
|
1254
|
-
) {
|
|
1255
|
-
log(`[error] request failed: ${err}; ${request_ip}; ${request.url}`);
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
if (!response.headersSent) {
|
|
1259
|
-
response.writeHead(err, {
|
|
1260
|
-
'Content-Type': 'text/html',
|
|
1261
|
-
'Cache-Control': 'no-cache, no-store',
|
|
1262
|
-
});
|
|
1263
|
-
response.end(`<!DOCTYPE html><html><body><h1>HTTP ${
|
|
1264
|
-
err
|
|
1265
|
-
}: ${
|
|
1266
|
-
http.STATUS_CODES[err] || 'Error'
|
|
1267
|
-
}</h1></body></html>`);
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
actions.halt = async () => {
|
|
1273
|
-
exiting = true;
|
|
1274
|
-
if (log_verbose) log('stop all');
|
|
1275
|
-
await Promise.all([
|
|
1276
|
-
actions.http_stop && actions.http_stop(),
|
|
1277
|
-
actions.https_stop && actions.https_stop(),
|
|
1278
|
-
services_shutdown(),
|
|
1279
|
-
]);
|
|
1280
|
-
await actions.spam_save();
|
|
1281
|
-
log('stopped all');
|
|
1282
|
-
}
|
|
1283
|
-
actions.exit = async status => {
|
|
1284
|
-
if (exiting) return;
|
|
1285
|
-
if (typeof status !== 'number') status = 0;
|
|
1286
|
-
await actions.halt();
|
|
1287
|
-
if (log_verbose) log('exit');
|
|
1288
|
-
process.exit(status);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
process.on('uncaughtException', err => {
|
|
1292
|
-
if (typeof err === 'symbol') err = err.toString();
|
|
1293
|
-
log('[error] uncaughtException: ' + (err.message || err));
|
|
1294
|
-
console.error(err);
|
|
1295
|
-
if (exiting) process.exit(1);
|
|
1296
|
-
actions.exit(1);
|
|
1297
|
-
});
|
|
1298
|
-
process.on('unhandledRejection', err => {
|
|
1299
|
-
log('[error] unhandledRejection: ' + (err.message || err));
|
|
1300
|
-
console.error(err);
|
|
1301
|
-
if (exiting) process.exit(1);
|
|
1302
|
-
actions.exit(1);
|
|
1303
|
-
});
|
|
1304
|
-
process.on('exit', actions.exit);
|
|
1305
|
-
process.on('SIGINT', actions.exit);
|
|
1306
|
-
//process.on('SIGUSR1', actions.exit);
|
|
1307
|
-
process.on('SIGUSR2', actions.exit);
|
|
1308
|
-
process.on('SIGTERM', actions.exit);
|
|
1309
|
-
|
|
1310
|
-
log(`rtjscomp v${
|
|
1311
|
-
VERSION
|
|
1312
|
-
} in ${
|
|
1313
|
-
IS_BUN ? 'bun' : 'node'
|
|
1314
|
-
} on ${
|
|
1315
|
-
process.platform
|
|
1316
|
-
.replace('win32', 'windows')
|
|
1317
|
-
}`);
|
|
1318
|
-
|
|
1319
|
-
await file_keep_new(PATH_CONFIG + 'init.js', async data => {
|
|
1320
|
-
if (!data) return;
|
|
1321
|
-
log('[deprecated] run global init script');
|
|
1322
|
-
try {
|
|
1323
|
-
await (
|
|
1324
|
-
new AsyncFunction('require', data)
|
|
1325
|
-
)(custom_require);
|
|
1326
|
-
}
|
|
1327
|
-
catch (err) {
|
|
1328
|
-
log('[error] init.js: ' + err.message);
|
|
1329
|
-
}
|
|
1330
|
-
});
|
|
1331
|
-
|
|
1332
|
-
// initial
|
|
1333
|
-
{
|
|
1334
|
-
const [
|
|
1335
|
-
stat_data,
|
|
1336
|
-
stat_public,
|
|
1337
|
-
...files_config
|
|
1338
|
-
] = await Promise.all([
|
|
1339
|
-
fsp.stat(PATH_DATA).catch(_ => null),
|
|
1340
|
-
fsp.stat(PATH_PUBLIC).catch(_ => null),
|
|
1341
|
-
...(
|
|
1342
|
-
'file_blocks,file_privates,file_raws,file_type_dyns,file_type_mimes,file_type_nocompress,path_aliases,port_http,port_https,services'
|
|
1343
|
-
.split(',')
|
|
1344
|
-
.map(name => fsp.readFile(PATH_CONFIG + name + '.txt', 'utf8').catch(_ => null))
|
|
1345
|
-
),
|
|
1346
|
-
]);
|
|
1347
|
-
|
|
1348
|
-
if (!stat_data) {
|
|
1349
|
-
fs.mkdirSync(PATH_DATA);
|
|
1350
|
-
}
|
|
1351
|
-
if (!stat_public) {
|
|
1352
|
-
fs.cpSync(
|
|
1353
|
-
__dirname + '/' + PATH_PUBLIC,
|
|
1354
|
-
PATH_PUBLIC,
|
|
1355
|
-
{recursive: true}
|
|
1356
|
-
);
|
|
1357
|
-
}
|
|
1358
|
-
if (
|
|
1359
|
-
files_config.some(file => file !== null)
|
|
1360
|
-
) {
|
|
1361
|
-
const json = JSON.parse(
|
|
1362
|
-
await fsp.readFile('rtjscomp.json', 'utf8').catch(_ => null)
|
|
1363
|
-
) || {};
|
|
1364
|
-
|
|
1365
|
-
const [
|
|
1366
|
-
old_file_blocks,
|
|
1367
|
-
old_file_privates,
|
|
1368
|
-
old_file_raws,
|
|
1369
|
-
old_file_type_dyns,
|
|
1370
|
-
old_file_type_mimes,
|
|
1371
|
-
old_file_type_nocompress,
|
|
1372
|
-
old_path_aliases,
|
|
1373
|
-
old_port_http,
|
|
1374
|
-
old_port_https,
|
|
1375
|
-
old_services,
|
|
1376
|
-
] = files_config;
|
|
1377
|
-
|
|
1378
|
-
if (old_file_blocks) {
|
|
1379
|
-
json['path_ghosts'] = parse_old_list(old_file_blocks);
|
|
1380
|
-
}
|
|
1381
|
-
if (old_file_privates) {
|
|
1382
|
-
json['path_hiddens'] = parse_old_list(old_file_privates);
|
|
1383
|
-
}
|
|
1384
|
-
if (old_file_raws) {
|
|
1385
|
-
json['path_statics'] = parse_old_list(old_file_raws);
|
|
1386
|
-
}
|
|
1387
|
-
if (old_file_type_dyns) {
|
|
1388
|
-
json['type_dynamics'] = parse_old_list(old_file_type_dyns);
|
|
1389
|
-
}
|
|
1390
|
-
if (old_file_type_mimes) {
|
|
1391
|
-
json['type_mimes'] = parse_old_map(old_file_type_mimes);
|
|
1392
|
-
}
|
|
1393
|
-
if (old_file_type_nocompress) {
|
|
1394
|
-
json['type_raws'] = parse_old_list(old_file_type_nocompress);
|
|
1395
|
-
}
|
|
1396
|
-
if (old_path_aliases) {
|
|
1397
|
-
json['path_aliases'] = parse_old_map(old_path_aliases);
|
|
1398
|
-
}
|
|
1399
|
-
if (old_port_http) {
|
|
1400
|
-
const number = parseInt(old_port_http);
|
|
1401
|
-
if (!isNaN(number)) {
|
|
1402
|
-
json['port_http'] = number;
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
if (old_port_https) {
|
|
1406
|
-
const number = parseInt(old_port_https);
|
|
1407
|
-
if (!isNaN(number)) {
|
|
1408
|
-
json['port_https'] = number;
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
if (old_services) {
|
|
1412
|
-
json['services'] = old_services.split('\n').filter(path => path.length > 0);
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
await fsp.writeFile(
|
|
1416
|
-
'rtjscomp.json',
|
|
1417
|
-
JSON.stringify(json, null, '\t') + '\n',
|
|
1418
|
-
'utf8'
|
|
1419
|
-
);
|
|
1420
|
-
log('[deprecated] config files found, rtjscomp.json written, please delete config files');
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// http(s)
|
|
1425
|
-
let connections_count = 0;
|
|
1426
|
-
let http_status_target = false;
|
|
1427
|
-
let http_listened_resolve = null;
|
|
1428
|
-
const http_connections = new Map;
|
|
1429
|
-
const server_http = http.createServer(
|
|
1430
|
-
(request, response) => request_handle(request, response, false)
|
|
1431
|
-
);
|
|
1432
|
-
server_http.on('error', err => {
|
|
1433
|
-
log('[error] http: ' + err.message);
|
|
1434
|
-
http_listened_resolve && http_listened_resolve();
|
|
1435
|
-
});
|
|
1436
|
-
server_http.on('connection', connection => {
|
|
1437
|
-
const id = ++connections_count;
|
|
1438
|
-
http_connections.set(id, connection);
|
|
1439
|
-
connection.on('close', () => {
|
|
1440
|
-
http_connections.delete(id);
|
|
1441
|
-
});
|
|
1442
|
-
});
|
|
1443
|
-
|
|
1444
|
-
actions.http_start = async () => {
|
|
1445
|
-
await actions.http_stop();
|
|
1446
|
-
if (!port_http) return;
|
|
1447
|
-
http_status_target = true;
|
|
1448
|
-
log('start http: http://localhost:' + port_http);
|
|
1449
|
-
await new Promise(resolve => server_http.listen(port_http, http_listened_resolve = resolve));
|
|
1450
|
-
if (http_listened_resolve) http_listened_resolve = null;
|
|
1451
|
-
else if (log_verbose) log('started http');
|
|
1452
|
-
}
|
|
1453
|
-
actions.http_stop = async () => {
|
|
1454
|
-
if (!http_status_target) return;
|
|
1455
|
-
http_status_target = false;
|
|
1456
|
-
log('stop http');
|
|
1457
|
-
const kill_timeout = setTimeout(actions.http_kill, 5e3);
|
|
1458
|
-
await new Promise(resolve => server_http.close(resolve));
|
|
1459
|
-
clearTimeout(kill_timeout);
|
|
1460
|
-
if (log_verbose) log('stopped http');
|
|
1461
|
-
}
|
|
1462
|
-
actions.http_kill = async () => {
|
|
1463
|
-
if (http_status_target) return;
|
|
1464
|
-
log('kill http');
|
|
1465
|
-
await Promise.all(
|
|
1466
|
-
[...http_connections.values()]
|
|
1467
|
-
.map(connection => connection.destroy())
|
|
1468
|
-
);
|
|
1469
|
-
if (log_verbose) log('killed http');
|
|
1470
|
-
http_connections.clear();
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
try {
|
|
1474
|
-
const https_key = fs.readFileSync(PATH_CONFIG + 'ssl/domain.key');
|
|
1475
|
-
const https_cert = fs.readFileSync(PATH_CONFIG + 'ssl/chained.pem');
|
|
1476
|
-
let https_status_target = false;
|
|
1477
|
-
let https_listened_resolve = null;
|
|
1478
|
-
const https_connections = new Map;
|
|
1479
|
-
const server_https = require('https').createServer(
|
|
1480
|
-
{key: https_key, cert: https_cert},
|
|
1481
|
-
(request, response) => request_handle(request, response, true)
|
|
1482
|
-
);
|
|
1483
|
-
server_https.on('error', err => {
|
|
1484
|
-
log('[error] https: ' + err.message);
|
|
1485
|
-
https_listened_resolve && https_listened_resolve();
|
|
1486
|
-
});
|
|
1487
|
-
server_https.on('connection', connection => {
|
|
1488
|
-
const id = ++connections_count;
|
|
1489
|
-
https_connections.set(id, connection);
|
|
1490
|
-
connection.on('close', () => {
|
|
1491
|
-
https_connections.delete(id);
|
|
1492
|
-
});
|
|
1493
|
-
});
|
|
1494
|
-
|
|
1495
|
-
actions.https_start = async () => {
|
|
1496
|
-
await actions.https_stop();
|
|
1497
|
-
if (!port_https) return;
|
|
1498
|
-
https_status_target = true;
|
|
1499
|
-
log('start https: https://localhost:' + port_https);
|
|
1500
|
-
await new Promise(resolve => server_https.listen(port_https, https_listened_resolve = resolve));
|
|
1501
|
-
if (https_listened_resolve) https_listened_resolve = null;
|
|
1502
|
-
else if (log_verbose) log('started https');
|
|
1503
|
-
}
|
|
1504
|
-
actions.https_stop = async () => {
|
|
1505
|
-
if (!https_status_target) return;
|
|
1506
|
-
https_status_target = false;
|
|
1507
|
-
log('stop https');
|
|
1508
|
-
const kill_timeout = setTimeout(actions.https_kill, 5000);
|
|
1509
|
-
await new Promise(resolve => server_https.close(resolve));
|
|
1510
|
-
clearTimeout(kill_timeout);
|
|
1511
|
-
if (log_verbose) log('stopped https');
|
|
1512
|
-
}
|
|
1513
|
-
actions.https_kill = async () => {
|
|
1514
|
-
if (https_status_target) return;
|
|
1515
|
-
log('kill https');
|
|
1516
|
-
await Promise.all(
|
|
1517
|
-
[...https_connections.values()]
|
|
1518
|
-
.map(connection => connection.destroy())
|
|
1519
|
-
);
|
|
1520
|
-
if (log_verbose) log('killed https');
|
|
1521
|
-
https_connections.clear();
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
catch (err) {
|
|
1525
|
-
if (log_verbose) log('https: no cert, disabled');
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
// config
|
|
1529
|
-
await file_keep_new('rtjscomp.json', data => {
|
|
1530
|
-
try {
|
|
1531
|
-
data = JSON.parse(data);
|
|
1532
|
-
if (typeof data !== 'object') {
|
|
1533
|
-
throw 'must contain {}';
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
const gzip_level_new = get_prop_uint(data, 'gzip_level', 9);
|
|
1537
|
-
const log_verbose_new = get_prop_bool(data, 'log_verbose', log_verbose_flag);
|
|
1538
|
-
const path_aliases_new = get_prop_map(data, 'path_aliases');
|
|
1539
|
-
const path_ghosts_new = get_prop_list(data, 'path_ghosts');
|
|
1540
|
-
const path_hiddens_new = get_prop_list(data, 'path_hiddens');
|
|
1541
|
-
const path_statics_new = get_prop_list(data, 'path_statics');
|
|
1542
|
-
const port_http_new = get_prop_uint(data, 'port_http', 8080);
|
|
1543
|
-
const port_https_new = get_prop_uint(data, 'port_https', 0);
|
|
1544
|
-
const services_new = get_prop_list(data, 'services') || [];
|
|
1545
|
-
const type_dynamics_new = get_prop_list(data, 'type_dynamics');
|
|
1546
|
-
const type_mimes_new = get_prop_map(data, 'type_mimes');
|
|
1547
|
-
const type_raws_new = get_prop_list(data, 'type_raws');
|
|
1548
|
-
|
|
1549
|
-
if (data) {
|
|
1550
|
-
const keys_left = Object.keys(data);
|
|
1551
|
-
if (keys_left.length > 0) {
|
|
1552
|
-
throw 'unknown: ' + keys_left.join(', ');
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
if (gzip_level_new > 9) {
|
|
1556
|
-
throw 'gzip_level > 9';
|
|
1557
|
-
}
|
|
1558
|
-
if (
|
|
1559
|
-
port_http_new > 65535 ||
|
|
1560
|
-
port_https_new > 65535
|
|
1561
|
-
) {
|
|
1562
|
-
throw 'port > 65535';
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
gz_enabled = gzip_level_new > 0;
|
|
1566
|
-
GZIP_OPTIONS.level = gzip_level_new;
|
|
1567
|
-
log_verbose = log_verbose_new;
|
|
1568
|
-
if (path_ghosts_new) {
|
|
1569
|
-
path_ghosts.clear();
|
|
1570
|
-
for (const key of path_ghosts_new) {
|
|
1571
|
-
config_path_check(key);
|
|
1572
|
-
path_ghosts.add(key);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
if (path_hiddens_new) {
|
|
1576
|
-
path_hiddens.clear();
|
|
1577
|
-
for (const key of path_hiddens_new) {
|
|
1578
|
-
config_path_check(key);
|
|
1579
|
-
path_hiddens.add(key);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
if (path_statics_new) {
|
|
1583
|
-
path_statics.clear();
|
|
1584
|
-
for (const key of path_statics_new) {
|
|
1585
|
-
config_path_check(key);
|
|
1586
|
-
path_statics.add(key);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
if (path_aliases_new) {
|
|
1590
|
-
path_aliases.clear();
|
|
1591
|
-
path_aliases_reverse.clear();
|
|
1592
|
-
path_aliases_templates.clear();
|
|
1593
|
-
for (const [key, value] of path_aliases_new) {
|
|
1594
|
-
config_path_check(key, true);
|
|
1595
|
-
config_path_check(value);
|
|
1596
|
-
if (key.includes('*')) {
|
|
1597
|
-
const path_split = key.split('/');
|
|
1598
|
-
const first = path_split.shift();
|
|
1599
|
-
if (path_aliases_templates.has(first)) {
|
|
1600
|
-
path_aliases_templates.get(first).push(
|
|
1601
|
-
{path_split, value}
|
|
1602
|
-
);
|
|
1603
|
-
}
|
|
1604
|
-
else {
|
|
1605
|
-
path_aliases_templates.set(first, [
|
|
1606
|
-
{path_split, value},
|
|
1607
|
-
]);
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
else {
|
|
1611
|
-
path_aliases.set(key, value);
|
|
1612
|
-
const existing = path_aliases_reverse.get(value);
|
|
1613
|
-
if (
|
|
1614
|
-
existing == null ||
|
|
1615
|
-
existing.length - 1 > value.length
|
|
1616
|
-
) {
|
|
1617
|
-
path_aliases_reverse.set(value, '/' + key);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
if (type_dynamics_new) {
|
|
1623
|
-
type_dynamics.clear();
|
|
1624
|
-
for (const key of type_dynamics_new) {
|
|
1625
|
-
type_dynamics.add(key);
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
if (type_mimes_new) {
|
|
1629
|
-
for (const [key, value] of type_mimes_new) {
|
|
1630
|
-
type_mimes.set(key, value);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
if (type_raws_new) {
|
|
1634
|
-
type_raws.clear();
|
|
1635
|
-
for (const key of type_raws_new) {
|
|
1636
|
-
type_raws.add(key);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
for (const path of services_new) config_path_check(path);
|
|
1641
|
-
const promises = [
|
|
1642
|
-
services_list_react(
|
|
1643
|
-
services_new
|
|
1644
|
-
.filter(path => path.charCodeAt(0) !== 35)
|
|
1645
|
-
),
|
|
1646
|
-
];
|
|
1647
|
-
|
|
1648
|
-
if (port_http_new !== port_http) {
|
|
1649
|
-
port_http = port_http_new;
|
|
1650
|
-
promises.push(
|
|
1651
|
-
port_http_new > 0
|
|
1652
|
-
? actions.http_start()
|
|
1653
|
-
: actions.http_stop()
|
|
1654
|
-
);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
if (
|
|
1658
|
-
actions.https_stop != null &&
|
|
1659
|
-
port_https_new !== port_https
|
|
1660
|
-
) {
|
|
1661
|
-
port_https = port_https_new;
|
|
1662
|
-
promises.push(
|
|
1663
|
-
port_https_new > 0
|
|
1664
|
-
? actions.https_start()
|
|
1665
|
-
: actions.https_stop()
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
return Promise.all(promises);
|
|
1670
|
-
}
|
|
1671
|
-
catch (err) {
|
|
1672
|
-
log('[error] rtjscomp.json: ' + (err.message || err));
|
|
1673
|
-
}
|
|
1674
|
-
});
|
|
1675
|
-
|
|
1676
|
-
if (log_verbose) log('started all');
|
|
1677
|
-
|
|
1678
|
-
})();
|
|
5
|
+
(async()=>{const D=require("fs"),E=require("fs/promises"),wa=require("http"),Oa=require("url"),Pa=require("zlib"),Qa=require("parse-multipart-data").parse,ea=require("querystring").decode,Ra=require("ipware")().get_ip,Sa=/bot|googlebot|crawler|spider|robot|crawling|favicon/i,Ta=/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i,
|
|
6
|
+
xa={level:9},Ua=/,\s*/,fa=/\bimport\(/g;var z="undefined"!==typeof Bun;const Va=/:([0-9]+)[\):]/,ya={paths:[require("path").resolve()]},Wa=/\bservice_require\(([^)]*)\)/g,ha={persistent:!0,interval:1E3},za=process.argv.includes("-v");let r=za,N=0,O=0,Aa=!0,P=!1;const X=new Map([["","index.html"]]),Q=new Map([["index.html","/"]]),R=new Map,ia=new Set,ja=new Set,ka=new Set,la=new Set(["events","html","json","txt"]),ma=new Map([["apk","application/zip"],["bpg","image/bpg"],["css","text/css; charset=utf-8"],
|
|
7
|
+
["events","text/event-stream"],["flac","audio/flac"],["gz","application/gzip"],["hta","application/hta"],["html","text/html; charset=utf-8"],["ico","image/x-icon"],["jpg","image/jpeg"],["js","text/javascript; charset=utf-8"],["json","application/json; charset=utf-8"],["mid","audio/midi"],["mp3","audio/mpeg3"],["pdf","application/pdf"],["png","image/png"],["rss","application/rss+xml; charset=utf-8"],["txt","text/plain; charset=utf-8"],["xml","application/xml; charset=utf-8"],["xz","application/x-xz"],
|
|
8
|
+
["zip","application/zip"]]),na=new Set("apk bpg flac gz jpg mp3 pdf png xz zip".split(" ")),Y=new Map,m={},A=global.rtjscomp={actions:m,version:"0.9.3"};Object.fromEntries||(Object.fromEntries=a=>{const b={};for(const c of a)b[c[0]]=c[1];return b});let S=D.watch;if(z){const a=S,b=new Map;S=(c,h,n)=>{b.has(c)||a(c,h,()=>{const k=b.get(c);k&&k()});b.set(c,n);return{close:()=>b.set(c,null)}}}global.globals=A;global.actions=A.actions;global.data_load=a=>{d("[deprecated] synchronous load file: data/"+
|
|
9
|
+
a);try{return D.readFileSync("data/"+a,"utf8")}catch(b){return null}};global.data_save=(a,b)=>(d("[deprecated] synchronous save file: data/"+a),D.writeFileSync("data/"+a,b,"utf8"));global.number_check_int=a=>Math.floor(a)===a;global.number_check_uint=a=>0<=a&&number_check_int(a);A.data_load=async a=>{r&&d("load file: data/"+a);const b=await E.readFile("data/"+a,"utf8").catch(()=>null);return a.endsWith(".json")?JSON.parse(b||null):b};A.data_load_watch=(a,b)=>oa("data/"+a,c=>b(a.endsWith(".json")?
|
|
10
|
+
JSON.parse(c||null):c));A.data_save=(a,b)=>(r&&d("save file: data/"+a),E.writeFile("data/"+a,a.endsWith(".json")?JSON.stringify(b):b,"utf8"));const Ba=a=>{try{return`:${a.stack.split("\n",2)[1].split(",").pop().match(Va)[1]-2}`}catch(b){return""}},Ca=new Set,pa=new Map,qa=new Map,ra=a=>{var b=pa.get(a);if(null!=b)return b;r&&d("require module: "+a);b=require.resolve(a,ya);Ca.add(b);pa.set(a,b=require(b));return b},sa=async a=>{let b=qa.get(a);if(null!=b)return b;r&&d("import module: "+a);qa.set(a,
|
|
11
|
+
b=await import("file://"+require.resolve(a,ya)));return b};m.module_cache_clear=()=>{for(const a of Ca)delete require.cache[a];pa.clear();qa.clear()};const ta=sa.constructor,F=new Map;let T=null,ua=null;const Xa=async a=>{await Promise.all([...F.values()].filter(c=>4>c.status&&!a.includes(c.path)).map(c=>(c.dependencies=null,c)).map(Da));const b=[];for(const c of a){let h=F.get(c);if(null==h)F.set(c,h={content:null,dependencies:null,x0:null,x1:null,x2:null,path:c,x5:null,x4:null,x7:null,
|
|
12
|
+
x6:null,status:0,x8:null});else if(4>h.status)continue;b.push(h)}await Promise.all(b.map(Z))},Ya=()=>Promise.all([...F.values()].map(a=>(a.dependencies=null,a)).map(Da)),Z=async a=>{T||(T=new Promise(k=>{ua=k}));const {path:b}=a;r&&d(b+": prepare for (re)start");let c=0;try{if(1===a.status||2===a.status)r&&d(b+": abort previous start"),a.status=0,a.x4&&a.x4();else if(3===a.status){a.status=0;for(var h of F.values())(2===h.status||3===h.status)&&h.dependencies&&h.dependencies.includes(a)&&(h.dependencies=
|
|
13
|
+
null,Z(h));Ea(a)}await a.x7;3<a.status&&(a.status=0);a.x7=new Promise(l=>{a.x6=l});if(!a.x1){h="public/"+b+".service.js";var n=await E.readFile(h,"utf8");if(0!==a.status)return;if(!a.x8){let l=0;a.x8=S(h,ha,()=>(clearTimeout(l),l=setTimeout(()=>(r&&d("file updated: "+b),a.x1=null,Z(a)),50)))}n.includes("globals.")&&d(`[deprecated] ${b}: uses globals object`);a.x0=[];for(let [,l]of n.matchAll(Wa)){const q=l.charCodeAt(0),w=l.charCodeAt(l.length-1);if(3>l.length||!(34===q&&34===w||39===q&&39===w))throw Error("service_require() needs inline string");
|
|
14
|
+
a.x0.includes(l=l.slice(1,-1))||a.x0.push(l)}a.x1=new ta("require","custom_import","service_require","service_require_try",`const log=a=>rtjscomp.log(${JSON.stringify(b+": ")}+a);${n.replace(fa,"custom_import(")}`)}a.dependencies=[];n=!1;for(const l of a.x0){const q=F.get(l);if(null==q)throw Error("unknown required service: "+l);a.dependencies.push(q);n=n||3!==q.status}if(n&&(r&&d(b+": wait for dependencies"),a.status=1,a.x5=new Promise(l=>{a.x4=l}),await a.x5,1!==a.status))return;d("start service: "+
|
|
15
|
+
b);a.status=2;c=setInterval(()=>{d(`[warning] ${b}: still starting`)},5E3);const k=a.content={},g=await a.x1.call(k,ra,sa,Fa,Ga);if(2!==a.status)return;"function"===typeof g&&(a.x2=g);const t=k.start;if(t&&(d(`[deprecated] ${b}: has start method`),delete k.start,await t(),2!==a.status))return;k.stop&&(d(`[deprecated] ${b}: has stop method`),a.x2=k.stop,delete k.stop);r&&d("started service: "+b);a.status=3}catch(k){k instanceof Error||(k=Error(k+"?! wtf"));d(`[error] ${b}${Ba(k)}: ${k.message}`);a.status=
|
|
16
|
+
5;a.dependencies=null;return}finally{clearInterval(c),a.x5=a.x4=null,3!==a.status&&(a.x6(),Ha())}a:for(const k of F.values())if(1===k.status){for(const g of k.dependencies)if(3!==g.status)continue a;k.x4()}Ha()},Ha=()=>{if(T){for(const a of F.values())if(3>a.status)return;ua();T=ua=null}},Da=async a=>{d("stop service: "+a.path);a.status=4;a.x1=null;a.x8&&a.x8.close();for(const b of F.values())b.dependencies&&b.dependencies.includes(a)&&(b.dependencies=null,4!==b.status&&Z(b));a.x4?a.x4():await Ea(a);
|
|
17
|
+
F.delete(a.path);r&&d("stopped service: "+a.path)},Ea=async a=>{const {x2:b,path:c}=a;if(b){a.x2=null;const h=setInterval(()=>{d(`[warning] ${c}: still stopping`)},1E3);try{await b()}catch(n){d(`[error] ${c}${Ba(n)} stop handler: ${n.message}`),a.status=5}clearInterval(h)}a.x6()},Fa=a=>{const b=F.get(a);if(null!=b&&3===b.status)return b.content;throw Error("service required: "+a);},Ga=a=>{a=F.get(a);return null!=a&&3===a.status?a.content:null},Za=(a,b)=>{const c=S(a,ha,()=>(c.close(),r&&d("file updated: "+
|
|
18
|
+
a),b()))},oa=async(a,b)=>{try{const h=await E.readFile(a,"utf8");r&&d("load file: "+a);await b(h)}catch(h){return await b(null),null}let c=0;return S(a,ha,()=>(clearTimeout(c),c=setTimeout(()=>P||(r&&d("file updated: "+a),E.readFile(a,"utf8").catch(()=>null).then(b)),50)))},$a=(a,b,c)=>{if(null===a||!(b in a))return c;c=a[b];if("boolean"!==typeof c)throw b+" must be boolean";delete a[b];return c},va=(a,b,c)=>{if(null===a||!(b in a))return c;c=a[b];if("number"!==typeof c||0>c||0<c%1)throw b+" must be positive integer";
|
|
19
|
+
delete a[b];return c},L=(a,b)=>{if(null===a||!(b in a))return null;const c=a[b];if("object"!==typeof c||!c||null==c.length||c.some(h=>"string"!==typeof h))throw b+" must be array of strings";delete a[b];return c},Ia=(a,b)=>{if(null===a||!(b in a))return null;let c=a[b];if("object"!==typeof c||!c||(c=Object.entries(c)).some(([,h])=>"string"!==typeof h))throw b+" must be object of strings";delete a[b];return c};var I=a=>a.split("\n").filter(b=>0<b.length&&35!==b.charCodeAt(0));const Ja=a=>Object.fromEntries(a.split("\n").filter(b=>
|
|
20
|
+
0<b.length&&35!==b.charCodeAt(0)).map(b=>b.split(":"))),M=(a,b=!1)=>{if(!b&&!a)throw"path is empty";if(47===a.charCodeAt(0))throw"path must not start with /";if(47===a.charCodeAt(a.length-1))throw"path must not end with /";if(a.includes(".."))throw"path must not contain ..";if(a.includes("~"))throw"path must not contain ~";if(a.includes("//"))throw"path must not contain //";};let Ka=A.log_history=[];m.log_clear=()=>{Ka=A.log_history=[]};const d=A.log=a=>(console.log(a),Ka.push(a),U?aa("log",[a]):
|
|
21
|
+
void 0),U=D.existsSync("spam.csv");A.spam_history="";m.spam_save=async(a=!1)=>{if(U)try{const b=A.spam_history;A.spam_history="";await E.appendFile("spam.csv",b,"utf8");r&&!a&&d("spam.csv saved")}catch(b){d("[error] cannot save spam.csv: "+b.message)}};const aa=(a,b)=>{A.spam_history+=Date.now()+","+a+","+JSON.stringify(b)+"\n";1E5<=A.spam_history.length&&m.spam_save()},La=async(a,b,c)=>{const h=a.method,n=a.headers,k=Ra(a).clientIp;"x-forwarded-proto"in n&&(c="https"===n["x-forwarded-proto"]);U&&
|
|
22
|
+
aa("request",[c,a.url,k]);try{const x=Oa.parse(a.url,!1);let e=x.pathname||"/";if(47!==e.charCodeAt(0)||e.includes("//"))throw 404;if(1<e.length&&e.endsWith("/")&&(e=e.slice(0,-1),e.lastIndexOf("/")<e.lastIndexOf(".")))throw b.setHeader("Location",e),301;e=e.slice(1);if(!(e.includes("php")||e.includes("sql")||e.includes(".git/")||ia.has(e))){b.setHeader("Server","l3p3 rtjscomp v0.9.3");b.setHeader("Access-Control-Allow-Origin","*");var g=null,t=null;if(Q.has(e))throw b.setHeader("Location",Q.get(e)),
|
|
23
|
+
301;if(X.has(e))e=X.get(e);else{var l=e.split("/"),q=R.get(l.shift());if(null!=q)a:for(var w of q){var G=w.x3,B=G.length;if(B===l.length){q={};for(let u=0;u<B;++u)if(42===G[u].charCodeAt(0))1<G[u].length&&(q[G[u].slice(1)]=l[u]);else if(G[u]!==l[u])continue a;e=w.value;g=q;break}}}var J=e.lastIndexOf(".");if(e.includes("..")||e.includes("~")||J<=e.lastIndexOf("/"))throw 404;var p=e.slice(J+1).toLowerCase(),y=Aa&&"accept-encoding"in n&&!na.has(p)&&n["accept-encoding"].split(Ua).includes("gzip"),H=
|
|
24
|
+
la.has(p)&&!ka.has(e);if("GET"!==h){if(!H)throw 405;"content-length"in n&&(t=new Promise(u=>{const C=[];a.on("data",v=>{C.push(v)});a.on("end",v=>{v&&C.push(v);u(Buffer.concat(C))})}))}l=J=null;w="public/"+e;if(H&&Y.has(e))J=Y.get(e);else{r&&d(`load ${H?"dynam":"stat"}ic file: ${e}`);if(ja.has(e)||e.endsWith(".service.js"))throw 403;if(!D.existsSync(w))throw 404;l=D.statSync(w);if(l.isDirectory())throw 403;if(H){const u=await E.readFile(w,"utf8");try{if(u.includes("\r"))throw"illegal line break, must be unix";
|
|
25
|
+
u.includes("globals.")&&d(`[deprecated] ${e}: uses globals object`);const C=u.length;let v=`const log=a=>rtjscomp.log(${JSON.stringify(e+": ")}+a);`;G=!1;for(q=B=0;q<C;)if(G){if(0>(q=u.indexOf("?>",B=q+2)))throw'"?>" missing';G=!1;B<q&&(v=61!==u.charCodeAt(B)?v+(u.slice(B,q).replace(fa,"custom_import(")+";"):v+`output.write(''+(${u.slice(++B,q).replace(fa,"custom_import(")}));`);q+=2}else-1<(q=u.indexOf("<?",B=q))?G=!0:q=C,B<q&&(v+=`output.write(${JSON.stringify(u.slice(B,q))});`);try{J=new ta("input",
|
|
26
|
+
"output","request","response","require","custom_import","service_require","service_require_try",v)}catch(f){throw f.message;}}catch(C){throw d(`[error] ${e} compile: ${C}`),500;}Y.set(e,J);Za(w,()=>Y.delete(e))}}b.statusCode=200;b.setHeader("Content-Type",ma.get(p)||ma.get("txt"));if(H){g=g||{};if(n.cookie)for(let f of n.cookie.split(";")){f=f.trim();const K=f.indexOf("=");0<K?g[f.slice(0,K).trimRight()]=decodeURI(f.slice(K+1).trimLeft()):0>K&&(g[f]=void 0)}n["x-input"]&&Object.assign(g,ea(n["x-input"]));
|
|
27
|
+
if(x.query)try{Object.assign(g,ea(x.query))}catch(f){throw d(`[error] ${e} request query: ${f.message}`),400;}if(t)try{const f=a.headers["content-type"]||"",K=g.body=await t;t=null;switch(f.split(";")[0]){case "application/x-www-form-urlencoded":t=ea(K.toString());break;case "application/json":t=JSON.parse(K.toString());break;case "multipart/form-data":t=Object.fromEntries(Qa(K,f.split("boundary=")[1].split(";")[0]).map(V=>{const {name:ab}=V;delete V.name;return[ab,V.type?V:V.data.toString()]}))}t&&
|
|
28
|
+
Object.assign(g,t)}catch(f){throw d(`[error] ${e} request body: ${f.message}`),400;}const u=g.user_agent=n["user-agent"];g.bot=Sa.test(u);g.mobil=Ta.test(u);g.https=c;g.ip=k;g.method=h.toLowerCase();g.path=x.pathname;let C;b.setHeader("Cache-Control","no-cache, no-store");y?(b.setHeader("Content-Encoding","gzip"),(C=Pa.createGzip(xa)).pipe(b)):C=b;U&&aa("execute",[e,Object.fromEntries(Object.entries(g).filter(f=>"body"!==f[0]).map(f=>"password"===f[0]?[f[0],"***"]:f).map(f=>"file"===f[0]?[f[0],"..."]:
|
|
29
|
+
f).map(f=>"object"!==typeof f[1]||f[1].length?f:[f[0],Object.keys(f[1]).slice(0,20)]).map(f=>"user_agent"!==f[0]&&"string"===typeof f[1]&&20<f[1].length?[f[0],f[1].slice(0,20)+"..."]:f))]);await T;let v;try{v=await J(g,C,a,b,ra,sa,Fa,Ga)}catch(f){f instanceof Error?(d(`[error] ${e}: ${f.message}`),v=f.message.startsWith("service required: ")?503:500):"number"===typeof f?(d(`[deprecated] ${e}: status code thrown, use return instead`),v=f):(d(`[error] ${e}: invalid throw type: ${typeof f}`),v=500)}if(null!=
|
|
30
|
+
v){b.headersSent?(null!=b.writableEnded?b.writableEnded:b.finished)||C.end(`${"html"===p?"<hr>":"\n\n---\n"}ERROR!`):y&&b.removeHeader("Content-Encoding");if("number"!==typeof v)throw d(`[error] ${e}: invalid return type: ${typeof v}`),500;if(b.headersSent&&500>v)throw d(`[error] ${e}: status code after content`),500;throw v;}C.end()}else c=null,y&&80<l.size&&D.existsSync(w+".gz")?c=D.createReadStream(w+".gz"):(y=!1,c=D.createReadStream(w)),U&&aa("static_send",[e,y]),b.setHeader("Cache-Control","public, max-age=600"),
|
|
31
|
+
y?b.setHeader("Content-Encoding","gzip"):b.setHeader("Content-Length",l.size),c.pipe(b)}}catch(x){"number"!==typeof x&&(console.error(x),x=500),(500<=x||r&&400<=x)&&d(`[error] request failed: ${x}; ${k}; ${a.url}`),b.headersSent||(b.writeHead(x,{"Content-Type":"text/html","Cache-Control":"no-cache, no-store"}),b.end(`<!DOCTYPE html><html><body><h1>HTTP ${x}: ${wa.STATUS_CODES[x]||"Error"}</h1></body></html>`))}};m.halt=async()=>{P=!0;r&&d("stop all");await Promise.all([m.http_stop&&m.http_stop(),
|
|
32
|
+
m.https_stop&&m.https_stop(),Ya()]);await m.spam_save();d("stopped all")};m.exit=async a=>{P||("number"!==typeof a&&(a=0),await m.halt(),r&&d("exit"),process.exit(a))};process.on("uncaughtException",a=>{"symbol"===typeof a&&(a=a.toString());d("[error] uncaughtException: "+(a.message||a));console.error(a);P&&process.exit(1);m.exit(1)});process.on("unhandledRejection",a=>{d("[error] unhandledRejection: "+(a.message||a));console.error(a);P&&process.exit(1);m.exit(1)});process.on("exit",m.exit);process.on("SIGINT",
|
|
33
|
+
m.exit);process.on("SIGUSR2",m.exit);process.on("SIGTERM",m.exit);d(`rtjscomp v${"0.9.3"} in ${z?"bun":"node"} on ${process.platform.replace("win32","windows")}`);await oa("config/init.js",async a=>{if(a){d("[deprecated] run global init script");try{await (new ta("require",a))(ra)}catch(b){d("[error] init.js: "+b.message)}}});const [bb,cb,...Ma]=await Promise.all([E.stat("data/").catch(a=>null),E.stat("public/").catch(a=>null),..."file_blocks file_privates file_raws file_type_dyns file_type_mimes file_type_nocompress path_aliases port_http port_https services".split(" ").map(a=>
|
|
34
|
+
E.readFile("config/"+a+".txt","utf8").catch(b=>null))]);bb||D.mkdirSync("data/");cb||D.cpSync(__dirname+"/public/","public/",{recursive:!0});if(Ma.some(a=>null!==a)){z=JSON.parse(await E.readFile("rtjscomp.json","utf8").catch(w=>null))||{};const [a,b,c,h,n,k,g,t,l,q]=Ma;a&&(z.path_ghosts=I(a));b&&(z.path_hiddens=I(b));c&&(z.path_statics=I(c));h&&(z.type_dynamics=I(h));n&&(z.type_mimes=Ja(n));k&&(z.type_raws=I(k));g&&(z.path_aliases=Ja(g));t&&(I=Number(t),isNaN(I)||(z.port_http=I));l&&(I=Number(l),
|
|
35
|
+
isNaN(I)||(z.port_https=I));q&&(z.services=q.split("\n").filter(w=>0<w.length));await E.writeFile("rtjscomp.json",JSON.stringify(z,null,"\t")+"\n","utf8");d("[deprecated] config files found, rtjscomp.json written, please delete config files")}let Na=0,ba=!1,W=null;const ca=new Map,da=wa.createServer((a,b)=>La(a,b,!1));da.on("error",a=>{d("[error] http: "+a.message);W&&W()});da.on("connection",a=>{const b=++Na;ca.set(b,a);a.on("close",()=>{ca.delete(b)})});m.http_start=async()=>{await m.http_stop();
|
|
36
|
+
N&&(ba=!0,d("start http: http://localhost:"+N),await new Promise(a=>da.listen(N,W=a)),W?W=null:r&&d("started http"))};m.http_stop=async()=>{if(ba){ba=!1;d("stop http");var a=setTimeout(m.http_kill,5E3);await new Promise(b=>da.close(b));clearTimeout(a);r&&d("stopped http")}};m.http_kill=async()=>{ba||(d("kill http"),await Promise.all([...ca.values()].map(a=>a.destroy())),r&&d("killed http"),ca.clear())};try{const a=D.readFileSync("config/ssl/domain.key"),b=D.readFileSync("config/ssl/chained.pem");
|
|
37
|
+
let c=!1,h=null;const n=new Map,k=require("https").createServer({key:a,cert:b},(g,t)=>La(g,t,!0));k.on("error",g=>{d("[error] https: "+g.message);h&&h()});k.on("connection",g=>{const t=++Na;n.set(t,g);g.on("close",()=>{n.delete(t)})});m.https_start=async()=>{await m.https_stop();O&&(c=!0,d("start https: https://localhost:"+O),await new Promise(g=>k.listen(O,h=g)),h?h=null:r&&d("started https"))};m.https_stop=async()=>{if(c){c=!1;d("stop https");var g=setTimeout(m.https_kill,5E3);await new Promise(t=>
|
|
38
|
+
k.close(t));clearTimeout(g);r&&d("stopped https")}};m.https_kill=async()=>{c||(d("kill https"),await Promise.all([...n.values()].map(g=>g.destroy())),r&&d("killed https"),n.clear())}}catch(a){r&&d("https: no cert, disabled")}await oa("rtjscomp.json",a=>{try{a=JSON.parse(a);if("object"!==typeof a)throw"must contain {}";const b=va(a,"gzip_level",9),c=$a(a,"log_verbose",za),h=Ia(a,"path_aliases"),n=L(a,"path_ghosts"),k=L(a,"path_hiddens"),g=L(a,"path_statics"),t=va(a,"port_http",8080),l=va(a,"port_https",
|
|
39
|
+
0),q=L(a,"services")||[],w=L(a,"type_dynamics"),G=Ia(a,"type_mimes"),B=L(a,"type_raws");if(a){const p=Object.keys(a);if(0<p.length)throw"unknown: "+p.join(", ");}if(9<b)throw"gzip_level > 9";if(65535<t||65535<l)throw"port > 65535";Aa=0<b;xa.level=b;r=c;if(n){ia.clear();for(const p of n)M(p),ia.add(p)}if(k){ja.clear();for(const p of k)M(p),ja.add(p)}if(g){ka.clear();for(const p of g)M(p),ka.add(p)}if(h){X.clear();Q.clear();R.clear();for(const [p,y]of h)if(M(p,!0),M(y),p.includes("*")){const H=p.split("/"),
|
|
40
|
+
x=H.shift();R.has(x)?R.get(x).push({x3:H,value:y}):R.set(x,[{x3:H,value:y}])}else{X.set(p,y);const H=Q.get(y);(null==H||H.length-1>y.length)&&Q.set(y,"/"+p)}}if(w){la.clear();for(const p of w)la.add(p)}if(G)for(const [p,y]of G)ma.set(p,y);if(B){na.clear();for(const p of B)na.add(p)}for(const p of q)M(p);const J=[Xa(q.filter(p=>35!==p.charCodeAt(0)))];t!==N&&(N=t,J.push(0<t?m.http_start():m.http_stop()));null!=m.https_stop&&l!==O&&(O=l,J.push(0<l?m.https_start():m.https_stop()));return Promise.all(J)}catch(b){d("[error] rtjscomp.json: "+
|
|
41
|
+
(b.message||b))}});r&&d("started all")})();
|