@caboodle-tech/node-simple-server 1.3.1 → 2.0.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/.eslintrc.json +35 -23
- package/COPYING.txt +7 -0
- package/LICENSE.txt +22 -0
- package/README.md +73 -40
- package/bin/nss.js +1039 -0
- package/changelogs/v1.md +7 -0
- package/changelogs/v2.md +28 -0
- package/examples/README.md +15 -0
- package/examples/controllers/website.js +60 -0
- package/examples/controllers/websocket.js +83 -0
- package/examples/run.js +17 -0
- package/examples/www-website/assets/fonts/roboto/LICENSE.txt +202 -0
- package/examples/www-website/assets/fonts/roboto/roboto-regular.ttf +0 -0
- package/examples/www-website/assets/fonts/roboto/roboto-regular.woff +0 -0
- package/examples/www-website/assets/fonts/roboto/roboto-regular.woff2 +0 -0
- package/examples/www-website/assets/imgs/logo.png +0 -0
- package/examples/www-website/css/main.css +96 -0
- package/examples/www-website/css/normalize.css +349 -0
- package/examples/www-website/index.html +47 -0
- package/examples/www-website/js/main.js +33 -0
- package/examples/www-websockets/assets/fonts/roboto/LICENSE.txt +202 -0
- package/examples/www-websockets/assets/fonts/roboto/roboto-regular.ttf +0 -0
- package/examples/www-websockets/assets/fonts/roboto/roboto-regular.woff +0 -0
- package/examples/www-websockets/assets/fonts/roboto/roboto-regular.woff2 +0 -0
- package/examples/www-websockets/assets/imgs/logo.png +0 -0
- package/examples/www-websockets/css/main.css +148 -0
- package/examples/www-websockets/css/normalize.css +349 -0
- package/examples/www-websockets/index.html +45 -0
- package/examples/www-websockets/js/main.js +63 -0
- package/{sources → handlers}/dir-listing.html +68 -5
- package/handlers/forbidden.html +43 -0
- package/{js → handlers/js}/content-types.js +3 -1
- package/{js → handlers/js}/http-status.js +2 -1
- package/handlers/live-reloading.html +209 -0
- package/handlers/not-found.html +43 -0
- package/package.json +13 -10
- package/LICENSE +0 -21
- package/server.js +0 -866
- package/sources/socket.html +0 -204
- /package/{sources → handlers}/favicon.ico +0 -0
package/bin/nss.js
ADDED
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
import Chokidar from 'chokidar';
|
|
2
|
+
import Fs from 'fs';
|
|
3
|
+
import Http from 'http';
|
|
4
|
+
import Os from 'os';
|
|
5
|
+
import Path from 'path';
|
|
6
|
+
import { WebSocketServer } from 'ws';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
import ContentTypes from '../handlers/js/content-types.js';
|
|
10
|
+
import HTTPStatus from '../handlers/js/http-status.js';
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
15
|
+
const __dirname = Path.dirname(__filename);
|
|
16
|
+
const APP_ROOT = Path.join(__dirname, '../');
|
|
17
|
+
|
|
18
|
+
class NodeSimpleServer {
|
|
19
|
+
|
|
20
|
+
#handlers = {};
|
|
21
|
+
|
|
22
|
+
#OPS = {
|
|
23
|
+
callbacks: [],
|
|
24
|
+
contentType: 'text/html',
|
|
25
|
+
dirListing: false,
|
|
26
|
+
indexPage: 'index.html',
|
|
27
|
+
port: 5000,
|
|
28
|
+
root: Path.normalize(`${process.cwd()}${Path.sep}`),
|
|
29
|
+
running: false
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
#reload = ['.html', '.htm'];
|
|
33
|
+
|
|
34
|
+
#server = null;
|
|
35
|
+
|
|
36
|
+
#socket = null;
|
|
37
|
+
|
|
38
|
+
#sockets = {
|
|
39
|
+
routes: {},
|
|
40
|
+
map: {}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
#VERSION = '2.0.0';
|
|
44
|
+
|
|
45
|
+
#watching = [];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Instantiate a new instance of Node Simple Server.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} options
|
|
51
|
+
* @param {string} options.contentType The default Content-Type to report to the browser;
|
|
52
|
+
* defaults to text/html.
|
|
53
|
+
* @param {boolean} options.dirListing If a directory is requested should the directory listing
|
|
54
|
+
* page be shown; default false.
|
|
55
|
+
* @param {string} options.indexPage If a directory is requested consider this file to be the
|
|
56
|
+
* index page if it exits at that location; defaults to index.html
|
|
57
|
+
* @param {number} options.port The port number the HTTP and WebSocket server should listen on
|
|
58
|
+
* for requests; default 5000.
|
|
59
|
+
* @param {string} options.root The absolute path to the directory that should be considered the
|
|
60
|
+
* servers root directory.
|
|
61
|
+
*/
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
if (options.disableAutoRestart) {
|
|
64
|
+
this.#OPS.disableAutoRestart = true;
|
|
65
|
+
}
|
|
66
|
+
if (options.contentType) {
|
|
67
|
+
this.#OPS.contentType = options.contentType;
|
|
68
|
+
}
|
|
69
|
+
if (options.dirListing) {
|
|
70
|
+
this.#OPS.dirListing = options.dirListing;
|
|
71
|
+
}
|
|
72
|
+
if (options.port) {
|
|
73
|
+
this.#OPS.port = options.port;
|
|
74
|
+
}
|
|
75
|
+
if (options.root) {
|
|
76
|
+
this.#OPS.root = options.root;
|
|
77
|
+
if (this.#OPS.root[this.#OPS.root.length - 1] !== Path.sep) {
|
|
78
|
+
this.#OPS.root += Path.sep;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this.#loadHandlers();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register a backend function to call when a frontend page messages in via websocket.
|
|
86
|
+
* This will allow you to have two way communications with a page as long as you registered
|
|
87
|
+
* a function on the frontend to capture and respond to websocket messages as well.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} pattern A RegExp object to check the page URL's against or a string
|
|
90
|
+
* representing a regular expression to check the page URL's against.
|
|
91
|
+
* @param {function} callback The function to call if this page (url) messages.
|
|
92
|
+
* @return {boolean} True if the function was registered, false otherwise.
|
|
93
|
+
*/
|
|
94
|
+
addWebsocketCallback(pattern, callback) {
|
|
95
|
+
const regex = this.makeRegex(pattern);
|
|
96
|
+
if (regex !== null && typeof callback === 'function') {
|
|
97
|
+
this.#OPS.callbacks.push([regex, callback]);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get an array of all the IP addresses you can reach this server at either from
|
|
105
|
+
* the machine itself or on the LAN.
|
|
106
|
+
*
|
|
107
|
+
* @return {Array} An array of loop back ip addresses and LAN addresses to this server.
|
|
108
|
+
*/
|
|
109
|
+
getAddresses(port) {
|
|
110
|
+
const locals = this.#getLocalAddresses();
|
|
111
|
+
const addresses = [
|
|
112
|
+
`http://localhost:${port}`,
|
|
113
|
+
`http://127.0.0.1:${port}`
|
|
114
|
+
];
|
|
115
|
+
Object.keys(locals).forEach((key) => {
|
|
116
|
+
addresses.push(`http://${locals[key]}:${port}`);
|
|
117
|
+
});
|
|
118
|
+
return addresses;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the contents of a directory for displaying in the directory listing page.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} location The directory to search.
|
|
125
|
+
* @return {object} The HTML entries for all directories [directories] and files [files] found.
|
|
126
|
+
*/
|
|
127
|
+
#getDirList(location) {
|
|
128
|
+
// Get all directories and files at this location.
|
|
129
|
+
const files = [];
|
|
130
|
+
const dirs = [];
|
|
131
|
+
Fs.readdirSync(location).forEach((item) => {
|
|
132
|
+
if (Fs.lstatSync(Path.join(location, item)).isDirectory()) {
|
|
133
|
+
dirs.push(item);
|
|
134
|
+
} else {
|
|
135
|
+
files.push(item);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
139
|
+
dirs.sort((a, b) => a.localeCompare(b));
|
|
140
|
+
// Build the innerHTML for the directory and files unordered list.
|
|
141
|
+
let fileHtml = '';
|
|
142
|
+
let dirHtml = '';
|
|
143
|
+
// Replace files in the directory listing template.
|
|
144
|
+
files.forEach((file) => {
|
|
145
|
+
fileHtml += `<li><a href="[b]/${file}">${file}</a></li>`;
|
|
146
|
+
});
|
|
147
|
+
// Add the go back link (parent directory) for nested directories.
|
|
148
|
+
if (location.replace(this.#OPS.root, '').length > 0) {
|
|
149
|
+
dirHtml += '<li><a href="[b]/../">../</a></li>';
|
|
150
|
+
}
|
|
151
|
+
// Replace directories in the directory listing template.
|
|
152
|
+
dirs.forEach((dir) => {
|
|
153
|
+
dirHtml += `<li><a href="[b]/${dir}">${dir}</a></li>`;
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
directories: dirHtml,
|
|
157
|
+
files: fileHtml
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Attempt to locate the default index page for the specified directory.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} location The absolute path to a dir the user is trying to
|
|
165
|
+
* access on the frontend.
|
|
166
|
+
* @return {string} The correct path to this directories default index or an empty path if
|
|
167
|
+
* none was found.
|
|
168
|
+
*/
|
|
169
|
+
#getIndexForLocation(location) {
|
|
170
|
+
let index = '';
|
|
171
|
+
for (let i = 0; i < this.#reload.length; i++) {
|
|
172
|
+
const tmpIndex = `index${this.#reload[i]}`;
|
|
173
|
+
if (Fs.existsSync(Path.normalize(Path.join(this.#OPS.root, location, tmpIndex)))) {
|
|
174
|
+
index = tmpIndex;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return index;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Convert a timestamp or date into a browser compliant header date.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} date Any timestamp string; usually the last modified date
|
|
185
|
+
* of a file.
|
|
186
|
+
* @return {string} The data converted to the format browsers expect to see
|
|
187
|
+
* in the content headers.
|
|
188
|
+
*/
|
|
189
|
+
getLastModified(date) {
|
|
190
|
+
// Use the current time if no time was passed in.
|
|
191
|
+
let timestamp = new Date();
|
|
192
|
+
if (date) {
|
|
193
|
+
timestamp = new Date(Date.parse(date));
|
|
194
|
+
}
|
|
195
|
+
// Build and return the timestamp.
|
|
196
|
+
const dayAry = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
197
|
+
const monthAry = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
198
|
+
const dayStr = dayAry[timestamp.getUTCDay()];
|
|
199
|
+
const monStr = monthAry[timestamp.getUTCMonth()];
|
|
200
|
+
let dayNum = timestamp.getUTCDate();
|
|
201
|
+
let hour = timestamp.getUTCHours();
|
|
202
|
+
let minute = timestamp.getUTCMinutes();
|
|
203
|
+
let second = timestamp.getUTCSeconds();
|
|
204
|
+
if (dayNum.length < 10) {
|
|
205
|
+
dayNum = `0${dayNum}`;
|
|
206
|
+
}
|
|
207
|
+
if (hour.length < 10) {
|
|
208
|
+
hour = `0${hour}`;
|
|
209
|
+
}
|
|
210
|
+
if (minute.length < 10) {
|
|
211
|
+
minute = `0${minute}`;
|
|
212
|
+
}
|
|
213
|
+
if (second.length < 10) {
|
|
214
|
+
second = `0${second}`;
|
|
215
|
+
}
|
|
216
|
+
return `${dayStr}, ${dayNum} ${monStr} ${timestamp.getUTCFullYear()} ${hour}:${minute}:${second} GMT`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all IP addresses that are considered external on this machine; the server will
|
|
221
|
+
* attempt to listen to the requested port on these addresses as well, allowing you to
|
|
222
|
+
* view the site from other devices on the same network.
|
|
223
|
+
*
|
|
224
|
+
* {@link https://github.com/nisaacson/interface-addresses | Original Source}.
|
|
225
|
+
*
|
|
226
|
+
* @author Noah Isaacson
|
|
227
|
+
* @return {object} An object of interface names [key] and their IPv4 IP addresses [value].
|
|
228
|
+
*/
|
|
229
|
+
#getLocalAddresses() {
|
|
230
|
+
const addresses = {};
|
|
231
|
+
const interfaces = Os.networkInterfaces();
|
|
232
|
+
Object.keys(interfaces).filter((key) => {
|
|
233
|
+
const items = interfaces[key];
|
|
234
|
+
return items.forEach((item) => {
|
|
235
|
+
const { family } = item;
|
|
236
|
+
if (family !== 'IPv4') {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
const { internal } = item;
|
|
240
|
+
if (internal) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const { address } = item;
|
|
244
|
+
addresses[key] = address;
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
return addresses;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Create the HTTP headers object for the specified file if any.
|
|
253
|
+
*
|
|
254
|
+
* @param {object} settings Additional settings to set on the header object.
|
|
255
|
+
* @param {string} settings.charset The charset to use for this content; defaults to utf-8.
|
|
256
|
+
* @param {string} settings.contentType The content type of this request; defaults to this.#OPS.contentType.
|
|
257
|
+
* @param {string} settings.file The systems absolute path to the file being requested by the server.
|
|
258
|
+
* @param {string} settings.location The location (URL) to redirect to; must set for all 302 responses.
|
|
259
|
+
* @return {object} The HTTP header object.
|
|
260
|
+
*/
|
|
261
|
+
#getHeaders(settings = {}) {
|
|
262
|
+
// Create the header object and set the Content-Type.
|
|
263
|
+
const headers = {};
|
|
264
|
+
const contentType = settings?.contentType || this.#OPS.contentType;
|
|
265
|
+
const charset = settings?.charset || 'UTF-8';
|
|
266
|
+
headers['Content-Type'] = `${contentType}; charset=${charset}`;
|
|
267
|
+
// Standard headers that should always be set for NSS.
|
|
268
|
+
let mtime = new Date().toUTCString();
|
|
269
|
+
if (settings?.file) {
|
|
270
|
+
mtime = Fs.statSync(settings.file).mtime;
|
|
271
|
+
}
|
|
272
|
+
const nssHeaders = {
|
|
273
|
+
'Cache-Control': 'public, max-age=0',
|
|
274
|
+
'Last-Modified': this.getLastModified(mtime),
|
|
275
|
+
// eslint-disable-next-line quote-props
|
|
276
|
+
'Vary': 'Origin',
|
|
277
|
+
'X-Powered-By': `Node Simple Server (NSS) ${this.#VERSION}`
|
|
278
|
+
};
|
|
279
|
+
// If this is a redirect what is the proper location?
|
|
280
|
+
if (settings?.location) {
|
|
281
|
+
headers.Location = settings.location;
|
|
282
|
+
}
|
|
283
|
+
// Combine the headers and return the header object.
|
|
284
|
+
return Object.assign(headers, nssHeaders);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Returns an array of watcher objects showing you which directories and files are
|
|
289
|
+
* actively being watched for changes.
|
|
290
|
+
*
|
|
291
|
+
* @return {Array} An array of watcher objects; 1 object per call to watch().
|
|
292
|
+
*/
|
|
293
|
+
getWatched() {
|
|
294
|
+
const watched = [];
|
|
295
|
+
this.#watching.forEach((watcher) => {
|
|
296
|
+
watched.push(watcher.getWatched());
|
|
297
|
+
});
|
|
298
|
+
return watched;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Loads all the handler files that we use to respond to various requests
|
|
303
|
+
* like directory listing, page not found, access denied, and so on.
|
|
304
|
+
*/
|
|
305
|
+
#loadHandlers() {
|
|
306
|
+
// eslint-disable-next-line max-len, no-template-curly-in-string
|
|
307
|
+
const internalError = '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>500 Internal Server Error</title><style>body,html{margin:0;padding:0}body{padding:15px}</style></head><body><h1>500 Internal Server Error</h1><p>Could not locate source file.</p><hr><p><i>Node Simple Server (NSS) {{version}} Server <script type="text/javascript">document.write(`${document.location.protocol}//${document.location.hostname}`);</script> Port <script type="text/javascript">document.write(document.location.port)</script></i></p>{{liveReloading}}</body></html>';
|
|
308
|
+
|
|
309
|
+
const dirListingSrc = Path.join(APP_ROOT, 'handlers', 'dir-listing.html');
|
|
310
|
+
const forbiddenSrc = Path.join(APP_ROOT, 'handlers', 'forbidden.html');
|
|
311
|
+
const liveReloadingSrc = Path.join(APP_ROOT, 'handlers', 'live-reloading.html');
|
|
312
|
+
const notFoundSrc = Path.join(APP_ROOT, 'handlers', 'not-found.html');
|
|
313
|
+
|
|
314
|
+
let dirListingContent = '';
|
|
315
|
+
try {
|
|
316
|
+
dirListingContent = Fs.readFileSync(dirListingSrc, { encoding: 'utf-8', flag: 'r' });
|
|
317
|
+
} catch (_) { dirListingContent = internalError; }
|
|
318
|
+
|
|
319
|
+
let forbiddenContent = '';
|
|
320
|
+
try {
|
|
321
|
+
forbiddenContent = Fs.readFileSync(forbiddenSrc, { encoding: 'utf-8', flag: 'r' });
|
|
322
|
+
} catch (_) { forbiddenContent = internalError; }
|
|
323
|
+
|
|
324
|
+
let liveReloadingContent = '';
|
|
325
|
+
try {
|
|
326
|
+
liveReloadingContent = Fs.readFileSync(liveReloadingSrc, { encoding: 'utf-8', flag: 'r' });
|
|
327
|
+
} catch (_) { liveReloadingContent = '<!-- 500 Internal Server Error -->'; }
|
|
328
|
+
|
|
329
|
+
let notFoundContent = '';
|
|
330
|
+
try {
|
|
331
|
+
notFoundContent = Fs.readFileSync(notFoundSrc, { encoding: 'utf-8', flag: 'r' });
|
|
332
|
+
} catch (_) { notFoundContent = internalError; }
|
|
333
|
+
|
|
334
|
+
dirListingContent = dirListingContent.replace('{{version}}', this.#VERSION);
|
|
335
|
+
dirListingContent = dirListingContent.replace('{{live_reload}}', liveReloadingContent);
|
|
336
|
+
forbiddenContent = forbiddenContent.replace('{{version}}', this.#VERSION);
|
|
337
|
+
forbiddenContent = forbiddenContent.replace('{{live_reload}}', liveReloadingContent);
|
|
338
|
+
notFoundContent = notFoundContent.replace('{{version}}', this.#VERSION);
|
|
339
|
+
notFoundContent = notFoundContent.replace('{{live_reload}}', liveReloadingContent);
|
|
340
|
+
|
|
341
|
+
this.#handlers = {
|
|
342
|
+
dirListing: dirListingContent,
|
|
343
|
+
forbidden: forbiddenContent,
|
|
344
|
+
liveReloading: liveReloadingContent,
|
|
345
|
+
notFound: notFoundContent
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Converts a regular expression (regex) string into an actual RegExp object.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} pattern A string of text or a regex expressed as a string; don't forget to
|
|
353
|
+
* escape characters that should be interpreted literally.
|
|
354
|
+
* @return {RegExp|null} A RegExp object if the string could be converted, null otherwise.
|
|
355
|
+
*/
|
|
356
|
+
makeRegex(pattern) {
|
|
357
|
+
try {
|
|
358
|
+
if (/\[|\]|\(|\)|\{|\}|\*|\$|\^/.test(pattern)) {
|
|
359
|
+
return new RegExp(pattern);
|
|
360
|
+
}
|
|
361
|
+
if (pattern[0] === '/' && pattern[pattern.length - 1] === '/') {
|
|
362
|
+
// eslint-disable-next-line no-param-reassign
|
|
363
|
+
pattern = pattern.substr(1, pattern.length - 2);
|
|
364
|
+
}
|
|
365
|
+
return new RegExp(`^${pattern}$`);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Message a frontend page via the WebSocket connection if the page is currently connected.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} pattern A RegExp object to check the page URL's against, a string
|
|
375
|
+
* representing a regular expression to check the page URL's against,
|
|
376
|
+
* or a font-end pages ID.
|
|
377
|
+
* @param {*} message The message you would like to send. Any standard datatype may be provided
|
|
378
|
+
* because the messages is converted to the NSS standard of:
|
|
379
|
+
* { data: message, type: typeof message }
|
|
380
|
+
* @return {boolean} True if the page is connected and the message was sent, false otherwise.
|
|
381
|
+
*/
|
|
382
|
+
message(pattern, message) {
|
|
383
|
+
const original = pattern.toString();
|
|
384
|
+
const regex = this.makeRegex(pattern);
|
|
385
|
+
let result = false;
|
|
386
|
+
// See if we can instantly message the specified socket.
|
|
387
|
+
if (this.#sockets.map[`S${pattern}`]) {
|
|
388
|
+
this.#sockets.map[`S${pattern}`].send(message);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
// Attempt to find the requested page and message it.
|
|
392
|
+
const keys = Object.keys(this.#sockets.routes);
|
|
393
|
+
for (let i = 0; i < keys.length; i++) {
|
|
394
|
+
if (regex != null) {
|
|
395
|
+
if (regex.test(keys[i])) {
|
|
396
|
+
this.#sockets.routes[keys[i]].forEach((socket) => {
|
|
397
|
+
socket.send(message);
|
|
398
|
+
});
|
|
399
|
+
result = true;
|
|
400
|
+
}
|
|
401
|
+
} else if (original === keys[i]) {
|
|
402
|
+
this.#sockets.routes[keys[i]].forEach((socket) => {
|
|
403
|
+
socket.send(message);
|
|
404
|
+
});
|
|
405
|
+
result = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Send the reload message to all connected pages.
|
|
413
|
+
*/
|
|
414
|
+
reloadAllPages() {
|
|
415
|
+
// Send the reload message to all connections.
|
|
416
|
+
const keys = Object.keys(this.#sockets.map);
|
|
417
|
+
keys.forEach((key) => {
|
|
418
|
+
this.#sockets.map[key].send('reload');
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Reload a single page or single group of font-end pages matching a specified pattern.
|
|
424
|
+
*
|
|
425
|
+
* @param {RegExp|String} pattern A RegExp object to check the page URL's against, a string
|
|
426
|
+
* representing a regular expression to check the page URL's
|
|
427
|
+
* against, or a string representing the pages front-end ID.
|
|
428
|
+
* @return {null} Used only as a short circuit.
|
|
429
|
+
*/
|
|
430
|
+
reloadSinglePage(pattern) {
|
|
431
|
+
const original = pattern.toString();
|
|
432
|
+
if (this.whatIs(pattern) !== 'regexp') {
|
|
433
|
+
// eslint-disable-next-line no-param-reassign
|
|
434
|
+
pattern = this.makeRegex(pattern);
|
|
435
|
+
}
|
|
436
|
+
// See if the pattern is a page id first.
|
|
437
|
+
if (this.#sockets.map[original]) {
|
|
438
|
+
this.#sockets.map[original].send('reload');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
// See if the pattern matches a specific URL and reload all those pages.
|
|
442
|
+
const keys = Object.keys(this.#sockets.routes);
|
|
443
|
+
if (pattern != null) {
|
|
444
|
+
for (let i = 0; i < keys.length; i++) {
|
|
445
|
+
if (pattern.test(keys[i])) {
|
|
446
|
+
this.#sockets.routes[keys[i]].forEach((socket) => {
|
|
447
|
+
socket.send('reload');
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Send the refreshCSS message to all connected pages; this reloads only the CSS
|
|
457
|
+
* and not the whole page.
|
|
458
|
+
*/
|
|
459
|
+
reloadAllStyles() {
|
|
460
|
+
// Send the refreshCSS message to all connections.
|
|
461
|
+
const keys = Object.keys(this.#sockets.map);
|
|
462
|
+
keys.forEach((key) => {
|
|
463
|
+
this.#sockets.map[key].send('refreshCSS');
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Reload the stylesheets for a single page or single group of pages matching a
|
|
469
|
+
* specified pattern.
|
|
470
|
+
*
|
|
471
|
+
* @param {RegExp|String} pattern A RegExp object to check the page URL's against, a string
|
|
472
|
+
* representing a regular expression to check the page URL's
|
|
473
|
+
* against, or a string representing the pages front-end ID.
|
|
474
|
+
* @return {null} Used only as a short circuit.
|
|
475
|
+
*/
|
|
476
|
+
reloadSingleStyles(pattern) {
|
|
477
|
+
const original = pattern.toString();
|
|
478
|
+
if (this.whatIs(pattern) !== 'regexp') {
|
|
479
|
+
// eslint-disable-next-line no-param-reassign
|
|
480
|
+
pattern = this.makeRegex(pattern) || original;
|
|
481
|
+
}
|
|
482
|
+
// See if the pattern is a page id first.
|
|
483
|
+
if (this.#sockets.map[original]) {
|
|
484
|
+
this.#sockets.map[original].send('refreshCSS');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// See if the pattern matches a specific URL and reload all those pages.
|
|
488
|
+
const keys = Object.keys(this.#sockets.routes);
|
|
489
|
+
if (pattern != null) {
|
|
490
|
+
for (let i = 0; i < keys.length; i++) {
|
|
491
|
+
if (pattern.test(keys[i])) {
|
|
492
|
+
this.#sockets.routes[keys[i]].forEach((socket) => {
|
|
493
|
+
socket.send('refreshCSS');
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Unregister a backend function that was set with addWebsocketCallback.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} pattern The same regular expression (regex) object or string that was
|
|
505
|
+
* used when the callback function was first registered.
|
|
506
|
+
* @param {function} callback The function that was originally registered as the callback.
|
|
507
|
+
* @return {boolean} True is the function was unregistered, false otherwise.
|
|
508
|
+
*/
|
|
509
|
+
removeWebsocketCallback(pattern, callback) {
|
|
510
|
+
// Make sure the pattern is not null.
|
|
511
|
+
let oldRegex = this.makeRegex(pattern);
|
|
512
|
+
if (!oldRegex) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
// Convert the regex to a string otherwise comparing will never work.
|
|
516
|
+
oldRegex = oldRegex.toString();
|
|
517
|
+
// Remove the pattern and callback from the registered callbacks if they exits.
|
|
518
|
+
for (let i = 0; i < this.#OPS.callbacks.length; i++) {
|
|
519
|
+
const regex = this.#OPS.callbacks[i][0].toString();
|
|
520
|
+
const func = this.#OPS.callbacks[i][1];
|
|
521
|
+
if (regex === oldRegex && func === callback) {
|
|
522
|
+
this.#OPS.callbacks.splice(i, 1);
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The server listener for NSS. All HTTP requests are handled here.
|
|
531
|
+
*
|
|
532
|
+
* @param {object} request The HTTP request object.
|
|
533
|
+
* @param {object} resp The HTTP response object waiting for communication back.
|
|
534
|
+
* @return {void} Used only as a short circuit.
|
|
535
|
+
*/
|
|
536
|
+
#serverListener(request, resp) {
|
|
537
|
+
|
|
538
|
+
let requestUrl = request.url.replace(this.#OPS.root, '').replace(/(\.{1,2}[\\/])/g, '');
|
|
539
|
+
let systemPath = Path.normalize(Path.join(this.#OPS.root, requestUrl.split(/[?#]/)[0]));
|
|
540
|
+
let filename = Path.basename(systemPath);
|
|
541
|
+
let ext = Path.extname(filename);
|
|
542
|
+
|
|
543
|
+
if (filename === 'websocket.ws') {
|
|
544
|
+
/*
|
|
545
|
+
* ERROR: This should never trigger! If it does that means the server has an issue.
|
|
546
|
+
* We can try and send back a 100 to save the socket connection but we might be
|
|
547
|
+
* toast, http.createServer is having issues on this machine/ network.
|
|
548
|
+
*/
|
|
549
|
+
resp.writeHead(HTTPStatus.continue, this.#getHeaders({ contentType: 'text/plain' }));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (requestUrl === '/favicon.ico' && filename === 'favicon.ico') {
|
|
554
|
+
let iconPath = Path.join(this.#OPS.root, 'favicon.ico'); // Users favicon.
|
|
555
|
+
// If the user does not have a favicon try to default to NSS's favicon.
|
|
556
|
+
if (!Fs.existsSync(iconPath)) {
|
|
557
|
+
iconPath = Path.normalize(`${APP_ROOT}/sources/favicon.ico`);
|
|
558
|
+
}
|
|
559
|
+
// If NSS's favicon is also missing don't use a favicon; hides the 404.
|
|
560
|
+
if (!Fs.existsSync(iconPath)) {
|
|
561
|
+
resp.writeHead(HTTPStatus.noContent, this.#getHeaders({ contentType: ContentTypes[''] }));
|
|
562
|
+
resp.end();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// We found a valid file, use it as the sites favicon.
|
|
566
|
+
const ico = Fs.readFileSync(iconPath, { encoding: 'binary', flag: 'r' });
|
|
567
|
+
resp.writeHead(HTTPStatus.found, this.#getHeaders({
|
|
568
|
+
contentType: ContentTypes['.ico'],
|
|
569
|
+
file: iconPath
|
|
570
|
+
}));
|
|
571
|
+
resp.write(ico, 'binary');
|
|
572
|
+
resp.end();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Attempt to locate the index it is missing or we are at the root of a directory.
|
|
577
|
+
if (requestUrl === '' || requestUrl === '/' || !ext) {
|
|
578
|
+
// Determine the correct home page by checking the allowed extensions:
|
|
579
|
+
const index = this.#getIndexForLocation(requestUrl);
|
|
580
|
+
// No home page found:
|
|
581
|
+
if (!index) {
|
|
582
|
+
// Show the directory listing if it is allowed.
|
|
583
|
+
this.#showDirListing(resp, systemPath);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// If we found an index make sure the requested path ends with a path separator.
|
|
587
|
+
if (requestUrl[requestUrl.length - 1] !== '/') {
|
|
588
|
+
requestUrl += '/';
|
|
589
|
+
// This means the system path needs it added as well.
|
|
590
|
+
systemPath += Path.sep;
|
|
591
|
+
}
|
|
592
|
+
// Home page was located, update variables and load it in a minute.
|
|
593
|
+
requestUrl += index;
|
|
594
|
+
systemPath += index;
|
|
595
|
+
filename = index;
|
|
596
|
+
ext = Path.extname(index);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Did the user specifically request a dir?
|
|
600
|
+
if (!ext) {
|
|
601
|
+
if (requestUrl.slice(-1) === '/') {
|
|
602
|
+
requestUrl = requestUrl.substring(0, requestUrl.length - 1);
|
|
603
|
+
}
|
|
604
|
+
if (systemPath.slice(-1) === Path.sep) {
|
|
605
|
+
systemPath = systemPath.substring(0, systemPath.length - 1);
|
|
606
|
+
}
|
|
607
|
+
this.#showDirListing(resp, systemPath);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Attempt to access the requested file.
|
|
612
|
+
try {
|
|
613
|
+
Fs.accessSync(systemPath, Fs.constants.R_OK);
|
|
614
|
+
} catch (err) {
|
|
615
|
+
if (err.message.includes('no such')) {
|
|
616
|
+
this.#showNotFound(resp);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
// If this forbidden is called it means there are file permission errors.
|
|
620
|
+
this.#showForbidden(resp);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Get the file.
|
|
625
|
+
const file = Fs.readFileSync(systemPath, { encoding: 'binary' });
|
|
626
|
+
|
|
627
|
+
// Output the file to the browser.
|
|
628
|
+
resp.writeHead(HTTPStatus.ok, this.#getHeaders({
|
|
629
|
+
contentType: ContentTypes[ext] || this.#OPS.contentType,
|
|
630
|
+
file: systemPath
|
|
631
|
+
}));
|
|
632
|
+
|
|
633
|
+
// If needed inject NSS's WebSocket at the end of the page.
|
|
634
|
+
if (this.#reload.includes(ext)) {
|
|
635
|
+
let html = file.toString();
|
|
636
|
+
const last = html.lastIndexOf('</body>');
|
|
637
|
+
if (last && last > 0) {
|
|
638
|
+
const start = html.substring(0, last);
|
|
639
|
+
const end = html.substring(last);
|
|
640
|
+
html = start + this.#handlers.liveReloading + end;
|
|
641
|
+
resp.write(html, 'utf-8');
|
|
642
|
+
} else {
|
|
643
|
+
resp.write(file, 'binary');
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
resp.write(file, 'binary');
|
|
647
|
+
}
|
|
648
|
+
resp.end();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* If enabled displays the directory listing page to the user.
|
|
653
|
+
*
|
|
654
|
+
* @param {ServerResponse} resp The response object for a request being made.
|
|
655
|
+
* @param {string} systemPath The directory that the user is trying to view.
|
|
656
|
+
* @return {void} Used only as a short circuit.
|
|
657
|
+
*/
|
|
658
|
+
#showDirListing(resp, systemPath) {
|
|
659
|
+
if (!Fs.existsSync(systemPath)) {
|
|
660
|
+
this.#showNotFound(resp);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (!this.#OPS.dirListing || !Fs.lstatSync(systemPath).isDirectory()) {
|
|
665
|
+
this.#showForbidden(resp);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const { directories, files } = this.#getDirList(systemPath);
|
|
670
|
+
let html = this.#handlers.dirListing;
|
|
671
|
+
html = html.replace('{{files}}', files);
|
|
672
|
+
html = html.replace('{{directories}}', directories);
|
|
673
|
+
resp.writeHead(HTTPStatus.ok, this.#getHeaders({ contentType: 'text/html' }));
|
|
674
|
+
resp.write(html, 'utf8');
|
|
675
|
+
resp.end();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Displays the 403 forbidden page to the user.
|
|
680
|
+
*
|
|
681
|
+
* @param {ServerResponse} resp The response object for a request being made.
|
|
682
|
+
*/
|
|
683
|
+
#showForbidden(resp) {
|
|
684
|
+
resp.writeHead(HTTPStatus.forbidden, this.#getHeaders({ contentType: 'text/html' }));
|
|
685
|
+
resp.write(this.#handlers.forbidden);
|
|
686
|
+
resp.end();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Displays the 404 page not found to the user.
|
|
691
|
+
*
|
|
692
|
+
* @param {ServerResponse} resp The response object for a request being made.
|
|
693
|
+
*/
|
|
694
|
+
#showNotFound(resp) {
|
|
695
|
+
resp.writeHead(HTTPStatus.notFound, this.#getHeaders({ contentType: 'text/html' }));
|
|
696
|
+
resp.write(this.#handlers.notFound);
|
|
697
|
+
resp.end();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* The WebSocket listener for NSS. All WebSocket requests are handled here.
|
|
702
|
+
*
|
|
703
|
+
* @param {object} socket The WebSocket object for this connection.
|
|
704
|
+
* @param {object} request The incoming initial connection.
|
|
705
|
+
*/
|
|
706
|
+
#socketListener(socket, request) {
|
|
707
|
+
|
|
708
|
+
// Strip the page ID and /ws tag off the url to get the actual url.
|
|
709
|
+
let cleanURL = request.url.substr(1, request.url.indexOf('/ws?id=') - 1);
|
|
710
|
+
if (!cleanURL) {
|
|
711
|
+
cleanURL = this.#OPS.indexPage;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Do not assuming this is a directory listing, check for an index first.
|
|
715
|
+
if (!Path.extname(cleanURL)) {
|
|
716
|
+
const index = this.#getIndexForLocation(cleanURL);
|
|
717
|
+
if (index) {
|
|
718
|
+
if (cleanURL[cleanURL.length - 1] !== '/') {
|
|
719
|
+
cleanURL += '/';
|
|
720
|
+
}
|
|
721
|
+
cleanURL += index;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Record the unique page ID directly on the socket object.
|
|
726
|
+
const pageId = request.url.substr(request.url.indexOf('?id=')).replace('?id=', '');
|
|
727
|
+
// eslint-disable-next-line no-param-reassign
|
|
728
|
+
socket.nssUid = pageId;
|
|
729
|
+
|
|
730
|
+
// Overwrite the default send method to NSS's standard.
|
|
731
|
+
const originalSend = socket.send.bind(socket);
|
|
732
|
+
// eslint-disable-next-line no-param-reassign
|
|
733
|
+
socket.send = (message) => {
|
|
734
|
+
originalSend(JSON.stringify({
|
|
735
|
+
message,
|
|
736
|
+
type: this.whatIs(message)
|
|
737
|
+
}));
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// Record new socket connections.
|
|
741
|
+
if (this.#sockets.routes[cleanURL]) {
|
|
742
|
+
this.#sockets.routes[cleanURL].push(socket); // This page has opened multiple times.
|
|
743
|
+
} else {
|
|
744
|
+
this.#sockets.routes[cleanURL] = [socket]; // First time we've seen this page.
|
|
745
|
+
}
|
|
746
|
+
this.#sockets.map[`S${pageId}`] = socket;
|
|
747
|
+
|
|
748
|
+
// If auto restart is supposed to be disabled tell the page now.
|
|
749
|
+
if (this.#OPS.disableAutoRestart && this.#OPS.disableAutoRestart === true) {
|
|
750
|
+
socket.send('disableAutoRestart');
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Handle future incoming WebSocket messages from this page.
|
|
754
|
+
socket.on('message', (message) => {
|
|
755
|
+
// NSS messages have a standard format
|
|
756
|
+
const msgObj = JSON.parse(message.toString());
|
|
757
|
+
// See if the message belongs to a callback and send it there.
|
|
758
|
+
for (let i = 0; i < this.#OPS.callbacks.length; i++) {
|
|
759
|
+
const regex = this.#OPS.callbacks[i][0];
|
|
760
|
+
const callback = this.#OPS.callbacks[i][1];
|
|
761
|
+
if (regex.test(cleanURL)) {
|
|
762
|
+
callback(msgObj, pageId);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// No one is listening for this message.
|
|
767
|
+
console.log(`Unanswered WebSocket message from ${cleanURL}: ${message.toString()}`);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// When a connection closes remove it from CONNECTIONS.
|
|
771
|
+
socket.on('close', () => {
|
|
772
|
+
// Remove this page from our list of active routes.
|
|
773
|
+
const connections = this.#sockets.routes[cleanURL];
|
|
774
|
+
for (let i = 0; i < connections.length; i++) {
|
|
775
|
+
if (connections[i].nssUid === pageId) {
|
|
776
|
+
connections.splice(i, 1);
|
|
777
|
+
delete this.#sockets.map[pageId];
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Attempt to start the HTTP server and WebSocket listener.
|
|
787
|
+
*
|
|
788
|
+
* @param {int|null} port Allows force overriding of port number; you should usually not use
|
|
789
|
+
* this, it's meant to be used internally to NSS.
|
|
790
|
+
* @param {function} [callback] Optional function to call when the server successfully starts
|
|
791
|
+
* (true) or gives up on trying to start (false);
|
|
792
|
+
* @return {void} Used only as a short circuit.
|
|
793
|
+
*/
|
|
794
|
+
start(port, callback) {
|
|
795
|
+
|
|
796
|
+
// Port is usually internal to NSS so check if a user placed the callback first.
|
|
797
|
+
if (port && typeof port === 'function') {
|
|
798
|
+
// eslint-disable-next-line no-param-reassign
|
|
799
|
+
callback = port;
|
|
800
|
+
// eslint-disable-next-line no-param-reassign
|
|
801
|
+
port = null;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Make sure we have a proper callback function or null the variable.
|
|
805
|
+
if (callback && typeof callback !== 'function') {
|
|
806
|
+
// eslint-disable-next-line no-param-reassign
|
|
807
|
+
callback = null;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Don't start an already running server.
|
|
811
|
+
if (this.#OPS.running) {
|
|
812
|
+
console.log('Server is already running.');
|
|
813
|
+
// Notify the callback.
|
|
814
|
+
if (callback) {
|
|
815
|
+
callback(true);
|
|
816
|
+
}
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Create the HTTP server.
|
|
821
|
+
this.#server = Http.createServer(this.#serverListener.bind(this));
|
|
822
|
+
// Capture connection upgrade requests so we don't break WebSocket connections.
|
|
823
|
+
this.#server.on('upgrade', (request, socket) => {
|
|
824
|
+
/*
|
|
825
|
+
* Node's http server is capable of handling websocket but you have to manually
|
|
826
|
+
* handle a lot of work like the handshakes. We use WebSocketServer to avoid
|
|
827
|
+
* having to do all that extra work. See the following if you want to handle
|
|
828
|
+
* websocket without the WebSocket module:
|
|
829
|
+
* https://medium.com/hackernoon/implementing-a-websocket-server-with-node-js-d9b78ec5ffa8
|
|
830
|
+
*
|
|
831
|
+
* this.#server.upgrade will clash with this.#socket.connection by running first. Currently
|
|
832
|
+
* this.#server.upgrade is not used but in case we do in the future ignore all websocket
|
|
833
|
+
* requests so they get passed on to the this.#socket.connection listener.
|
|
834
|
+
*/
|
|
835
|
+
if (request.headers.upgrade === 'websocket') {
|
|
836
|
+
// eslint-disable-next-line no-useless-return
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
// Capture server errors and respond as needed.
|
|
841
|
+
this.#server.on('error', (error) => {
|
|
842
|
+
// The port we tried to use is taken, increment and try to start again.
|
|
843
|
+
if (error.code === 'EADDRINUSE') {
|
|
844
|
+
if (port) {
|
|
845
|
+
// Stop trying new ports after 100 attempts.
|
|
846
|
+
if (this.#OPS.port - port > 100) {
|
|
847
|
+
console.log(`FATAL ERROR: Could not find an available port number in the range of ${this.#OPS.port}–${this.#OPS.port + 100}.`);
|
|
848
|
+
// Notify the callback.
|
|
849
|
+
if (callback) {
|
|
850
|
+
callback(false);
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
this.start(port + 1, callback);
|
|
855
|
+
} else {
|
|
856
|
+
this.start(this.#OPS.port + 1, callback);
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
console.log('Server error', error);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// Attempt to start the server now.
|
|
864
|
+
// eslint-disable-next-line no-param-reassign
|
|
865
|
+
port = port || this.#OPS.port;
|
|
866
|
+
this.#server.listen(port, () => {
|
|
867
|
+
// Server started successfully without error.
|
|
868
|
+
this.#OPS.running = true;
|
|
869
|
+
|
|
870
|
+
// Start the WebSocket Server on the same port.
|
|
871
|
+
this.#socket = new WebSocketServer({ server: this.#server });
|
|
872
|
+
this.#socket.on('connection', this.#socketListener.bind(this));
|
|
873
|
+
|
|
874
|
+
// Warn the user if we had to change port numbers.
|
|
875
|
+
if (port && (port !== this.#OPS.port)) {
|
|
876
|
+
console.log(`Port ${this.#OPS.port} was in use, switched to using ${port}.\n`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Record port in use.
|
|
880
|
+
this.#OPS.portInUse = port;
|
|
881
|
+
|
|
882
|
+
// Log the ip addresses being watched.
|
|
883
|
+
console.log('Node Simple Server live @:');
|
|
884
|
+
const addresses = this.getAddresses(port);
|
|
885
|
+
addresses.forEach((address) => {
|
|
886
|
+
console.log(` ${address}`);
|
|
887
|
+
});
|
|
888
|
+
console.log('');
|
|
889
|
+
|
|
890
|
+
// Notify the callback.
|
|
891
|
+
if (callback) {
|
|
892
|
+
callback(true);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Stop the HTTP server and WebSocket listener gracefully.
|
|
899
|
+
*
|
|
900
|
+
* @param {function} [callback] Optional function to call when the server successfully
|
|
901
|
+
* stops (true).
|
|
902
|
+
*/
|
|
903
|
+
stop(callback) {
|
|
904
|
+
if (this.#OPS.running) {
|
|
905
|
+
// If any back-end files are being watched for changes stop monitoring them.
|
|
906
|
+
this.watchEnd();
|
|
907
|
+
// Close all socket connections; these would force the server to stay up.
|
|
908
|
+
const keys = Object.keys(this.#sockets);
|
|
909
|
+
keys.forEach((key) => {
|
|
910
|
+
this.#sockets.routes[key].forEach((socket) => {
|
|
911
|
+
socket.send('close');
|
|
912
|
+
socket.close();
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
// Now gracefully close SERVER and SOCKET.
|
|
916
|
+
this.#server.close();
|
|
917
|
+
this.#socket.close();
|
|
918
|
+
// Reset NSS.
|
|
919
|
+
this.#server = null;
|
|
920
|
+
this.#socket = null;
|
|
921
|
+
this.#OPS.running = false;
|
|
922
|
+
console.log('Server has been stopped.');
|
|
923
|
+
}
|
|
924
|
+
// Notify the callback.
|
|
925
|
+
if (callback) {
|
|
926
|
+
callback(true);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Stop watching directories or files for changes; previously registered with watch().
|
|
932
|
+
*
|
|
933
|
+
* Warning: If you watched a directory for changes watch() will auto watch all contents
|
|
934
|
+
* of that directory recursively. You will need a different paths argument to truly
|
|
935
|
+
* remove all files, it may be easier to call watchEnd() and then restart the watch()
|
|
936
|
+
* you still need.
|
|
937
|
+
*
|
|
938
|
+
* @param {String|Array} paths Files, directories, or glob patterns for tracking. Takes an
|
|
939
|
+
* array of strings or just one string.
|
|
940
|
+
*/
|
|
941
|
+
unwatch(paths) {
|
|
942
|
+
// Convert paths to array if it's not already.
|
|
943
|
+
if (this.whatIs(paths) === 'string') {
|
|
944
|
+
// eslint-disable-next-line no-param-reassign
|
|
945
|
+
paths = [paths];
|
|
946
|
+
}
|
|
947
|
+
// Search all watchers and remove any paths that match.
|
|
948
|
+
this.#watching.forEach((watcher) => {
|
|
949
|
+
watcher.unwatch(paths);
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Start watching a file, files, directory, or directories for changes and then callback to
|
|
955
|
+
* functions that can/ will respond to these changes.
|
|
956
|
+
*
|
|
957
|
+
* @param {string|array} paths Files, directories, or glob patterns for tracking. Takes an
|
|
958
|
+
* array of strings or just one string.
|
|
959
|
+
* @param {object} options A special configuration object that includes chokidar options and NSS options.
|
|
960
|
+
* There are to many options to list here, see NSS's README for more
|
|
961
|
+
* information and remember `options.events` is required!
|
|
962
|
+
* @return {boolean} True if it appears everything worked, false if something was missing or
|
|
963
|
+
* an error was thrown.
|
|
964
|
+
*/
|
|
965
|
+
watch(paths, options) {
|
|
966
|
+
// Insure we have an options object.
|
|
967
|
+
if (!options || !options.events) {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
/*
|
|
971
|
+
* For security and a better user experience set the watchers current working
|
|
972
|
+
* directory to NSS's root if the setting is missing.
|
|
973
|
+
*/
|
|
974
|
+
if (!options.cwd) {
|
|
975
|
+
// eslint-disable-next-line no-param-reassign
|
|
976
|
+
options.cwd = this.#OPS.root;
|
|
977
|
+
}
|
|
978
|
+
// Convert paths to array if it's not already.
|
|
979
|
+
if (this.whatIs(paths) === 'string') {
|
|
980
|
+
// eslint-disable-next-line no-param-reassign
|
|
981
|
+
paths = [paths];
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
// Start watching the path(s).
|
|
985
|
+
const watcher = Chokidar.watch(paths, options);
|
|
986
|
+
this.#watching.push(watcher);
|
|
987
|
+
// Hookup requested listeners; they are case sensitive so type them right in your code!
|
|
988
|
+
const safe = ['all', 'add', 'addDir', 'change', 'unlink', 'unlinkDir', 'ready', 'raw', 'error'];
|
|
989
|
+
Object.keys(options.events).forEach((key) => {
|
|
990
|
+
if (safe.includes(key)) {
|
|
991
|
+
/**
|
|
992
|
+
* NON-STANDARD ALTERATION
|
|
993
|
+
*
|
|
994
|
+
* Chokidar provides paths in the correct OS format but NSS will change
|
|
995
|
+
* all backslashes (\) into forward slashes (/).
|
|
996
|
+
*/
|
|
997
|
+
watcher.on(key, (evt, path) => {
|
|
998
|
+
// Capture the call and alter the path before passing it on.
|
|
999
|
+
const altPath = path.replace(/\\/g, '/');
|
|
1000
|
+
// Since we're messing with the path already grab the extension for the user.
|
|
1001
|
+
const ext = Path.extname(altPath);
|
|
1002
|
+
options.events[key](evt, altPath, ext.replace('.', ''));
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
console.log(error);
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Stop watching registered file, files, directory, or directories for changes.
|
|
1015
|
+
*/
|
|
1016
|
+
watchEnd() {
|
|
1017
|
+
this.#watching.forEach((watcher) => {
|
|
1018
|
+
watcher.close();
|
|
1019
|
+
});
|
|
1020
|
+
this.#watching.splice(0, this.#watching.length);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* The fastest way to get the actual type of anything in JavaScript.
|
|
1025
|
+
*
|
|
1026
|
+
* {@link https://jsbench.me/ruks9jljcu/2 | See benchmarks}.
|
|
1027
|
+
*
|
|
1028
|
+
* @param {*} unknown Anything you wish to check the type of.
|
|
1029
|
+
* @return {string|undefined} The type in lowercase of the unknown value passed in or undefined.
|
|
1030
|
+
*/
|
|
1031
|
+
whatIs(unknown) {
|
|
1032
|
+
try {
|
|
1033
|
+
return ({}).toString.call(unknown).match(/\s([^\]]+)/)[1].toLowerCase();
|
|
1034
|
+
} catch (e) { return undefined; }
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export default NodeSimpleServer;
|