rtjscomp 0.9.0 → 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.0",
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 = () => {
@@ -528,8 +792,12 @@ const request_handle = async (request, response, https) => {
528
792
  path.includes('//')
529
793
  ) throw 404;
530
794
  if (path.length > 1 && path.endsWith('/')) {
531
- response.setHeader('Location', path.slice(0, -1));
532
- throw 301;
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
+ }
533
801
  }
534
802
  path = path.slice(1);
535
803
 
@@ -537,6 +805,7 @@ const request_handle = async (request, response, https) => {
537
805
  if (
538
806
  path.includes('php') ||
539
807
  path.includes('sql') ||
808
+ path.includes('.git/') ||
540
809
  path_ghosts.has(path)
541
810
  ) return;
542
811
 
@@ -594,6 +863,7 @@ const request_handle = async (request, response, https) => {
594
863
  ).toLowerCase();
595
864
 
596
865
  let file_gz_enabled = (
866
+ gz_enabled &&
597
867
  'accept-encoding' in request_headers &&
598
868
  !type_raws.has(file_type) &&
599
869
  request_headers['accept-encoding'].split(HTTP_LIST_REG).includes('gzip')
@@ -635,7 +905,7 @@ const request_handle = async (request, response, https) => {
635
905
  file_dyn_enabled
636
906
  ? 'dynam'
637
907
  : 'stat'
638
- }ic file: ${path_real}`);
908
+ }ic file: ${path}`);
639
909
 
640
910
  if (
641
911
  path_hiddens.has(path) ||
@@ -739,6 +1009,8 @@ const request_handle = async (request, response, https) => {
739
1009
  'response',
740
1010
  'require',
741
1011
  'custom_import',
1012
+ 'service_require',
1013
+ 'service_require_try',
742
1014
  code
743
1015
  );
744
1016
  }
@@ -877,47 +1149,76 @@ const request_handle = async (request, response, https) => {
877
1149
  )
878
1150
  ]);
879
1151
 
880
- if (services_loading.size > 0) {
881
- await Promise.all(Array.from(services_loading));
882
- }
1152
+ await services_loaded_promise;
883
1153
 
1154
+ let returned;
884
1155
  try {
885
- await file_function(
1156
+ returned = await file_function(
886
1157
  file_function_input,
887
1158
  file_function_output,
888
1159
  request,
889
1160
  response,
890
1161
  custom_require,
891
- custom_import
1162
+ custom_import,
1163
+ service_require,
1164
+ service_require_try,
892
1165
  );
893
- file_function_output.end();
894
1166
  }
895
1167
  catch (err) {
896
1168
  if (err instanceof Error) {
897
1169
  log(`[error] ${path}: ${err.message}`);
898
-
899
- if (err.message.startsWith('service required: ')) {
900
- 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!`);
901
1197
  }
902
1198
  }
903
- if (typeof err === 'number') {
1199
+ else if (file_gz_enabled) {
904
1200
  response.removeHeader('Content-Encoding');
905
- throw err;
906
1201
  }
907
-
908
- file_function_output.end((
909
- file_type === 'html'
910
- ? '<hr>'
911
- : '\n\n---\n'
912
- ) + '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;
913
1213
  }
1214
+ file_function_output.end();
914
1215
  }
915
1216
  else { // static file
916
1217
  let file_data = null;
917
1218
 
918
1219
  if (
919
1220
  file_gz_enabled &&
920
- file_stat.size > 90 &&
1221
+ file_stat.size > 80 &&
921
1222
  fs.existsSync(path_real + '.gz')
922
1223
  ) {
923
1224
  file_data = fs.createReadStream(path_real + '.gz');
@@ -954,16 +1255,22 @@ const request_handle = async (request, response, https) => {
954
1255
  log(`[error] request failed: ${err}; ${request_ip}; ${request.url}`);
955
1256
  }
956
1257
 
957
- response.writeHead(err, {
958
- 'Content-Type': 'text/html',
959
- 'Cache-Control': 'no-cache, no-store',
960
- });
961
- 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
+ }
962
1269
  }
963
1270
  }
964
1271
 
965
- let exiting = false;
966
1272
  actions.halt = async () => {
1273
+ exiting = true;
967
1274
  if (log_verbose) log('stop all');
968
1275
  await Promise.all([
969
1276
  actions.http_stop && actions.http_stop(),
@@ -975,7 +1282,6 @@ actions.halt = async () => {
975
1282
  }
976
1283
  actions.exit = async status => {
977
1284
  if (exiting) return;
978
- exiting = true;
979
1285
  if (typeof status !== 'number') status = 0;
980
1286
  await actions.halt();
981
1287
  if (log_verbose) log('exit');
@@ -1007,6 +1313,7 @@ log(`rtjscomp v${
1007
1313
  IS_BUN ? 'bun' : 'node'
1008
1314
  } on ${
1009
1315
  process.platform
1316
+ .replace('win32', 'windows')
1010
1317
  }`);
1011
1318
 
1012
1319
  await file_keep_new(PATH_CONFIG + 'init.js', async data => {
@@ -1107,7 +1414,7 @@ await file_keep_new(PATH_CONFIG + 'init.js', async data => {
1107
1414
 
1108
1415
  await fsp.writeFile(
1109
1416
  'rtjscomp.json',
1110
- JSON.stringify(json, null, 2),
1417
+ JSON.stringify(json, null, '\t') + '\n',
1111
1418
  'utf8'
1112
1419
  );
1113
1420
  log('[deprecated] config files found, rtjscomp.json written, please delete config files');
@@ -1156,7 +1463,7 @@ actions.http_kill = async () => {
1156
1463
  if (http_status_target) return;
1157
1464
  log('kill http');
1158
1465
  await Promise.all(
1159
- Array.from(http_connections.values())
1466
+ [...http_connections.values()]
1160
1467
  .map(connection => connection.destroy())
1161
1468
  );
1162
1469
  if (log_verbose) log('killed http');
@@ -1207,7 +1514,8 @@ try {
1207
1514
  if (https_status_target) return;
1208
1515
  log('kill https');
1209
1516
  await Promise.all(
1210
- Array.from(https_connections.values()).map(connection => connection.destroy())
1517
+ [...https_connections.values()]
1518
+ .map(connection => connection.destroy())
1211
1519
  );
1212
1520
  if (log_verbose) log('killed https');
1213
1521
  https_connections.clear();
@@ -1225,6 +1533,8 @@ await file_keep_new('rtjscomp.json', data => {
1225
1533
  throw 'must contain {}';
1226
1534
  }
1227
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);
1228
1538
  const path_aliases_new = get_prop_map(data, 'path_aliases');
1229
1539
  const path_ghosts_new = get_prop_list(data, 'path_ghosts');
1230
1540
  const path_hiddens_new = get_prop_list(data, 'path_hiddens');
@@ -1242,22 +1552,37 @@ await file_keep_new('rtjscomp.json', data => {
1242
1552
  throw 'unknown: ' + keys_left.join(', ');
1243
1553
  }
1244
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
+ }
1245
1564
 
1565
+ gz_enabled = gzip_level_new > 0;
1566
+ GZIP_OPTIONS.level = gzip_level_new;
1567
+ log_verbose = log_verbose_new;
1246
1568
  if (path_ghosts_new) {
1247
1569
  path_ghosts.clear();
1248
1570
  for (const key of path_ghosts_new) {
1571
+ config_path_check(key);
1249
1572
  path_ghosts.add(key);
1250
1573
  }
1251
1574
  }
1252
1575
  if (path_hiddens_new) {
1253
1576
  path_hiddens.clear();
1254
1577
  for (const key of path_hiddens_new) {
1578
+ config_path_check(key);
1255
1579
  path_hiddens.add(key);
1256
1580
  }
1257
1581
  }
1258
1582
  if (path_statics_new) {
1259
1583
  path_statics.clear();
1260
1584
  for (const key of path_statics_new) {
1585
+ config_path_check(key);
1261
1586
  path_statics.add(key);
1262
1587
  }
1263
1588
  }
@@ -1266,6 +1591,8 @@ await file_keep_new('rtjscomp.json', data => {
1266
1591
  path_aliases_reverse.clear();
1267
1592
  path_aliases_templates.clear();
1268
1593
  for (const [key, value] of path_aliases_new) {
1594
+ config_path_check(key, true);
1595
+ config_path_check(value);
1269
1596
  if (key.includes('*')) {
1270
1597
  const path_split = key.split('/');
1271
1598
  const first = path_split.shift();
@@ -1282,7 +1609,13 @@ await file_keep_new('rtjscomp.json', data => {
1282
1609
  }
1283
1610
  else {
1284
1611
  path_aliases.set(key, value);
1285
- path_aliases_reverse.set(value, '/' + key);
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
+ }
1286
1619
  }
1287
1620
  }
1288
1621
  }
@@ -1304,8 +1637,12 @@ await file_keep_new('rtjscomp.json', data => {
1304
1637
  }
1305
1638
  }
1306
1639
 
1640
+ for (const path of services_new) config_path_check(path);
1307
1641
  const promises = [
1308
- services_list_react(services_new),
1642
+ services_list_react(
1643
+ services_new
1644
+ .filter(path => path.charCodeAt(0) !== 35)
1645
+ ),
1309
1646
  ];
1310
1647
 
1311
1648
  if (port_http_new !== port_http) {