rtjscomp 0.9.1 → 0.9.2

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/README.md CHANGED
@@ -33,6 +33,8 @@ $ npm start
33
33
 
34
34
  it is only needed if you want to change default settings. it has the following properties:
35
35
 
36
+ - `gzip_level`: compression level, 0-9, default is 9
37
+ - `log_verbose`: boolean, set true for more verbose logging or run with -v
36
38
  - `path_aliases`: object where the key is the url path and the value is the file path
37
39
  - `path_ghosts`: array of paths that are simply ignored, no response is sent
38
40
  - `path_hiddens`: array of paths that give 404
@@ -106,6 +108,7 @@ in every served dynamic file (like .html), you can insert `<?` and `?>` tags to
106
108
  request-independent services can be created using .service.js files referenced in services.txt.
107
109
  in both file types (dynamic served and services), you can use all node/bun methods including `require`, but also those:
108
110
 
111
+ - `log(msg)`: logs the message to the console
109
112
  - `service_require(service path)`: returns the matching service object
110
113
  - `service_require_try(service path)`: returns the matching service object or null if not found or if disabled
111
114
  - `rtjscomp`: has these properties/methods:
@@ -0,0 +1,58 @@
1
+ add compression of dynamic data
2
+ add https
3
+ add actions
4
+ 0.5
5
+ add shutdown action
6
+ prefer gzip over lzma
7
+ move public files into separate dir
8
+ 0.51
9
+ add list of raw files
10
+ 0.52
11
+ ignore empty areas (<??> or ?><?)
12
+ 0.53
13
+ move configuration data to separate files
14
+ 0.54
15
+ add path aliases
16
+ block accessing directories
17
+ 0.55
18
+ add http-header Content-Location
19
+ 0.56
20
+ add list of file types not to compress
21
+ keep raw files in ram
22
+ remove lzma
23
+ add compression of raw files
24
+ load already compressed files
25
+ just compress when at least 100 bytes of data
26
+ 0.57
27
+ cleanup code
28
+ execute and send file in parallel
29
+ add services
30
+ 0.58
31
+ add analytic logger
32
+ use compressed file even if file type is not intended as being compressed
33
+ 0.59
34
+ remove static files from cache when precompressed files got changed
35
+ 0.591
36
+ set gzip level to 9
37
+ save automatically compressed files
38
+ 0.592
39
+ block requests with invalid hostname
40
+ 0.593
41
+ add user data storage
42
+ 0.594
43
+ remove custom file init
44
+ asynchronize request handling
45
+ temporarily disable chaching of static files
46
+ 0.595
47
+ send partial files
48
+ load and interpret file in parallel
49
+ optimize source codes of sent files
50
+ 0.596
51
+ stop services before replacing them
52
+ service_require return service object
53
+ 0.597
54
+ stop services on exit
55
+ 0.598
56
+ read request body
57
+ 0.599
58
+ ???
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rtjscomp",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "php-like server but with javascript",
5
5
  "repository": {
6
6
  "type": "git",
package/rtjscomp.js CHANGED
@@ -24,19 +24,30 @@ const AGENT_CHECK_BOT = /bot|googlebot|crawler|spider|robot|crawling|favicon/i;
24
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
25
  const GZIP_OPTIONS = {level: 9};
26
26
  const HTTP_LIST_REG = /,\s*/;
27
- const IMPORT_REG = /import\(/g;
27
+ const IMPORT_REG = /\bimport\(/g;
28
28
  const IS_BUN = typeof Bun !== 'undefined';
29
+ const LINENUMBER_REG = /:([0-9]+)[\):]/;
29
30
  const PATH_CONFIG = 'config/';
30
31
  const PATH_DATA = 'data/';
31
32
  const PATH_PUBLIC = 'public/';
32
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
33
41
  const VERSION = require('./package.json').version;
34
42
  const WATCH_OPTIONS = {persistent: true, interval: 1000};
35
43
 
36
44
  // config
37
- let log_verbose = process.argv.includes('-v');
45
+ const log_verbose_flag = process.argv.includes('-v');
46
+ let log_verbose = log_verbose_flag;
38
47
  let port_http = 0;
39
48
  let port_https = 0;
49
+ let gz_enabled = true;
50
+ let exiting = false;
40
51
  /// any path -> file
41
52
  const path_aliases = new Map([
42
53
  ['', 'index.html'],
@@ -108,7 +119,7 @@ if (!Object.fromEntries) {
108
119
  const object = {};
109
120
  for (const entry of entries) object[entry[0]] = entry[1];
110
121
  return object;
111
- };
122
+ }
112
123
  }
113
124
 
114
125
  // workaround for bun: https://github.com/oven-sh/bun/issues/18919
@@ -160,7 +171,11 @@ global.number_check_uint = number => (
160
171
  rtjscomp.data_load = async name => {
161
172
  if (log_verbose) log('load file: ' + PATH_DATA + name);
162
173
  const data = await fsp.readFile(PATH_DATA + name, 'utf8').catch(() => null);
163
- return name.endsWith('.json') ? JSON.parse(data || null) : data;
174
+ return (
175
+ name.endsWith('.json')
176
+ ? JSON.parse(data || null)
177
+ : data
178
+ );
164
179
  }
165
180
  rtjscomp.data_load_watch = (name, callback) => (
166
181
  file_keep_new(PATH_DATA + name, data => (
@@ -175,11 +190,33 @@ rtjscomp.data_save = (name, data) => (
175
190
  log_verbose && log('save file: ' + PATH_DATA + name),
176
191
  fsp.writeFile(
177
192
  PATH_DATA + name,
178
- name.endsWith('.json') ? JSON.stringify(data) : data,
193
+ (
194
+ name.endsWith('.json')
195
+ ? JSON.stringify(data)
196
+ : data
197
+ ),
179
198
  'utf8'
180
199
  )
181
200
  )
182
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
+
183
220
  const custom_require_paths = new Set;
184
221
  const custom_require_cache = new Map;
185
222
  const custom_import_cache = new Map;
@@ -218,153 +255,348 @@ actions.module_cache_clear = () => {
218
255
  }
219
256
  const AsyncFunction = custom_import.constructor;
220
257
 
221
- const services_active = new Map;
222
- const services_loading = new Set;
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
+ */
223
264
  const services_list_react = async list => {
265
+ // stop all services not in list
224
266
  await Promise.all(
225
- Array.from(services_active.entries())
226
- .filter(([path, _]) => !list.includes(path))
227
- .map(([_, service_object]) => service_stop(service_object, true))
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)
228
278
  );
229
- for (const path of list)
230
- if (
231
- path.charCodeAt(0) !== 35 &&
232
- !services_active.has(path)
233
- ) {
234
- await service_start(path);
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);
235
301
  }
302
+ await Promise.all(
303
+ start_queue.map(service_start)
304
+ );
236
305
  }
237
- const service_start = async path => {
238
- const service_object = {
239
- content: null,
240
- handler_stop: null,
241
- path,
242
- stopped: false,
243
- watcher: null,
244
- };
245
-
246
- service_object.watcher = await file_keep_new(
247
- PATH_PUBLIC + path + '.service.js',
248
- async file_content => {
249
- if (file_content === null) {
250
- log('[error] service file not found: ' + path);
251
- return service_stop(service_object, true);
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();
252
339
  }
253
- if (services_loading.size > 0) {
254
- await Promise.all(Array.from(services_loading));
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);
255
355
  }
256
- const start_promise = service_start_inner(path, service_object, file_content);
257
- services_loading.add(start_promise);
258
- await start_promise;
259
- services_loading.delete(start_promise);
356
+ service_stop_inner(service_object);
260
357
  }
261
- );
262
- }
263
- const service_start_inner = async (path, service_object, file_content) => {
264
- if (services_active.has(path)) {
265
- await service_stop_handler(service_object);
266
- }
267
- const content_object = service_object.content = {};
268
- log('start service: ' + path);
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
+ }
269
381
 
270
- const start_interval = setInterval(() => {
271
- log(`[warning] ${path}: still starting`);
272
- }, 1e3);
382
+ if (file_content.includes('globals.')) {
383
+ log(`[deprecated] ${path}: uses globals object`);
384
+ }
273
385
 
274
- if (file_content.includes('globals.')) {
275
- log(`[deprecated] ${path}: uses globals object`);
276
- }
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
+ }
277
405
 
278
- try {
279
- const result = await (new AsyncFunction(
280
- 'require',
281
- 'custom_import',
282
- `const log=a=>rtjscomp.log(${
283
- JSON.stringify(path + ': ')
284
- }+a);${
285
- file_content.replace(IMPORT_REG, 'custom_import(') + '\n'
286
- }`
287
- )).call(content_object, custom_require, custom_import);
288
- if (service_object.stopped) {
289
- clearInterval(start_interval);
290
- return;
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
+ );
291
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;
292
457
  if (typeof result === 'function') {
293
458
  service_object.handler_stop = result;
294
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;
295
475
  }
296
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 {
297
492
  clearInterval(start_interval);
298
- log(`[error] ${path}: ${err.message}`);
299
- if (service_object.stopped) return;
300
- return service_stop(service_object, false);
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
+ }
301
499
  }
302
500
 
303
- const handler_start = content_object.start;
304
- if (handler_start) {
305
- log(`[deprecated] ${path}: has start method`);
306
- delete content_object.start;
307
- try {
308
- await handler_start();
309
- }
310
- catch (err) {
311
- clearInterval(start_interval);
312
- log(`[error] ${path} start: ${err.message}`);
313
- return service_stop(service_object, false);
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;
314
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();
315
534
  }
316
- clearInterval(start_interval);
317
535
 
318
- if (content_object.stop) {
319
- log(`[deprecated] ${path}: has stop method`);
320
- service_object.handler_stop = content_object.stop;
321
- delete content_object.stop;
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
+ }
322
545
  }
323
546
 
324
- services_active.set(path, service_object);
325
- if (log_verbose) log('started service: ' + path);
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);
326
555
  }
327
- const services_shutdown = () => (
328
- Promise.all(
329
- Array.from(services_active.values())
330
- .map(service_object => service_stop(service_object, true))
331
- )
332
- )
333
- const service_stop = (service_object, forget) => (
334
- service_object.stopped = true,
335
- forget && service_object.watcher &&
336
- service_object.watcher.close(),
337
- service_stop_handler(service_object)
338
- )
339
- const service_stop_handler = async service_object => {
340
- services_active.delete(service_object.path);
341
- log('stop service: ' + service_object.path);
342
- const handler_stop = service_object.handler_stop;
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;
343
564
  if (handler_stop) {
565
+ service_object.handler_stop = null;
344
566
  const stop_interval = setInterval(() => {
345
- log(`[warning] ${service_object.path}: still stopping`);
567
+ log(`[warning] ${path}: still stopping`);
346
568
  }, 1e3);
347
569
  try {
348
- service_object.handler_stop = null;
349
570
  await handler_stop();
350
571
  }
351
572
  catch (err) {
352
- log(`[error] ${service_object.path} stop: ${err.message}`);
573
+ log(`[error] ${
574
+ path
575
+ }${
576
+ linenumber_try(err)
577
+ } stop handler: ${
578
+ err.message
579
+ }`);
580
+ service_object.status = SERVICE_STATUS_FAILED;
353
581
  }
354
582
  clearInterval(stop_interval);
355
583
  }
356
- if (log_verbose) log('stopped service: ' + service_object.path);
584
+ service_object.promise_stopped_resolve();
357
585
  }
358
- global.service_require = path => {
359
- const service = services_active.get(path);
360
- if (service != null) return service.content;
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;
361
592
  throw new Error('service required: ' + path);
362
593
  }
363
- global.service_require_try = path => {
364
- const service = services_active.get(path);
594
+ const service_require_try = path => {
595
+ const service_object = services.get(path);
365
596
  return (
366
- service != null
367
- ? service.content
597
+ service_object != null &&
598
+ service_object.status === SERVICE_STATUS_ACTIVE
599
+ ? service_object.content
368
600
  : null
369
601
  );
370
602
  }
@@ -390,7 +622,7 @@ const file_keep_new = async (path, callback) => {
390
622
  let timeout = 0;
391
623
  return fs_watch(path, WATCH_OPTIONS, () => (
392
624
  clearTimeout(timeout),
393
- timeout = setTimeout(() => (
625
+ timeout = setTimeout(() => exiting || (
394
626
  log_verbose && log('file updated: ' + path),
395
627
  fsp.readFile(path, 'utf8')
396
628
  .catch(() => null)
@@ -399,6 +631,18 @@ const file_keep_new = async (path, callback) => {
399
631
  ));
400
632
  }
401
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
+ }
402
646
  const get_prop_uint = (obj, prop, fallback) => {
403
647
  if (
404
648
  obj === null ||
@@ -467,6 +711,26 @@ const parse_old_map = data => (
467
711
  .map(entry => entry.split(':'))
468
712
  )
469
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
+ }
470
734
 
471
735
  let log_history = rtjscomp.log_history = [];
472
736
  actions.log_clear = () => {
@@ -541,6 +805,7 @@ const request_handle = async (request, response, https) => {
541
805
  if (
542
806
  path.includes('php') ||
543
807
  path.includes('sql') ||
808
+ path.includes('.git/') ||
544
809
  path_ghosts.has(path)
545
810
  ) return;
546
811
 
@@ -598,6 +863,7 @@ const request_handle = async (request, response, https) => {
598
863
  ).toLowerCase();
599
864
 
600
865
  let file_gz_enabled = (
866
+ gz_enabled &&
601
867
  'accept-encoding' in request_headers &&
602
868
  !type_raws.has(file_type) &&
603
869
  request_headers['accept-encoding'].split(HTTP_LIST_REG).includes('gzip')
@@ -639,7 +905,7 @@ const request_handle = async (request, response, https) => {
639
905
  file_dyn_enabled
640
906
  ? 'dynam'
641
907
  : 'stat'
642
- }ic file: ${path_real}`);
908
+ }ic file: ${path}`);
643
909
 
644
910
  if (
645
911
  path_hiddens.has(path) ||
@@ -743,6 +1009,8 @@ const request_handle = async (request, response, https) => {
743
1009
  'response',
744
1010
  'require',
745
1011
  'custom_import',
1012
+ 'service_require',
1013
+ 'service_require_try',
746
1014
  code
747
1015
  );
748
1016
  }
@@ -881,47 +1149,76 @@ const request_handle = async (request, response, https) => {
881
1149
  )
882
1150
  ]);
883
1151
 
884
- if (services_loading.size > 0) {
885
- await Promise.all(Array.from(services_loading));
886
- }
1152
+ await services_loaded_promise;
887
1153
 
1154
+ let returned;
888
1155
  try {
889
- await file_function(
1156
+ returned = await file_function(
890
1157
  file_function_input,
891
1158
  file_function_output,
892
1159
  request,
893
1160
  response,
894
1161
  custom_require,
895
- custom_import
1162
+ custom_import,
1163
+ service_require,
1164
+ service_require_try,
896
1165
  );
897
- file_function_output.end();
898
1166
  }
899
1167
  catch (err) {
900
1168
  if (err instanceof Error) {
901
1169
  log(`[error] ${path}: ${err.message}`);
902
-
903
- if (err.message.startsWith('service required: ')) {
904
- err = 503;
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!`);
905
1197
  }
906
1198
  }
907
- if (typeof err === 'number') {
1199
+ else if (file_gz_enabled) {
908
1200
  response.removeHeader('Content-Encoding');
909
- throw err;
910
1201
  }
911
-
912
- file_function_output.end((
913
- file_type === 'html'
914
- ? '<hr>'
915
- : '\n\n---\n'
916
- ) + 'ERROR!');
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;
917
1213
  }
1214
+ file_function_output.end();
918
1215
  }
919
1216
  else { // static file
920
1217
  let file_data = null;
921
1218
 
922
1219
  if (
923
1220
  file_gz_enabled &&
924
- file_stat.size > 90 &&
1221
+ file_stat.size > 80 &&
925
1222
  fs.existsSync(path_real + '.gz')
926
1223
  ) {
927
1224
  file_data = fs.createReadStream(path_real + '.gz');
@@ -958,16 +1255,22 @@ const request_handle = async (request, response, https) => {
958
1255
  log(`[error] request failed: ${err}; ${request_ip}; ${request.url}`);
959
1256
  }
960
1257
 
961
- response.writeHead(err, {
962
- 'Content-Type': 'text/html',
963
- 'Cache-Control': 'no-cache, no-store',
964
- });
965
- response.end(`<!DOCTYPE html><html><body><h1>HTTP ${err}: ${http.STATUS_CODES[err] || 'Error'}</h1></body></html>`);
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
+ }
966
1269
  }
967
1270
  }
968
1271
 
969
- let exiting = false;
970
1272
  actions.halt = async () => {
1273
+ exiting = true;
971
1274
  if (log_verbose) log('stop all');
972
1275
  await Promise.all([
973
1276
  actions.http_stop && actions.http_stop(),
@@ -979,7 +1282,6 @@ actions.halt = async () => {
979
1282
  }
980
1283
  actions.exit = async status => {
981
1284
  if (exiting) return;
982
- exiting = true;
983
1285
  if (typeof status !== 'number') status = 0;
984
1286
  await actions.halt();
985
1287
  if (log_verbose) log('exit');
@@ -1011,6 +1313,7 @@ log(`rtjscomp v${
1011
1313
  IS_BUN ? 'bun' : 'node'
1012
1314
  } on ${
1013
1315
  process.platform
1316
+ .replace('win32', 'windows')
1014
1317
  }`);
1015
1318
 
1016
1319
  await file_keep_new(PATH_CONFIG + 'init.js', async data => {
@@ -1160,7 +1463,7 @@ actions.http_kill = async () => {
1160
1463
  if (http_status_target) return;
1161
1464
  log('kill http');
1162
1465
  await Promise.all(
1163
- Array.from(http_connections.values())
1466
+ [...http_connections.values()]
1164
1467
  .map(connection => connection.destroy())
1165
1468
  );
1166
1469
  if (log_verbose) log('killed http');
@@ -1211,7 +1514,8 @@ try {
1211
1514
  if (https_status_target) return;
1212
1515
  log('kill https');
1213
1516
  await Promise.all(
1214
- Array.from(https_connections.values()).map(connection => connection.destroy())
1517
+ [...https_connections.values()]
1518
+ .map(connection => connection.destroy())
1215
1519
  );
1216
1520
  if (log_verbose) log('killed https');
1217
1521
  https_connections.clear();
@@ -1229,6 +1533,8 @@ await file_keep_new('rtjscomp.json', data => {
1229
1533
  throw 'must contain {}';
1230
1534
  }
1231
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);
1232
1538
  const path_aliases_new = get_prop_map(data, 'path_aliases');
1233
1539
  const path_ghosts_new = get_prop_list(data, 'path_ghosts');
1234
1540
  const path_hiddens_new = get_prop_list(data, 'path_hiddens');
@@ -1246,22 +1552,37 @@ await file_keep_new('rtjscomp.json', data => {
1246
1552
  throw 'unknown: ' + keys_left.join(', ');
1247
1553
  }
1248
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
+ }
1249
1564
 
1565
+ gz_enabled = gzip_level_new > 0;
1566
+ GZIP_OPTIONS.level = gzip_level_new;
1567
+ log_verbose = log_verbose_new;
1250
1568
  if (path_ghosts_new) {
1251
1569
  path_ghosts.clear();
1252
1570
  for (const key of path_ghosts_new) {
1571
+ config_path_check(key);
1253
1572
  path_ghosts.add(key);
1254
1573
  }
1255
1574
  }
1256
1575
  if (path_hiddens_new) {
1257
1576
  path_hiddens.clear();
1258
1577
  for (const key of path_hiddens_new) {
1578
+ config_path_check(key);
1259
1579
  path_hiddens.add(key);
1260
1580
  }
1261
1581
  }
1262
1582
  if (path_statics_new) {
1263
1583
  path_statics.clear();
1264
1584
  for (const key of path_statics_new) {
1585
+ config_path_check(key);
1265
1586
  path_statics.add(key);
1266
1587
  }
1267
1588
  }
@@ -1270,6 +1591,8 @@ await file_keep_new('rtjscomp.json', data => {
1270
1591
  path_aliases_reverse.clear();
1271
1592
  path_aliases_templates.clear();
1272
1593
  for (const [key, value] of path_aliases_new) {
1594
+ config_path_check(key, true);
1595
+ config_path_check(value);
1273
1596
  if (key.includes('*')) {
1274
1597
  const path_split = key.split('/');
1275
1598
  const first = path_split.shift();
@@ -1314,8 +1637,12 @@ await file_keep_new('rtjscomp.json', data => {
1314
1637
  }
1315
1638
  }
1316
1639
 
1640
+ for (const path of services_new) config_path_check(path);
1317
1641
  const promises = [
1318
- services_list_react(services_new),
1642
+ services_list_react(
1643
+ services_new
1644
+ .filter(path => path.charCodeAt(0) !== 35)
1645
+ ),
1319
1646
  ];
1320
1647
 
1321
1648
  if (port_http_new !== port_http) {