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/rtjscomp.js CHANGED
@@ -1,1678 +1,41 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- RTJSCOMP by L3P3, 2017-2025
2
+ "use strict";/*
3
+ RTJSCOMP by L3P3, 2017-2025
4
4
  */
5
-
6
- "use strict";
7
-
8
- (async () => {
9
-
10
- // node libs
11
- const fs = require('fs');
12
- const fsp = require('fs/promises');
13
- const http = require('http');
14
- const url = require('url');
15
- const zlib = require('zlib');
16
-
17
- // external libs
18
- const multipart_parse = require('parse-multipart-data').parse;
19
- const querystring_parse = require('querystring').decode;
20
- const request_ip_get = require('ipware')().get_ip;
21
-
22
- // constants
23
- const AGENT_CHECK_BOT = /bot|googlebot|crawler|spider|robot|crawling|favicon/i;
24
- const AGENT_CHECK_MOBIL = /(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;
25
- const GZIP_OPTIONS = {level: 9};
26
- const HTTP_LIST_REG = /,\s*/;
27
- const IMPORT_REG = /\bimport\(/g;
28
- const IS_BUN = typeof Bun !== 'undefined';
29
- const LINENUMBER_REG = /:([0-9]+)[\):]/;
30
- const PATH_CONFIG = 'config/';
31
- const PATH_DATA = 'data/';
32
- const PATH_PUBLIC = 'public/';
33
- const RESOLVE_OPTIONS = {paths: [require('path').resolve()]};
34
- const SERVICE_REQUIRE_REG = /\bservice_require\(([^)]*)\)/g;
35
- const SERVICE_STATUS_PENDING = 0; // just added to list
36
- const SERVICE_STATUS_WAITING = 1; // waiting for deps
37
- const SERVICE_STATUS_STARTING = 2; // running startup
38
- const SERVICE_STATUS_ACTIVE = 3; // ready for use
39
- const SERVICE_STATUS_STOPPING = 4; // running stop fn
40
- const SERVICE_STATUS_FAILED = 5; // waiting for fix
41
- const VERSION = require('./package.json').version;
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")})();