rtjscomp 0.8.1

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 ADDED
@@ -0,0 +1,67 @@
1
+ # rtjscomp
2
+
3
+ easy to use http server that allows for using javascript just as php was used back in the days
4
+
5
+ > [!WARNING]
6
+ > as the issues indicate, a lot of breaking changes are ahead.
7
+ > make sure to check for these in future until version 1.0.0 is relessed.
8
+
9
+ ## Usage
10
+
11
+ go into the directory where you want to have your project and run
12
+
13
+ ```console
14
+ $ npx rtjscomp
15
+ ```
16
+
17
+ or in case you prefer [bun](https://bun.sh):
18
+
19
+ ```console
20
+ $ bunx rtjscomp
21
+ ```
22
+
23
+ and now http://localhost:8080 offers a greeting!
24
+
25
+ for developing rtjscomp itself, clone this repository and run
26
+
27
+ ```console
28
+ $ npm install
29
+ $ npm start
30
+ ```
31
+
32
+ ## Files
33
+
34
+ when run first time, it will be created with some defaults. all files in config/public are watched, no reload command needed.
35
+
36
+ ### Directories
37
+
38
+ - config dir, has simple txt files (this will change soon!)
39
+ - data dir, services store their data here
40
+ - public dir, containing dynamic and static offerings and also services
41
+
42
+ ### File types
43
+
44
+ - static files like .png in public dir are sent to requests 1:1
45
+ - dynamic files like .html are transpiled to javascript and thereby compiled to machine code by the js engine as they are first time requested -> "rtjscomp"
46
+ - .service.js files are executed and their `this` can be accessed by other files, they are not accessible to outside
47
+
48
+ ## API
49
+
50
+ in every served dynamic file (like .html), you can insert `<?` and `?>` tags to insert javascript code that is executed server-side. `<?= ... ?>` can be used to insert the result of an expression.
51
+ request-independent services can be created using .service.js files referenced in services.txt.
52
+ in both file types (dynamic served and services), you can use all node/bun methods including `require`, but also those:
53
+
54
+ - `service_require(service path)`: returns the matching service object
55
+ - `service_require_try(service path)`: returns the matching service object or null if not found or if disabled
56
+ - `rtjscomp`: has these properties/methods:
57
+ - `actions`: an object with methods for server control (http[s]_[start|stop|restart|kill], log_clear, halt, exit)
58
+ - `async data_load(path)`: reads the file in data directory and returns its content or null
59
+ - `async data_load_watch(path, callback(content))`: executes callback first and on every change
60
+ - `async data_save(path, content)`: writes the content to the file in data directory
61
+
62
+ ## Supported environments
63
+
64
+ - node 4.0.0 or higher
65
+ - bun
66
+
67
+ any os
@@ -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
+ ???
@@ -0,0 +1,7 @@
1
+ # put file types here that should be compiled
2
+ # <? ... ?> will only be executed in those!
3
+
4
+ html
5
+ txt
6
+ json
7
+ events
@@ -0,0 +1,21 @@
1
+ html:text/html; charset=utf-8
2
+ txt:text/plain; charset=utf-8
3
+ xml:application/xml; charset=utf-8
4
+ rss:application/rss+xml; charset=utf-8
5
+ js:text/javascript; charset=utf-8
6
+ json:application/json; charset=utf-8
7
+ hta:application/hta
8
+ css:text/css; charset=utf-8
9
+ ico:image/x-icon
10
+ jpg:image/jpeg
11
+ png:image/png
12
+ bpg:image/bpg
13
+ zip:application/zip
14
+ apk:application/zip
15
+ gz:application/gzip
16
+ xz:application/x-xz
17
+ mp3:audio/mpeg3
18
+ mid:audio/midi
19
+ flac:audio/flac
20
+ pdf:application/pdf
21
+ events:text/event-stream
@@ -0,0 +1,8 @@
1
+ # these file types should not be sent compressed
2
+
3
+ png
4
+ jpg
5
+ pdf
6
+ zip
7
+ gz
8
+ xz
@@ -0,0 +1,5 @@
1
+ # the part before the : will be replaced by the part after it
2
+ # insert * to have parameters extracted from the path
3
+
4
+ :index.html
5
+ zahl/*zahl:index.html
@@ -0,0 +1 @@
1
+ 8080
File without changes
@@ -0,0 +1,2 @@
1
+ # list paths to service.js files in your public directory
2
+ # example "counter" to enable public/counter.service.js
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "rtjscomp",
3
+ "author": "L3P3 <dev@l3p3.de> (https://l3p3.de)",
4
+ "license": "Zlib",
5
+ "version": "0.8.1",
6
+ "description": "php-like server but with javascript",
7
+ "keywords": [
8
+ "http",
9
+ "javascript",
10
+ "server"
11
+ ],
12
+ "dependencies": {
13
+ "ipware": "latest",
14
+ "parse-multipart-data": "latest",
15
+ "querystring": "latest"
16
+ },
17
+ "scripts": {
18
+ "start": "./rtjscomp.js"
19
+ },
20
+ "bin": {
21
+ "rtjscomp": "./rtjscomp.js"
22
+ }
23
+ }
@@ -0,0 +1,4 @@
1
+ <?
2
+ const zahl = Number(input.zahl || 42);
3
+ ?><h1>Hallo, Welt!</h1>
4
+ <p>Zahl: <?= zahl ?></p>
package/rtjscomp.js ADDED
@@ -0,0 +1,1016 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ RTJSCOMP by L3P3, 2017-2025
4
+ */
5
+
6
+ "use strict";
7
+
8
+ (async () => {
9
+
10
+ const http = require('http');
11
+ const url = require('url');
12
+ const fs = require('fs');
13
+ const fsp = require('fs/promises');
14
+ const multipart_parse = require('parse-multipart-data').parse;
15
+ const zlib = require('zlib');
16
+ const request_ip_get = require('ipware')().get_ip;
17
+ const querystring_parse = require('querystring').decode;
18
+ const resolve_options = {paths: [require('path').resolve()]};
19
+
20
+ const VERSION = require('./package.json').version;
21
+ const PATH_PUBLIC = 'public/';
22
+ const PATH_CONFIG = 'config/';
23
+ const PATH_DATA = 'data/';
24
+ const GZIP_OPTIONS = {level: 9};
25
+ const AGENT_CHECK_BOT = /bot|googlebot|crawler|spider|robot|crawling|favicon/i;
26
+ 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;
27
+ const HTTP_LIST_REG = /,\s*/;
28
+
29
+ let port_http = 0;
30
+ let port_https = 0;
31
+ const file_type_mimes = new Map;
32
+ const file_type_dyns = new Set;
33
+ const file_type_nocompress = new Set;
34
+ /// forced static files
35
+ const file_raws = new Set;
36
+ /// hidden files
37
+ const file_privates = new Set;
38
+ /// files where requests should be totally ignored
39
+ const file_blocks = new Set;
40
+ /// any path -> file
41
+ const path_aliases = new Map;
42
+ const path_aliases_templates = new Map;
43
+ const services = new Set;
44
+ /// compiled file handlers
45
+ const file_cache_functions = new Map;
46
+ const actions = {};
47
+ const rtjscomp = global.rtjscomp = {
48
+ actions,
49
+ version: VERSION,
50
+ };
51
+
52
+ if (!Object.fromEntries) {
53
+ Object.fromEntries = entries => {
54
+ const object = {};
55
+ for (const entry of entries) object[entry[0]] = entry[1];
56
+ return object;
57
+ };
58
+ }
59
+
60
+ // legacy, will be removed soon!
61
+ global.globals = rtjscomp;
62
+ global.actions = rtjscomp.actions;
63
+ global.data_load = name => {
64
+ log('[deprecated!] load: ' + name);
65
+ try {
66
+ return fs.readFileSync(PATH_DATA + name, 'utf8');
67
+ }
68
+ catch (err) {
69
+ return null;
70
+ }
71
+ }
72
+ global.data_save = (name, data) => (
73
+ log('[deprecated!] save: ' + name),
74
+ fs.writeFileSync(PATH_DATA + name, data, 'utf8')
75
+ )
76
+ global.number_check_int = number => (
77
+ Math.floor(number) === number
78
+ )
79
+ global.number_check_uint = number => (
80
+ number >= 0 && number_check_int(number)
81
+ )
82
+
83
+ rtjscomp.data_load = async name => {
84
+ log('load: ' + name);
85
+ const data = await fsp.readFile(PATH_DATA + name, 'utf8').catch(() => null);
86
+ return name.endsWith('.json') ? JSON.parse(data || null) : data;
87
+ }
88
+ rtjscomp.data_load_watch = (name, callback) => (
89
+ file_keep_new(PATH_DATA + name, data => (
90
+ log('load: ' + name),
91
+ callback(
92
+ name.endsWith('.json')
93
+ ? JSON.parse(data || null)
94
+ : data
95
+ )
96
+ ))
97
+ )
98
+ rtjscomp.data_save = (name, data) => (
99
+ log('save: ' + name),
100
+ fsp.writeFile(
101
+ PATH_DATA + name,
102
+ name.endsWith('.json') ? JSON.stringify(data) : data,
103
+ 'utf8'
104
+ )
105
+ )
106
+
107
+ const custom_require_cache = new Map;
108
+ const custom_require = path => {
109
+ let result = custom_require_cache.get(path);
110
+ if (result != null) return result;
111
+
112
+ custom_require_cache.set(
113
+ path,
114
+ result = require(
115
+ require.resolve(path, resolve_options)
116
+ )
117
+ );
118
+ return result;
119
+ }
120
+
121
+ const services_active = new Map;
122
+ const services_list_react = async () => {
123
+ await Promise.all(
124
+ Array.from(services_active.entries())
125
+ .filter(([path, _]) => !services.has(path))
126
+ .map(([_, service_object]) => service_stop(service_object, true))
127
+ );
128
+ for (const path of services) {
129
+ if (!services_active.has(path)) await service_start(path);
130
+ }
131
+ }
132
+ const service_start = async path => {
133
+ const service_object = {
134
+ path,
135
+ start: null,
136
+ started: false,
137
+ stop: null,
138
+ stopped: false,
139
+ };
140
+
141
+ await file_keep_new(PATH_PUBLIC + path + '.service.js', async file_content => {
142
+ if (file_content === null) {
143
+ log('error, service file not found: ' + path);
144
+ await service_stop(service_object, true);
145
+ return;
146
+ }
147
+ await service_stop_handler(service_object);
148
+ await service_start_inner(path, service_object, file_content);
149
+ });
150
+ }
151
+ const service_start_inner = async (path, service_object, file_content) => {
152
+ try {
153
+ const fun = new Function(
154
+ 'require',
155
+ `const log=a=>rtjscomp.log(${
156
+ JSON.stringify(path + ': ')
157
+ }+a);${file_content}`
158
+ );
159
+ fun.call(service_object, custom_require);
160
+ }
161
+ catch (err) {
162
+ log(`error in service ${path}: ${err.message}`);
163
+ await service_stop(service_object, false);
164
+ return;
165
+ }
166
+
167
+ if (service_object.start) {
168
+ try {
169
+ await service_object.start();
170
+ service_object.start = null;
171
+ service_object.started = true;
172
+ }
173
+ catch (err) {
174
+ services_active.delete(path);
175
+ log(`error while starting ${path}: ${err.message}`);
176
+ return;
177
+ }
178
+ }
179
+ services_active.set(path, service_object);
180
+ log('service started: ' + path);
181
+ }
182
+ const services_shutdown = () => (
183
+ log('shutdown services...'),
184
+ Promise.all(
185
+ Array.from(services_active.values())
186
+ .map(service_object => service_stop(service_object, true))
187
+ )
188
+ )
189
+ const service_stop = async (service_object, forget) => {
190
+ service_object.stopped = true;
191
+ if (forget) fs.unwatchFile(PATH_PUBLIC + service_object.path + '.service.js');
192
+ await service_stop_handler(service_object);
193
+ services_active.delete(service_object.path);
194
+ log('service stopped: ' + service_object.path);
195
+ }
196
+ const service_stop_handler = async service_object => {
197
+ if (service_object.stop) {
198
+ try {
199
+ await service_object.stop();
200
+ service_object.stop = null;
201
+ }
202
+ catch (err) {
203
+ log(`error while stopping ${service_object.path}: ${err.message}`);
204
+ }
205
+ }
206
+ }
207
+ global.service_require = path => {
208
+ const service = services_active.get(path);
209
+ if (service) return service;
210
+ throw new Error('service required: ' + path);
211
+ }
212
+ global.service_require_try = path => (
213
+ services_active.get(path) || null
214
+ )
215
+
216
+ const map_generate_bol = (set, data) => {
217
+ set.clear();
218
+ for (const key of data.split('\n'))
219
+ if (
220
+ key.length > 0 &&
221
+ key.charCodeAt(0) !== 35
222
+ ) {
223
+ set.add(key);
224
+ }
225
+ }
226
+ const map_generate_equ = (map, data) => {
227
+ map.clear();
228
+ for (const entry of data.split('\n'))
229
+ if (
230
+ entry.length > 0 &&
231
+ entry.charCodeAt(0) !== 35
232
+ ) {
233
+ const equ = entry.split(':');
234
+ map.set(equ[0], equ[1] || '');
235
+ }
236
+ }
237
+
238
+ const file_compare = (curr, prev, path) => (
239
+ curr.mtime > prev.mtime && (
240
+ log('file changed: ' + path),
241
+ true
242
+ )
243
+ )
244
+ const file_watch = (path, callback) => (
245
+ fs.watchFile(path, (curr, prev) => {
246
+ if (file_compare(curr, prev, path)) {
247
+ fs.unwatchFile(path);
248
+ callback();
249
+ }
250
+ })
251
+ )
252
+ const file_keep_new = async (path, callback) => {
253
+ try {
254
+ await callback(await fsp.readFile(path, 'utf8'));
255
+ fs.watchFile(path, async (curr, prev) => {
256
+ if (file_compare(curr, prev, path)) {
257
+ await callback(
258
+ await fsp.readFile(path, 'utf8').catch(() => null)
259
+ );
260
+ }
261
+ });
262
+ }
263
+ catch (err) {
264
+ await callback(null);
265
+ }
266
+ }
267
+
268
+ let log_history = rtjscomp.log_history = [];
269
+ actions.log_clear = () => {
270
+ log_history = rtjscomp.log_history = [];
271
+ }
272
+ const log = rtjscomp.log = msg => (
273
+ console.log(msg),
274
+ log_history.push(msg),
275
+ spam('log', [msg])
276
+ )
277
+
278
+ const spam_enabled = fs.existsSync('spam.csv');
279
+ rtjscomp.spam_history = '';
280
+ actions.spam_save = async (muted = false) => {
281
+ if (!spam_enabled) return;
282
+
283
+ try {
284
+ fsp.appendFile('spam.csv', rtjscomp.spam_history, 'utf8');
285
+ rtjscomp.spam_history = '';
286
+ muted || log('spam.csv saved');
287
+ }
288
+ catch (err) {
289
+ log('error saving spam.csv: ' + err.message);
290
+ }
291
+ }
292
+ const spam = (type, data) => {
293
+ if (!spam_enabled) return;
294
+
295
+ rtjscomp.spam_history += (
296
+ Date.now() +
297
+ ',' +
298
+ type +
299
+ ',' +
300
+ JSON.stringify(data) +
301
+ '\n'
302
+ );
303
+
304
+ if (rtjscomp.spam_history.length >= 1e5) {
305
+ actions.spam_save();
306
+ }
307
+ }
308
+
309
+ const request_handle = async (request, response, https) => {
310
+ const request_method = request.method;
311
+ const request_headers = request.headers;
312
+ const request_ip = request_ip_get(request).clientIp;
313
+
314
+ if ('x-forwarded-proto' in request_headers) {
315
+ https = request_headers['x-forwarded-proto'] === 'https';
316
+ }
317
+
318
+ spam('request', [https, request.url, request_ip]);
319
+
320
+ try {
321
+ const request_url_parsed = url.parse(request.url, false);
322
+
323
+ let path = request_url_parsed.pathname || '';
324
+
325
+ // ignore (timeout) many hack attempts
326
+ if (path.includes('php') || path.includes('sql')) return;
327
+
328
+ // remove leading/trailing /
329
+ while (path.charCodeAt(0) === 47) {
330
+ path = path.substring(1);
331
+ }
332
+ while (path.charCodeAt(path.length - 1) === 47) {
333
+ path = path.substring(0, path.length - 1);
334
+ }
335
+
336
+ if (path.includes('..') || path.includes('~')) throw 403;
337
+
338
+ if (file_blocks.has(path)) return;
339
+
340
+ response.setHeader('Server', 'l3p3 rtjscomp v' + VERSION);
341
+ response.setHeader('Access-Control-Allow-Origin', '*');
342
+
343
+ let path_params = null;
344
+ let request_body_promise = null;
345
+
346
+ if (path_aliases.has(path)) {
347
+ response.setHeader(
348
+ 'Content-Location',
349
+ path = path_aliases.get(path)
350
+ );
351
+ }
352
+ else { // aliases with *
353
+ const path_split = path.split('/');
354
+ const templates = path_aliases_templates.get(path_split[0]);
355
+ if (templates) {
356
+ path_split.shift();
357
+ template: for (const template_pair of templates) {
358
+ const template = template_pair[0];
359
+ const template_length = template.length;
360
+ if (template_length !== path_split.length) continue;
361
+ const params = {};
362
+ for (let i = 0; i < template_length; ++i) {
363
+ if (template[i].charCodeAt(0) === 42) {
364
+ if (template[i].length > 1) params[template[i].substr(1)] = path_split[i];
365
+ }
366
+ else if (template[i] !== path_split[i]) continue template;
367
+ }
368
+ response.setHeader('Content-Location', path = template_pair[1]);
369
+ path_params = params;
370
+ break;
371
+ }
372
+ }
373
+ }
374
+
375
+ const file_type_index = path.lastIndexOf('.');
376
+ // no type ending -> dir?
377
+ if (file_type_index <= path.lastIndexOf('/')) throw 404;
378
+ const file_type = path.substring(
379
+ file_type_index + 1
380
+ ).toLowerCase();
381
+
382
+ let file_gz_enabled = (
383
+ 'accept-encoding' in request_headers &&
384
+ !file_type_nocompress.has(file_type) &&
385
+ request_headers['accept-encoding'].split(HTTP_LIST_REG).includes('gzip')
386
+ );
387
+
388
+ const file_dyn_enabled = (
389
+ file_type_dyns.has(file_type) &&
390
+ !file_raws.has(path)
391
+ );
392
+
393
+ if (file_dyn_enabled) {
394
+ if (
395
+ request_method !== 'GET' &&
396
+ 'content-length' in request_headers
397
+ ) {
398
+ request_body_promise = new Promise(resolve => {
399
+ const request_body_chunks = [];
400
+ request.on('data', chunk => {
401
+ request_body_chunks.push(chunk);
402
+ });
403
+ request.on('end', chunk => {
404
+ chunk && request_body_chunks.push(chunk);
405
+ resolve(Buffer.concat(request_body_chunks));
406
+ });
407
+ });
408
+ }
409
+ }
410
+ else if (request_method !== 'GET') {
411
+ throw 400;
412
+ }
413
+
414
+ let file_function = null;
415
+ let file_stat = null;
416
+ const path_real = PATH_PUBLIC + path;
417
+
418
+ if (
419
+ file_dyn_enabled &&
420
+ file_cache_functions.has(path)
421
+ ) {
422
+ file_function = file_cache_functions.get(path);
423
+ }
424
+ else {
425
+ log(`load ${
426
+ file_dyn_enabled
427
+ ? 'dynam'
428
+ : 'stat'
429
+ }ic file: ${path}`);
430
+
431
+ if (
432
+ file_privates.has(path) ||
433
+ path.endsWith('.service.js')
434
+ ) throw 403;
435
+ if (!fs.existsSync(path_real)) throw 404;
436
+ file_stat = fs.statSync(path_real);
437
+ if (file_stat.isDirectory()) throw 403;
438
+
439
+ if (file_dyn_enabled) { // compile file
440
+ const file_content = fs.readFileSync(path_real, 'utf8');
441
+ try {
442
+ if (file_content.includes('\r')) {
443
+ throw 'illegal line break, must be unix';
444
+ }
445
+ const file_content_length = file_content.length;
446
+
447
+ let code = `async (input,output,request,response,require)=>{const log=a=>rtjscomp.log(${
448
+ JSON.stringify(path + ': ')
449
+ }+a);`;
450
+
451
+ let section_dynamic = false;
452
+ let index_start = 0;
453
+ let index_end = 0;
454
+
455
+ while (index_end < file_content_length) {
456
+ if (section_dynamic) {
457
+ if (
458
+ (
459
+ index_end = file_content.indexOf(
460
+ '?>',
461
+ // skip `<?`
462
+ index_start = index_end + 2
463
+ )
464
+ ) < 0
465
+ ) throw '"?>" missing';
466
+ section_dynamic = false;
467
+ // section not empty?
468
+ if (index_start < index_end) {
469
+ // `<?`?
470
+ if (file_content.charCodeAt(index_start) !== 61) {
471
+ code += (
472
+ file_content.substring(
473
+ index_start,
474
+ index_end
475
+ ) +
476
+ ';'
477
+ );
478
+ }
479
+ else { // `<?=`?
480
+ code += `output.write(''+(${
481
+ file_content.substring(
482
+ ++index_start,
483
+ index_end
484
+ )
485
+ }));`;
486
+ }
487
+ }
488
+ // skip `?>`
489
+ index_end += 2;
490
+ }
491
+ else { // static section
492
+ // still something dynamic coming?
493
+ if (
494
+ (
495
+ index_end = file_content.indexOf(
496
+ '<?',
497
+ index_start = index_end
498
+ )
499
+ ) > -1
500
+ ) {
501
+ section_dynamic = true;
502
+ }
503
+ else {
504
+ index_end = file_content_length;
505
+ }
506
+
507
+ // section not empty?
508
+ if (index_start < index_end) {
509
+ code += `output.write(${
510
+ JSON.stringify(
511
+ file_content.substring(index_start, index_end)
512
+ )
513
+ });`;
514
+ }
515
+ }
516
+ }
517
+
518
+ try {
519
+ file_function = eval(code += '}');
520
+ }
521
+ catch (err) {
522
+ throw err.message;
523
+ }
524
+ }
525
+ catch (err) {
526
+ log('compile error: ' + err);
527
+ throw 500;
528
+ }
529
+
530
+ file_cache_functions.set(path, file_function);
531
+ file_watch(path_real, () => {
532
+ file_cache_functions.delete(path);
533
+ });
534
+ }
535
+ }
536
+
537
+ response.statusCode = 200;
538
+ response.setHeader(
539
+ 'Content-Type',
540
+ file_type_mimes.get(file_type) || file_type_mimes.get('txt')
541
+ );
542
+
543
+ if (file_dyn_enabled) { // dynamic file
544
+ const file_function_input = path_params || {};
545
+
546
+ if (request_headers['cookie'])
547
+ for (let cookie of request_headers['cookie'].split(';')) {
548
+ cookie = cookie.trim();
549
+ const index_equ = cookie.indexOf('=');
550
+ if (index_equ > 0) {
551
+ file_function_input[
552
+ cookie
553
+ .substring(0, index_equ)
554
+ .trimRight()
555
+ ] = decodeURI(
556
+ cookie
557
+ .substr(index_equ + 1)
558
+ .trimLeft()
559
+ );
560
+ }
561
+ else if (index_equ < 0) {
562
+ file_function_input[cookie] = undefined;
563
+ }
564
+ }
565
+
566
+ if (request_headers['x-input']) {
567
+ Object.assign(
568
+ file_function_input,
569
+ querystring_parse(request_headers['x-input'])
570
+ );
571
+ }
572
+
573
+ if (request_url_parsed.query) {
574
+ try {
575
+ Object.assign(
576
+ file_function_input,
577
+ querystring_parse(request_url_parsed.query)
578
+ );
579
+ }
580
+ catch (err) {
581
+ log('request query error: ' + err.message);
582
+ throw 400;
583
+ }
584
+ }
585
+
586
+ if (request_body_promise) {
587
+ try {
588
+ const content_type = request.headers['content-type'] || '';
589
+ const body_raw = file_function_input['body'] = await request_body_promise;
590
+ let body = null;
591
+ switch (content_type.split(';')[0]) {
592
+ case 'application/x-www-form-urlencoded':
593
+ body = querystring_parse(body_raw.toString());
594
+ break;
595
+ case 'application/json':
596
+ body = JSON.parse(body_raw.toString());
597
+ break;
598
+ case 'multipart/form-data': {
599
+ body = Object.fromEntries(
600
+ multipart_parse(
601
+ body_raw,
602
+ content_type.split('boundary=')[1].split(';')[0]
603
+ )
604
+ .map(({ name, ...value }) => [
605
+ name,
606
+ value.type ? value : value.data.toString()
607
+ ])
608
+ );
609
+ }
610
+ }
611
+ if (body) {
612
+ Object.assign(file_function_input, body);
613
+ }
614
+ }
615
+ catch (err) {
616
+ log('request body error: ' + err.message);
617
+ throw 400;
618
+ }
619
+ }
620
+
621
+ const request_headers_user_agent = file_function_input['user_agent'] = request_headers['user-agent'];
622
+ file_function_input['bot'] = AGENT_CHECK_BOT.test(request_headers_user_agent);
623
+ file_function_input['mobil'] = AGENT_CHECK_MOBIL.test(request_headers_user_agent);
624
+
625
+ file_function_input['https'] = https;
626
+ file_function_input['ip'] = request_ip;
627
+ file_function_input['method'] = request_method.toLowerCase();
628
+ file_function_input['path'] = request_url_parsed.pathname;
629
+
630
+ let file_function_output;
631
+ response.setHeader('Cache-Control', 'no-cache, no-store');
632
+
633
+ if (file_gz_enabled) {
634
+ response.setHeader('Content-Encoding', 'gzip');
635
+
636
+ (
637
+ file_function_output = zlib.createGzip(GZIP_OPTIONS)
638
+ ).pipe(response);
639
+ }
640
+ else {
641
+ file_function_output = response;
642
+ }
643
+
644
+ spam('execute', [
645
+ path,
646
+ Object.fromEntries(
647
+ Object.entries(file_function_input)
648
+ .filter(e => e[0] !== 'body')
649
+ .map(e => e[0] === 'password' ? [e[0], '***'] : e)
650
+ .map(e => e[0] === 'file' ? [e[0], '...'] : e)
651
+ .map(e => (typeof e[1] === 'object' && !e[1].length) ? [e[0], Object.keys(e[1]).slice(0, 20)] : e)
652
+ .map(e => (e[0] !== 'user_agent' && typeof e[1] === 'string' && e[1].length > 20) ? [e[0], e[1].substr(0, 20) + '...'] : e)
653
+ )
654
+ ]);
655
+
656
+ try {
657
+ await file_function(
658
+ file_function_input,
659
+ file_function_output,
660
+ request,
661
+ response,
662
+ custom_require
663
+ );
664
+ file_function_output.end();
665
+ }
666
+ catch (err) {
667
+ if (err instanceof Error) {
668
+ log(`error in file ${path}: ${err.message}`);
669
+
670
+ if (err.message.startsWith('service required: ')) {
671
+ err = 503;
672
+ }
673
+ }
674
+ if (typeof err === 'number') {
675
+ response.removeHeader('Content-Encoding');
676
+ throw err;
677
+ }
678
+
679
+ file_function_output.end((
680
+ file_type === 'html'
681
+ ? '<hr>'
682
+ : '\n\n---\n'
683
+ ) + 'ERROR!');
684
+ }
685
+ }
686
+ else { // static file
687
+ let file_data = null;
688
+
689
+ if (
690
+ file_gz_enabled &&
691
+ file_stat.size > 90 &&
692
+ fs.existsSync(path_real + '.gz')
693
+ ) {
694
+ file_data = fs.createReadStream(path_real + '.gz');
695
+ }
696
+ else {
697
+ file_gz_enabled = false;
698
+ file_data = fs.createReadStream(path_real);
699
+ }
700
+
701
+ spam('static_send', [path, file_gz_enabled]);
702
+ response.setHeader('Cache-Control', 'public, max-age=600');
703
+
704
+ if (file_gz_enabled) {
705
+ response.setHeader('Content-Encoding', 'gzip');
706
+ }
707
+ else {
708
+ response.setHeader('Content-Length', file_stat.size);
709
+ }
710
+
711
+ file_data.pipe(response);
712
+ }
713
+ }
714
+ catch (err) {
715
+ // catch internal errors
716
+ if (typeof err !== 'number') {
717
+ console.error(err);
718
+ err = 500;
719
+ }
720
+
721
+ if (err >= 400) {
722
+ log(`error ${err} at request: ${request_ip}; ${request.url}`);
723
+ }
724
+
725
+ response.writeHead(err, {
726
+ 'Content-Type': 'text/html',
727
+ 'Cache-Control': 'no-cache, no-store',
728
+ });
729
+ response.end(`<!DOCTYPE html><html><body><h1>HTTP ${err}: ${http.STATUS_CODES[err] || 'Error'}</h1></body></html>`);
730
+ }
731
+ }
732
+
733
+ let exiting = false;
734
+ actions.halt = async () => {
735
+ await Promise.all([
736
+ actions.http_stop(),
737
+ actions.https_stop && actions.https_stop(),
738
+ services_shutdown(),
739
+ ].filter(Boolean));
740
+ await actions.spam_save();
741
+ log('stopped everything');
742
+ }
743
+ actions.exit = async status => {
744
+ if (exiting) return;
745
+ if (typeof status !== 'number') status = 0;
746
+ await actions.halt();
747
+ log('exiting...');
748
+ exiting = true;
749
+ process.exit(status);
750
+ }
751
+
752
+ process.on('uncaughtException', err => {
753
+ err = err.message || err;
754
+ if (typeof err === 'symbol') err = err.toString();
755
+ log('error uncaughtException: ' + err);
756
+ console.log(err);
757
+ actions.exit(1);
758
+ });
759
+ process.on('unhandledRejection', err => {
760
+ log('error unhandledRejection: ' + (err.message || err));
761
+ console.log(err);
762
+ actions.exit(1);
763
+ });
764
+ process.on('exit', actions.exit);
765
+ process.on('SIGINT', actions.exit);
766
+ //process.on('SIGUSR1', actions.exit);
767
+ process.on('SIGUSR2', actions.exit);
768
+ process.on('SIGTERM', actions.exit);
769
+
770
+ log(`rtjscomp v${VERSION} in ${typeof Bun === 'undefined' ? 'node' : 'bun'} on ${process.platform}`);
771
+
772
+ // initial
773
+ await Promise.all([
774
+ fsp.stat(PATH_CONFIG).catch(_ => null),
775
+ fsp.stat(PATH_DATA).catch(_ => null),
776
+ fsp.stat(PATH_PUBLIC).catch(_ => null),
777
+ ]).then(([stat_config, stat_data, stat_public]) => {
778
+ if (!stat_config) {
779
+ log('creating config template directory');
780
+ fs.mkdirSync(PATH_CONFIG);
781
+ fs.mkdirSync(PATH_CONFIG + 'ssl');
782
+ for (const file of 'file_type_dyns,file_type_mimes,file_type_nocompress,path_aliases,port_http,port_https,services'.split(',')) {
783
+ fs.copyFileSync(
784
+ __dirname + '/' + PATH_CONFIG + file + '.txt',
785
+ PATH_CONFIG + file + '.txt'
786
+ );
787
+ }
788
+ }
789
+ if (!stat_data) {
790
+ fs.mkdirSync(PATH_DATA);
791
+ }
792
+ if (!stat_public) {
793
+ fs.cpSync(
794
+ __dirname + '/' + PATH_PUBLIC,
795
+ PATH_PUBLIC,
796
+ {recursive: true}
797
+ );
798
+ }
799
+ });
800
+
801
+ await Promise.all([
802
+ file_keep_new(PATH_CONFIG + 'init.js', data => {
803
+ if (!data) return;
804
+ log('[deprecated!] run global init script');
805
+ try {
806
+ var require = custom_require;
807
+ eval(data);
808
+ }
809
+ catch (err) {
810
+ log('error in init.js: ' + err.message);
811
+ }
812
+ }),
813
+ file_keep_new(PATH_CONFIG + 'services.txt', async data => {
814
+ log('load service list');
815
+ map_generate_bol(services, data);
816
+ await services_list_react();
817
+ }),
818
+ file_keep_new(PATH_CONFIG + 'file_type_mimes.txt', data => {
819
+ log('load file type map');
820
+ map_generate_equ(file_type_mimes, data);
821
+ if (!file_type_mimes.has('txt')) {
822
+ file_type_mimes.set('txt', 'text/plain; charset=utf-8');
823
+ }
824
+ }),
825
+ file_keep_new(PATH_CONFIG + 'path_aliases.txt', data => {
826
+ log('load path aliases map');
827
+ map_generate_equ(path_aliases, data);
828
+ path_aliases_templates.clear();
829
+ for (const [key, value] of path_aliases.entries()) {
830
+ const star_index = key.indexOf('*');
831
+ if (star_index < 0) continue;
832
+ path_aliases.delete(key);
833
+ const template = key.split('/');
834
+ const first = template.shift();
835
+ if (path_aliases_templates.has(first)) {
836
+ path_aliases_templates.get(first).push([template, value]);
837
+ }
838
+ else {
839
+ path_aliases_templates.set(first, [
840
+ [template, value],
841
+ ]);
842
+ }
843
+ }
844
+ }),
845
+ file_keep_new(PATH_CONFIG + 'file_type_dyns.txt', data => {
846
+ log('load dynamic file type list');
847
+ map_generate_bol(file_type_dyns, data);
848
+ }),
849
+ file_keep_new(PATH_CONFIG + 'file_type_nocompress.txt', data => {
850
+ log('load non-compressable file list');
851
+ map_generate_bol(file_type_nocompress, data);
852
+ }),
853
+ file_keep_new(PATH_CONFIG + 'file_raws.txt', data => {
854
+ if (!data) return;
855
+ log('load static file list');
856
+ map_generate_bol(file_raws, data);
857
+ }),
858
+ file_keep_new(PATH_CONFIG + 'file_privates.txt', data => {
859
+ if (!data) return;
860
+ log('load private file list');
861
+ map_generate_bol(file_privates, data);
862
+ }),
863
+ file_keep_new(PATH_CONFIG + 'file_blocks.txt', data => {
864
+ if (!data) return;
865
+ log('load blocked file list');
866
+ map_generate_bol(file_blocks, data);
867
+ }),
868
+ ]);
869
+
870
+ let connections_count = 0;
871
+ const server_http = http.createServer(
872
+ (request, response) => request_handle(request, response, false)
873
+ );
874
+ let http_status = false;
875
+ let http_status_target = false;
876
+ const http_connections = new Map;
877
+
878
+ server_http.on('connection', connection => {
879
+ const id = ++connections_count;
880
+ http_connections.set(id, connection);
881
+ connection.on('close', () => {
882
+ http_connections.delete(id);
883
+ });
884
+ });
885
+
886
+ actions.http_start = () => {
887
+ if (http_status) return;
888
+ try {
889
+ server_http.listen(port_http);
890
+ http_status = http_status_target = true;
891
+ log('http started at port ' + port_http);
892
+ }
893
+ catch (err) {
894
+ log('error while starting http: ' + err.message);
895
+ }
896
+ }
897
+ actions.http_restart = () => {
898
+ if (!http_status) actions.http_start();
899
+ else if (http_status_target) {
900
+ http_status_target = false;
901
+ log('http is restarting...');
902
+ server_http.close(() => {
903
+ http_status = false;
904
+ log('http stopped');
905
+ actions.http_start();
906
+ });
907
+ }
908
+ }
909
+ actions.http_stop = async () => {
910
+ if (!http_status_target || !http_status) return;
911
+ http_status_target = false;
912
+ log('http is stopping...');
913
+ await new Promise(resolve => server_http.close(resolve));
914
+ http_status = false;
915
+ log('http stopped');
916
+ }
917
+ actions.http_kill = () => {
918
+ if (http_status_target || !http_status) return;
919
+ log('killing http...');
920
+ for (const connection of http_connections.values()) connection.destroy();
921
+ http_connections.clear();
922
+ }
923
+
924
+ file_keep_new(PATH_CONFIG + 'port_http.txt', data => {
925
+ log('load http port number');
926
+ if (
927
+ !data ||
928
+ isNaN(data = Number(data)) ||
929
+ !number_check_uint(data)
930
+ ) {
931
+ log('error: invalid http port number');
932
+ }
933
+ else if (data !== port_http) {
934
+ port_http = data;
935
+ actions.http_restart();
936
+ }
937
+ });
938
+
939
+ try {
940
+ const https_key = fs.readFileSync(PATH_CONFIG + 'ssl/domain.key');
941
+ const https_cert = fs.readFileSync(PATH_CONFIG + 'ssl/chained.pem');
942
+ const server_https = require('https').createServer(
943
+ {key: https_key, cert: https_cert},
944
+ (request, response) => request_handle(request, response, true)
945
+ );
946
+
947
+ let https_status = false;
948
+ let https_status_target = false;
949
+ const https_connections = new Map;
950
+
951
+ server_https.on('connection', connection => {
952
+ const id = ++connections_count;
953
+ https_connections.set(id, connection);
954
+ connection.on('close', () => {
955
+ https_connections.delete(id);
956
+ });
957
+ });
958
+
959
+ actions.https_start = () => {
960
+ if (https_status) return;
961
+ try {
962
+ server_https.listen(port_https);
963
+ https_status = https_status_target = true;
964
+ log('https started at port ' + port_https);
965
+ }
966
+ catch (err) {
967
+ log('error while starting https: ' + err.message);
968
+ }
969
+ }
970
+ actions.https_restart = () => {
971
+ if (!https_status) actions.https_start();
972
+ else if (https_status_target) {
973
+ https_status_target = false;
974
+ log('https is restarting...');
975
+ server_https.close(function () {
976
+ https_status = false;
977
+ log('https stopped');
978
+ actions.https_start();
979
+ });
980
+ }
981
+ }
982
+ actions.https_stop = async () => {
983
+ if (!https_status_target || !https_status) return;
984
+ https_status_target = false;
985
+ log('https is stopping...');
986
+ await new Promise(resolve => server_https.close(resolve));
987
+ https_status = false;
988
+ log('https stopped');
989
+ }
990
+ actions.https_kill = () => {
991
+ if (https_status_target || !https_status) return;
992
+ log('killing https...');
993
+ for (const connection of https_connections.values()) connection.destroy();
994
+ https_connections.clear();
995
+ }
996
+
997
+ file_keep_new(PATH_CONFIG + 'port_https.txt', data => {
998
+ log('load https port number');
999
+ if (
1000
+ !data ||
1001
+ isNaN(data = Number(data)) ||
1002
+ !number_check_uint(data)
1003
+ ) {
1004
+ log('error: invalid https port number');
1005
+ }
1006
+ else if (data !== port_https) {
1007
+ port_https = data;
1008
+ actions.https_restart();
1009
+ }
1010
+ });
1011
+ }
1012
+ catch (err) {
1013
+ log('https is disabled');
1014
+ }
1015
+
1016
+ })();