-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathserver.js
More file actions
3176 lines (3023 loc) · 94.1 KB
/
server.js
File metadata and controls
3176 lines (3023 loc) · 94.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env node
/*
The MIT License (MIT)
Copyright (c) Александр Меняйло (Aleksandr Meniailo), Mendeo 2025 (thesolve@mail.ru, deorathemen@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
'use strict';
const http = require('http');
const https = require('https');
const path = require('path');
const fs = require('fs');
const os = require('os');
const zlib = require('zlib');
const crypto = require('crypto');
const cluster = require('cluster');
const JSZip = require('jszip');
const cpus = os.cpus;
const net = os.networkInterfaces();
const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json')).toString()).version;
{ //Show version
const v = process.argv.includes('-v') || process.argv.includes('-V') || process.argv.includes('--version');
if (v)
{
console.log(VERSION);
process.exit(0);
}
}
{//Show license
const l = process.argv.includes('-l') || process.argv.includes('-L') || process.argv.includes('--license');
if (l)
{
const license = fs.readFileSync(path.join(__dirname, 'LICENSE.txt')).toString();
console.log(license);
process.exit(0);
}
}
{//Show help
const h = process.argv.includes('-h') || process.argv.includes('-H') || process.argv.includes('--help');
if (h)
{
const help =
`web-shared-folder
This is free software and is provided "as is", without warranty of any kind (MIT License).
Convenient http server on nodejs. Designed to share files and folders on a local network or even
on the Internet via a web interface. Can also be used as a web server to serve static sites.
Capabilities
- Shared directory in a local network or the Internet via a web interface.
- There is an option not only to download data from a specified folder but also to upload data to it.
- It is possible to create users with access only to a specified folder.
- It can work as a server for hosting a static website.
- It is possible to work over HTTPS protocol with an automatic redirect from HTTP.
- If the load is high, then you can enable cluster mode, using all cores of the server machine.
- There is a dark theme.
- Pages are automatically displayed in the user language (only two languages are available at the moment).
Usage
wsf </path/to/folder/for/sharing> <port> [</path/to/key> </path/to/cert>] [--upload or -u]
To output only the version number:
wsf -v [or --version]
To output license
wsf -l [or --license]
Instead of "wsf" you can use the full program name "web-shared-folder".
Basic
To run the server, you need to specify at least the path to the directory to be shared and
the port number on which the server will operate. The folder path is specified in the first
parameter, followed by the port number.
The web-shared-folder server operates as follows:
If there is an index.html file in the root of the specified directory, web-shared-folder
starts working as a web server that hosts a static website and sends index.html when the root URL
is requested. Otherwise, the server switches to a mode that displays the contents of the directory
specified in the startup parameters. In this case, the user can download files and folders located
in that directory.
Example:
wsf . 80
If there is no index.html file in the root directory, this command will start the server on port 80
and share the current folder for browsing and downloading.
The server will be accessible on all available network interfaces (this can be changed by setting
the appropriate environment variable). For example, if the server has an IP address of 192.168.1.2,
you can access it in the local network by entering the address http://192.168.1.2 in your browser.
Since the standard port number 80 is used, the port number does not need to be specified in the
address. If a non-standard port number is used, such as 8080, it must be included in the address:
http://192.168.1.2:8080. On Linux machines, you may need to run the program from root to work with
port 80.
The eye-shaped link next to the file allows you to open it directly in the browser in a separate tab.
This way, you can view, for example, text, photos, or some videos (if the browser supports the encoding).
If you need to give users the ability to upload files to the server in a shared folder, as well as the
ability to rename, delete, and move existing files and folders, then the web-shared-folder server must
be launched with the --upload or -u key.
However, there are limitations on file uploads. The issue is that the uploaded files are transmitted
in a single request, and browsers have a limit on the duration of such a request, typically 5 or 10
minutes (depending on the browser). If the upload duration exceeds this time, it will end with an
error. Another limitation is the total size of files uploaded at once. It should not exceed 2 GiB.
Example:
wsf . 80 -u
In this mode, clicking on the link in the form of an up arrow next to the zip archive file will unpack
this archive into the current directory.
Using the HTTPS
To work over HTTPS, you need to specify the path to the SSL secret key file (usually privkey.pem) and
the path to the SSL certificate file (usually fullchain.pem).
Example:
wsf . 443 /etc/ssl/privkey.pem /etc/ssl/fullchain.pem
or with upload capability enabled:
wsf . 443 /etc/ssl/privkey.pem /etc/ssl/fullchain.pem -u
In the examples above, the standard port number for HTTPS is used: 443. The private key and
certificate files are typically issued by certification authorities organizations. However,
you can also generate a self-signed certificate, for example, using openssl, but in this case,
the browser will display a warning.
Environment variables
Advanced server configuration is done by setting environment variables.
First of all, you can store the command-line arguments described above in these variables and run the
server without any additional arguments.
Below is a list with describing all possible environment variables.
Basic environment variables
WSF_ROOT
Path to the folder that needs to be made publicly accessible.
WSF_PORT
Port that the server will listen on.
WSF_UPLOAD_ENABLE
Switches the server to a mode in which the user can upload their files to the shared folder, as well
as move, rename and delete files and folders within it.
WSF_DIRECTORY_MODE
If there is an "index.html" file in the root folder, the server by default starts in the mode of
displaying the web page associated with "index.html", rather than displaying the directory contents.
To forcibly switch to directory browsing mode, set "WSF_DIRECTORY_MODE=1".
WSF_DIRECTORY_MODE_TITLE
Sets the title of the pages displayed in the browser tab. By default, it displays "Remote file manager".
Working via HTTPS
WSF_CERT
Path to the SSL certificate file (usually "fullchain.pem").
WSF_KEY
Path to the SSL private key file (usually "privkey.pem").
WSF_AUTO_REDIRECT_HTTP_PORT
When the server operates in secure mode (using HTTPS), it is possible to enable automatic redirection
for clients attempting to connect via HTTP. For example, if the server is running on the standard HTTPS
port 443, you can set up automatic redirection for clients connecting via HTTP (standard port 80) by
setting "WSF_AUTO_REDIRECT_HTTP_PORT=80".
Security Settings
WSF_ALLOWED_INTERFACES
List of interfaces on which the server will be accessible. By default, the server is accessible on all
available network interfaces, but this is not desirable if, for example, you need to restrict access
from external networks. This variable specifies a list of IP addresses (separated by commas) on which
the server will operate. For example, to allow access only from localhost, set
"WSF_ALLOWED_INTERFACES=127.0.0.1".
WSF_FORBIDDEN_PATHS
List of paths, relative to the root directory, that will not be displayed to clients. Paths are separated
by a colon ":". For example, to hide the ".git" and "secret" folders, set "WSF_FORBIDDEN_PATHS=.git:secret".
User creation and access restrictions
WSF_USERS
Setting this environment variable switches the server to a mode where access to files is allowed to
specified users. The format of this variable is as follows: username, followed by the "@" symbol, then
the SHA-256 hash (hex) of the user's password, followed by the path the user will have access to, then
a colon ":" and similar data for other users. The path must start with a "/", be relative to the root
directory, and point to a folder. If there are errors in the path, they will only become apparent when
the user attempts to log in. Example:
username1@sha256password1InHex/path1/relative/ROOT_PATH:username2@sha256password2InHex/path2/relative/ROOT_PATH
Note that operating in this mode over HTTP is insecure, as usernames and passwords are transmitted over
the network in plain text and can be easily intercepted. To securely operate in this mode, use HTTPS.
In this case even with a self-signed SSL certificate, communication with the server will be encrypted.
WSF_SESSION_TIMEOUT
User session timeout (in seconds). If the user is inactive for this period, the session ends, and the
user will need to re-enter their username and password upon the next request. By default, this time is
set to 30 minutes.
WSF_SESSION_UPDATE_PAUSE_MILLISECONDS
The time (in milliseconds) during which the session will not be extended, even if requests are received
from the user. By default, this value is set to 5000 milliseconds.
Cluster mode operation
WSF_USE_CLUSTER_MODE
Switches the server to cluster mode. In this mode, multiple copies of the server process (by number of
CPU cores) are launched, and the load is distributed among these processes, allowing full utilization of
CPU resources and increasing server performance under high request loads. However, this mode also requires
a significant amount of RAM. At the same time, this mode makes the server more resilient, as setting the
"WSF_SHOULD_RESTART_WORKER" variable will automatically restart worker processes in case of failures.
WSF_SHOULD_RESTART_WORKER
Whether to restart worker processes in cluster mode in case of unexpected termination. By default, these
processes are not restarted.
Appearance customization
WSF_ICONS_TYPE
Web-shared-folder uses the "file-icon-vectors" (https://www.npmjs.com/package/file-icon-vectors) npm
package to display file and folder icons. There are three available icon styles: "classic", "square-o",
and "vivid". You can set "WSF_ICONS_TYPE" to one of these options. By default, "square-o" is used.
Other settings
WSF_DISABLE_COMPRESSION
Allows to forcibly disable file compression during network transmission. This is useful for debugging
purposes and does not affect what is displayed in the user's browser.
WSF_SHOW_SYSTEM_FILES_REQUESTS
Forces the server to log not only requests to shared files but also requests to the web-shared-folder
application files themselves, requested from a path starting with "/wsf_app_files", such as
"/wsf_app_files/favicon.icon" and others.
`;
console.log(help);
process.exit(0);
}
}
const ARGS = [];
for (let arg of process.argv)
{
ARGS.push(arg);
}
const UPLOAD_ENABLE = checkUpload(ARGS);
const USE_CLUSTER_MODE = Number(process.env.WSF_USE_CLUSTER_MODE);
const SHOULD_RESTART_WORKER = Number(process.env.WSF_SHOULD_RESTART_WORKER);
const DIRECTORY_MODE = Number(process.env.WSF_DIRECTORY_MODE);
const AUTO_REDIRECT_HTTP_PORT = Number(process.env.WSF_AUTO_REDIRECT_HTTP_PORT);
const DISABLE_COMPRESSION = Number(process.env.WSF_DISABLE_COMPRESSION);
let ROOT_PATH_RAW = ARGS[2] || process.env.WSF_ROOT;
ROOT_PATH_RAW = ROOT_PATH_RAW ? ROOT_PATH_RAW.replace(/"/g, '') : null;
if (ROOT_PATH_RAW && !path.isAbsolute(ROOT_PATH_RAW)) ROOT_PATH_RAW = path.join(process.cwd(), ROOT_PATH_RAW);
const ROOT_PATH = ROOT_PATH_RAW; //Папка относительно которой будут задаваться все папки, которые идут с адресом
ROOT_PATH_RAW = null;
const PORT = Number(ARGS[3] || process.env.WSF_PORT);
const KEY = ARGS[4] || process.env.WSF_KEY;
const CERT = ARGS[5] || process.env.WSF_CERT;
const USERS_RAW = process.env.WSF_USERS;
//Format: username1@sha256password1InHex/path1/relative/ROOT_PATH:username2@sha256password2InHex/path2/relative/ROOT_PATH
let USERS = null;
if (USERS_RAW)
{
const usersStrs = USERS_RAW.split(':');
if (usersStrs.length > 0)
{
USERS = new Map();
for (let ustr of usersStrs)
{
const ustrArr = ustr.split('@');
if (ustrArr.length !== 2)
{
USERS = null;
console.log('Error in WSF_USERS (@)');
break;
}
const username = ustrArr[0];
const pp = ustrArr[1];
const pi = pp.indexOf('/');
if (pi === -1)
{
USERS = null;
console.log('Error in WSF_USERS (/)');
break;
}
const passwordHash = pp.slice(0, pi);
if (passwordHash.length !== 64 || (/[^0-9a-f]/gi).test(passwordHash))
{
USERS = null;
console.log('Error in WSF_USERS (not sha256 password)');
break;
}
const root = pp.slice(pi);
USERS.set(username, { passwordHash, root });
}
}
}
let _primarySessions = USERS ? new Map() : null;
let SESSION_TIMEOUT = Number(process.env.WSF_SESSION_TIMEOUT);
if (!SESSION_TIMEOUT) SESSION_TIMEOUT = 1800;
//Если запросы приходят чаще, чем это время, то сессия на эти запросы не обновляется.
let SESSION_UPDATE_PAUSE_MILLISECONDS = Number(process.env.WSF_SESSION_UPDATE_PAUSE_MILLISECONDS);
if (!SESSION_UPDATE_PAUSE_MILLISECONDS) SESSION_UPDATE_PAUSE_MILLISECONDS = 5000;
if (!ROOT_PATH || !PORT)
{
console.log(`web-shared-folder, version ${VERSION}
To show help use "--help" key`);
process.exit(0);
}
let ALLOWED_INTERFACES = null;
{
const ifs = process.env.WSF_ALLOWED_INTERFACES;
if (ifs) ALLOWED_INTERFACES = ifs.split(',').map(v => v.trim());
}
const numCPUs = USE_CLUSTER_MODE ? cpus().length : 1;
if (cluster.isPrimary)
{
console.log('web-shared-folder, version ' + VERSION);
console.log();
console.log('This is free software and is provided "as is", without warranty of any kind.');
console.log();
console.log('Port = ' + PORT);
console.log('Root = ' + ROOT_PATH);
if (USE_CLUSTER_MODE) console.log('CPUs number = ' + numCPUs);
console.log();
console.log('Available on:');
if (ALLOWED_INTERFACES)
{
const realIfs = [];
for (let ip of getIpV4())
{
if(ALLOWED_INTERFACES.includes(ip))
{
console.log(ip);
realIfs.push(ip);
}
}
ALLOWED_INTERFACES = realIfs;
}
else
{
getIpV4().forEach(ip => console.log(ip));
}
console.log();
}
let _generateIndex = false;
fs.stat(ROOT_PATH, (err, stats) =>
{
if (err)
{
console.log(err?.message);
process.exit(1);
}
else if (stats.isFile())
{
console.log('Path is not directory');
process.exit(1);
}
else
{
if (!isNaN(DIRECTORY_MODE))
{
_generateIndex = DIRECTORY_MODE > 0;
}
else
{
const indexFile = path.join(ROOT_PATH, 'index.html');
_generateIndex = !fs.existsSync(indexFile);
}
if (_generateIndex && cluster.isPrimary)
{
console.log('Directory watch mode.');
if (DISABLE_COMPRESSION) console.log('Compression is disable.');
if (UPLOAD_ENABLE) console.log('\x1b[31m%s\x1b[0m', 'Upload to server is enabled!');
}
if (cluster.isPrimary)
{
if (KEY && CERT)
{
console.log('Started in secure (HTTPS) mode.');
if (AUTO_REDIRECT_HTTP_PORT)
{
if (AUTO_REDIRECT_HTTP_PORT === PORT)
{
console.log('HTTP port for autoredirect is equal to HTTPS port!');
process.exit(1);
}
else
{
console.log(`Auto redirect from http port ${AUTO_REDIRECT_HTTP_PORT} is enabled.`);
}
}
}
else
{
console.log('Started in NOT secure (HTTP) mode.');
}
if (USERS) console.log('Authentication mode enabled.');
if (USE_CLUSTER_MODE)
{
console.log(`Primary ${process.pid} is running`);
// Fork workers.
const workers = new Set();
for (let i = 0; i < numCPUs; i++)
{
workers.add(cluster.fork());
}
if (USERS)
{
for (let w of workers)
{
w.on('message', (msg) =>
{
if (msg.newSession)
{
onNewSession(msg, w, workers);
}
else if (msg.deleteSession)
{
onDeleteSession(msg, w, workers);
}
else if (msg.updateSession)
{
onUpdateSession(msg, w, workers);
}
else if (msg.hasSession)
{
onHasSession(msg, w);
}
});
}
}
cluster.on('exit', (worker, code, signal) =>
{
console.log(`Worker ${worker.process.pid} died. Code ${code}, signal: ${signal}`);
workers.delete(worker);
if (SHOULD_RESTART_WORKER)
{
console.log('Restarting...');
const w = cluster.fork();
workers.add(w);
if (USERS)
{
w.on('message', (msg) =>
{
if (msg === 'ready')
{
for (let session of _primarySessions)
{
w.send({ newSession: session[0], username: session[1].username });
}
console.log('Restat complete.');
}
else if (msg.newSession)
{
onNewSession(msg, w, workers);
}
else if (msg.deleteSession)
{
onDeleteSession(msg, w, workers);
}
else if (msg.updateSession)
{
onUpdateSession(msg, w, workers);
}
else if (msg.hasSession)
{
onHasSession(msg, w);
}
});
}
}
});
function onNewSession(msg, currentWorker, workers)
{
const sessionId = msg.newSession;
const timerId = setTimeout(() =>
{
_primarySessions.delete(sessionId);
for (let w of workers)
{
w.send({ deleteSession: sessionId });
}
}, SESSION_TIMEOUT * 1000);
_primarySessions.set(sessionId, { username: msg.username, timerId });
for (let w of workers)
{
if (currentWorker !== w) w.send(msg);
}
}
function onDeleteSession(msg, currentWorker, workers)
{
const sessionId = msg.deleteSession;
clearTimeout(_primarySessions.get(sessionId));
_primarySessions.delete(sessionId);
for (let w of workers)
{
if (currentWorker !== w) w.send(msg);
}
}
function onUpdateSession(msg, currentWorker, workers)
{
const sessionId = msg.updateSession;
const sessionData = _primarySessions.get(sessionId);
if (sessionData)
{
clearTimeout(sessionData.timerId);
sessionData.timeStamp = msg.timeStamp;
for (let w of workers)
{
if (currentWorker !== w) w.send(msg);
}
sessionData.timerId = setTimeout(() =>
{
_primarySessions.delete(sessionId);
for (let w of workers)
{
w.send({ deleteSession: sessionId });
}
}, SESSION_TIMEOUT * 1000);
}
}
function onHasSession(msg, currentWorker)
{
currentWorker.send({ hasSession: _primarySessions.has(msg.hasSession) });
}
}
else
{
workerFlow();
}
}
else
{
console.log(`Worker ${process.pid} started`);
workerFlow();
}
}
});
function workerFlow()
{
const SHOW_SYSTEM_FILES_REQUESTS = Number(process.env.WSF_SHOW_SYSTEM_FILES_REQUESTS);
const DIRECTORY_MODE_TITLE = process.env.WSF_DIRECTORY_MODE_TITLE;
const MAX_FILE_LENGTH = 2147483647;
const MAX_STRING_LENGTH = require('buffer').constants.MAX_STRING_LENGTH;
let ICONS_TYPE = process.env.WSF_ICONS_TYPE;
const DEFAULT_ICON_TYPE = 'square-o';
if (!ICONS_TYPE)
{
ICONS_TYPE = DEFAULT_ICON_TYPE;
}
else
{
if (ICONS_TYPE !== 'square-o' && ICONS_TYPE !== 'classic' && ICONS_TYPE !== 'vivid')
{
console.log(`Icon type ${ICONS_TYPE} not found. Using ${DEFAULT_ICON_TYPE}.`);
ICONS_TYPE = DEFAULT_ICON_TYPE;
}
}
const FILE_REG_EXP = new RegExp(/[<>":?*|\\\x7f\x00-\x1f/]/g);
const DEFAULT_LANG = 'en-US';
let DEFAULT_LOCALE_TRANSLATION = null;
let _workerSessions = null;
let _loginExceptions = null;
if (USERS)
{
if (USE_CLUSTER_MODE) _workerSessions = new Map();
_loginExceptions = new Set();
_loginExceptions.add('/wsf_app_files/login.css');
_loginExceptions.add('/wsf_app_files/favicon.ico');
_loginExceptions.add('/wsf_app_files/404.css');
_loginExceptions.add('/robots.txt');
if (USE_CLUSTER_MODE)
{
process.on('message', (msg) =>
{
//console.log(msg);
if (msg.newSession)
{
_workerSessions.set(msg.newSession, { username: msg.username });
}
else if (msg.deleteSession)
{
_workerSessions.delete(msg.deleteSession);
}
else if (msg.updateSession)
{
const sessionData = _workerSessions.get(msg.updateSession);
sessionData.timeStamp = msg.timeStamp;
}
});
}
}
let _indexHtmlbase = null;
let _favicon = null;
let _index_js = null;
let _index_css = null;
let _light_css = null;
let _dark_css = null;
let _robots_txt = null;
let _locales = null;
let _icons_css = null;
let _404_css = null;
let _404_html = null;
let _login_css = null;
let _login_html = null;
const _icons_svg_map = new Map();
const _icons_catalog = new Set();
const _forbidden_paths = new Set();
const FORBIDDEN_PATHS = process.env.WSF_FORBIDDEN_PATHS;
if (FORBIDDEN_PATHS)
{
FORBIDDEN_PATHS.split(':').forEach(fp => _forbidden_paths.add(path.join(ROOT_PATH, fp)));
}
_forbidden_paths.add(path.join(ROOT_PATH, 'wsf_app_files'));
if (_generateIndex)
{
_indexHtmlbase = fs.readFileSync(path.join(__dirname, 'app_files', 'index.html')).toString().split('~%~');
_favicon = fs.readFileSync(path.join(__dirname, 'app_files', 'favicon.ico'));
const index_js_splitted = fs.readFileSync(path.join(__dirname, 'app_files', 'index.js')).toString().split('/*---UPLOAD_SPLITTER---*/');
_index_js = UPLOAD_ENABLE ? index_js_splitted.join('') : index_js_splitted[0] + index_js_splitted[2];
const index_css_splitted = fs.readFileSync(path.join(__dirname, 'app_files', 'index.css')).toString().split('/*---UPLOAD_SPLITTER---*/');
_index_css = UPLOAD_ENABLE ? index_css_splitted.join('') : index_css_splitted[0];
_light_css = fs.readFileSync(path.join(__dirname, 'app_files', 'light.css'));
_dark_css = fs.readFileSync(path.join(__dirname, 'app_files', 'dark.css'));
_robots_txt = fs.readFileSync(path.join(__dirname, 'app_files', 'robots.txt'));
readIconsFiles();
}
if (USERS)
{
_login_css = fs.readFileSync(path.join(__dirname, 'app_files', 'login.css'));
_login_html = fs.readFileSync(path.join(__dirname, 'app_files', 'login.html')).toString().split('~%~');
}
readTranslationFiles();
_404_css = fs.readFileSync(path.join(__dirname, 'app_files', '404.css'));
_404_html = fs.readFileSync(path.join(__dirname, 'app_files', '404.html')).toString().split('~%~');
start();
function readIconsFiles()
{
let pathCombined = path.join(__dirname, 'node_modules', 'file-icon-vectors', 'dist');
_icons_css = fs.readFileSync(path.join(pathCombined, `file-icon-${ICONS_TYPE}.min.css`));
pathCombined = path.join(pathCombined, 'icons', ICONS_TYPE);
const iconFileNames = fs.readdirSync(pathCombined);
for (let fileName of iconFileNames)
{
if (fileName === 'catalog.json')
{
const catalog = JSON.parse(fs.readFileSync(path.join(pathCombined, fileName)).toString());
for (let ext of catalog)
{
_icons_catalog.add(ext);
}
}
else
{
_icons_svg_map.set(`/wsf_app_files/icons/${ICONS_TYPE}/${fileName}`, fs.readFileSync(path.join(pathCombined, fileName)));
}
}
_icons_svg_map.set('/wsf_app_files/eye.svg', fs.readFileSync(path.join(__dirname, 'app_files', 'img', 'eye.svg')));
_icons_svg_map.set('/wsf_app_files/unzip.svg', fs.readFileSync(path.join(__dirname, 'app_files', 'img', 'unzip.svg')));
_icons_svg_map.set('/wsf_app_files/sun.svg', fs.readFileSync(path.join(__dirname, 'app_files', 'img', 'sun.svg')));
_icons_svg_map.set('/wsf_app_files/auto.svg', fs.readFileSync(path.join(__dirname, 'app_files', 'img', 'auto.svg')));
_icons_svg_map.set('/wsf_app_files/moon.svg', fs.readFileSync(path.join(__dirname, 'app_files', 'img', 'moon.svg')));
_icons_svg_map.set('/wsf_app_files/circle.svg', fs.readFileSync(path.join(__dirname, 'app_files', 'img', 'circle.svg')));
_icons_svg_map.set('/wsf_app_files/rename.svg', fs.readFileSync(path.join(__dirname, 'app_files', 'img', 'rename.svg')));
}
function readTranslationFiles()
{
//Считываем файлы с локализациями.
_locales = new Map();
const localeDir = path.join(__dirname, 'app_files', 'locale');
const localeFiles = fs.readdirSync(localeDir);
for (let file of localeFiles)
{
const filePath = path.join(localeDir, file);
_locales.set(path.basename(file, '.json'), JSON.parse(fs.readFileSync(filePath).toString()));
}
DEFAULT_LOCALE_TRANSLATION = _locales.get(DEFAULT_LANG);
}
let _lastReqTime = new Date(0);
let _lastIP = '';
let _lastUser = '';
function start()
{
if (KEY && CERT)
{
const ssl_cert =
{
key: fs.readFileSync(KEY),
cert: fs.readFileSync(CERT)
};
createServer(app, PORT, ssl_cert);
if (AUTO_REDIRECT_HTTP_PORT) createServer(redirectApp, AUTO_REDIRECT_HTTP_PORT);
}
else
{
createServer(app, PORT);
}
}
function createServer(app, port, ssl_cert)
{
if (ALLOWED_INTERFACES)
{
for (let ip of ALLOWED_INTERFACES)
{
if (ssl_cert)
{
https.createServer(ssl_cert, app).listen(port, ip);
}
else
{
http.createServer(app).listen(port, ip);
}
}
}
else
{
if (ssl_cert)
{
https.createServer(ssl_cert, app).listen(port);
}
else
{
http.createServer(app).listen(port);
}
}
}
function redirectApp(req, res)
{
const html = `<html>
<head><title>301 Moved Permanently</title></head>
<body><h1>301 Moved Permanently</h1></body>
</html>`;
const urlArray = req.url.split('/');
let tail = urlArray.slice(1).join('/');
if (tail !== '')
{
if (tail[tail.length - 1] === '/')
{
tail = tail.slice(0, tail.length - 1);
}
if (tail !== '') tail = '/' + tail;
}
const url = `${urlArray[0]}:${PORT}${tail}`;
const uri = `https://${req.headers.host}${url}`;
console.log('Redirect to ' + uri);
res.writeHead(301,
{
'Content-Type': 'text/html',
'Content-Length': html.length,
'Location': uri
});
res.end(html);
}
function app(req, res)
{
//console.log(`Pid: ${process.pid}, isPrimary: ${cluster.isPrimary}, size: ${USE_CLUSTER_MODE ? (cluster.isPrimary ? _primarySessions.size : _workerSessions.size) : 'not a cluster'}`);
let now = new Date();
let ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
const url = req.url.split('?');
const urlPath = decodeURIComponent(url[0]);
const cookie = parseCookie(req.headers?.cookie);
const reqGetData = parseRequest(url[1]);
const acceptEncoding = req.headers['accept-encoding'];
const acceptLanguage = req.headers['accept-language'];
const responseCookie = [];
const clientLang = getClientLanguage(acceptLanguage, cookie, responseCookie);
const localeTranslation = _locales.get(clientLang);
//Проводим аутентификацию и вывод логов
{
if (USERS)
{
login((sessionId) =>
{
if (sessionId)
{
const username = cluster.isPrimary ? _primarySessions.get(sessionId).username : _workerSessions.get(sessionId).username;
//console.log(sessionId, username);
if (!urlPath.startsWith('/wsf_app_files')) updateSessionTimeout(sessionId);
const userdata = { username, root: USERS.get(username).root };
log(username);
normalWork(userdata);
}
else
{
log();
}
});
}
else
{
log();
normalWork();
}
function updateSessionTimeout(sessionId)
{
const now = Date.now();
const sessionCookie = `sessionId=${sessionId}; path=/; max-age=${SESSION_TIMEOUT}; samesite=strict; httpOnly`;
if (cluster.isPrimary)
{
const sessionData = _primarySessions.get(sessionId);
if (now - sessionData.timeStamp > SESSION_UPDATE_PAUSE_MILLISECONDS)
{
clearTimeout(sessionData.timerId);
sessionData.timeStamp = now;
sessionData.timerId = setTimeout(() =>
{
_primarySessions.delete(sessionId);
}, SESSION_TIMEOUT * 1000);
responseCookie.push(sessionCookie);
}
}
else
{
const sessionData = _workerSessions.get(sessionId);
if (now - sessionData.timeStamp > SESSION_UPDATE_PAUSE_MILLISECONDS)
{
process.send({ updateSession: sessionId, timeStamp: now });
sessionData.timeStamp = now;
responseCookie.push(sessionCookie);
}
}
}
function log(username)
{
if (SHOW_SYSTEM_FILES_REQUESTS)
{
show(username);
}
else if (!urlPath.startsWith('/wsf_app_files/'))
{
show(username);
}
function show(username)
{
if (now - _lastReqTime > 1000 || _lastIP !== ip || (username && _lastUser !== username)) console.log(`*******${ip}, ${now.toLocaleString()} *******`);
_lastReqTime = now;
_lastIP = ip;
if (username) _lastUser = username;
if (username)
{
console.log(`User: ${username}, url: ${urlPath}`);
}
else if (USERS)
{
if (_loginExceptions.has(urlPath))
{
console.log('url: ' + urlPath);
}
else
{
console.log('Not authorized yet, url: ' + urlPath);
}
}
else
{
console.log('Url: ' + urlPath);
}
}
}
}
function login(callback)
{
if (urlPath === '/wsf_app_files/credentials')
{
const contentType = req.headers['content-type']?.split(';')[0].trim();
if (contentType === 'application/x-www-form-urlencoded')
{
getPostBody(req, (err, postBody) =>
{
if (err)
{
console.log(err.message);
res.end('Error occured while handling request!');
callback(null);
}
else
{
const reqPostData = parseXwwwFormUrlEncoded(postBody);
const username = decodeURIComponent(reqPostData?.username);
const password = decodeURIComponent(reqPostData?.password);
if (username !== undefined && USERS.has(username))
{
const passwordHash = USERS.get(username).passwordHash;
if (passwordHash === crypto.createHash('sha256').update(password).digest('hex'))
{
let refLink = '/';
if (cookie?.reflink) refLink = cookie.reflink;
const sessionId = generateSessionId();
responseCookie.push(generateSessionCookie(sessionId, username));
if (cookie?.reflink) responseCookie.push('reflink=/; path=/; max-age=0; samesite=strict');
const now = Date.now();
if (cluster.isPrimary)
{
const timerId = setTimeout(() =>
{
_primarySessions.delete(sessionId);
}, SESSION_TIMEOUT * 1000);
_primarySessions.set(sessionId, { username, timerId, timeStamp: now });
}
else
{
_workerSessions.set(sessionId, { username, timeStamp: now });
process.send({ newSession: sessionId, username, timeStamp: now });
}
reload(res, refLink, responseCookie);
}
else
{
reload(res, '/wsf_app_files/login_error.html', responseCookie);
}
}
else
{
reload(res, '/wsf_app_files/login_error.html', responseCookie);
}
}
});
callback(null);
}
else
{
reload(res, '/wsf_app_files/login.html', responseCookie);
callback(null);
}
}
//Login exceptions
else if (_loginExceptions.has(urlPath))
{
normalWork();
callback(null);
}
else
{
let sessionId = null;
if (cookie?.sessionId)
{
if (cluster.isPrimary)
{
if (_primarySessions.has(cookie.sessionId)) sessionId = cookie.sessionId;
next();