-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
391 lines (358 loc) · 10.7 KB
/
index.js
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
/* eslint-env node */
/**
* @description Server builder.
* @module
* @requires http
* @requires https
* @requires colors
* @requires external-ip
* @requires ./utils
* @exports Server
*/
const { info, error } = require('nclr');
const { use, getPublicIP } = require('./src/utils');
/**
* Normalize a port into a number, string, or false.
* @param {(string|number)} val Port
* @return {(string|number|boolean)} Port
* @protected
*/
const normalizePort = (val) => {
let port = parseInt(val, 10); /* @todo test if val | 0 is better*/
if (isNaN(port)) return port; //Named pipe
if (port >= 0) return port; //Port number
return false;
};
/**
* @description Default options for {@link Server.constructor}.
* @type {{name: string, useHttps: boolean, securityOptions: Object, showPublicIP: boolean, silent: boolean}}
*/
const DEFAULT_OPTS = {
name: 'Server',
useHttps: false,
useHttp2: false,
securityOptions: {},
showPublicIP: false,
silent: false
};
/**
* @name getEnv
* @description Get the environment name.
* @param {function|Object} app Application
* @protected
*/
const getEnv = (app) => {
if (process.env.NODE_ENV) return process.env.NODE_ENV;
return (typeof app.get === 'function') ? app.get('env') : 'development';
};
/**
* @description Create a server.
* @param {Server} instance Server instance
* @returns {(http.Server|https.Server|http2.Server)} HTTP* server
*/
const createServer = (instance) => {
if (instance._useHttp2) return require('http2').createSecureServer(instance._options, instance._app);
return instance._useHttps ?
require('https').createServer(instance._options, instance._app) :
require('http').createServer(instance._app);
};
/**
* @description Re-usable server.
* @public
*/
class Server {
/**
* @description Create a NodeJS HTTP(s) server.
* @param {express} associatedApp Associated express application
* @param {(string|number)} [port=(process.env.PORT || 3e3)] Port/pipe to use
* @param {{name: string, useHttps: boolean, useHttp2: boolean, securityOptions: object, showPublicIP: boolean, silent: boolean, gracefulClose: boolean, autoRun: boolean}} [opts={name: 'Server', useHttps: false, securityOptions: {}, showPublicIP: false, silent: false, gracefulClose: true, autoRun: false}]
* Options including the server's name, HTTPS, options needed for the HTTPs server (public keys and certificates) and whether it should show its public
* IP and whether it needs to be silent (<em>which won't affect the public IP log</em>) and if it should run automatically upon being instantiated.
*
*
* @example
* const express = require('express');
* let opts = {
* name: 'Custom Server'
* }
* let server = new Server(express(), 3002, opts);
* server
* .run()
* .then(serv => console.log('READY'), console.error)
* @memberof Server
* @throws {Error} Invalid port
* @returns {undefined|Promise} Nothing or the promise returned by <code>run</code>
*/
constructor(associatedApp, port = (process.env.PORT || 3e3), opts = DEFAULT_OPTS) {
this._port = normalizePort(port);
if (this._port === NaN || !this._port) throw new Error(`Port should be >= 0 and < 65536. Received ${this._port}`);
this._useHttp2 = opts.useHttp2 || DEFAULT_OPTS.useHttp2;
this._useHttps = opts.useHttps || DEFAULT_OPTS.useHttps;
this._app = associatedApp;
this._options = opts.securityOptions || DEFAULT_OPTS.securityOptions;
this._silent = opts.silent || DEFAULT_OPTS.silent;
this._server = createServer(this);
this._name = opts.name || DEFAULT_OPTS.name;
this._server.name = this._name;
this._server.on('error', this.onError);
this._showPublicIP = opts.showPublicIP || DEFAULT_OPTS.showPublicIP;
this._env = getEnv(this._app);
this._handler = () => {
if (!this._silent) {
try {
info(`${this._name} listening at ${use('inp', this.address)} (${this._env} environment)`);
} catch (err) {
this.onError(err);
}
}
};
opts.gracefulClose && process.on('SIGTERM', () => this.close()) && process.on('SIGINT', () => this.close());
if (opts.autoRun) return this.run();
}
/**
* @description Get the associated application (Express instance).
* @return {express} Associated Express instance
* @memberof Server
* @public
*/
get app() {
return this._app;
}
/**
* @description Set the Express application associated to the router.
* @param {express} value Express app
* @memberof Server
* @public
*/
set app(value) {
this._app = value;
}
/**
* @description Get the port/pipe of used by the server.
* @return {(number|string)} Port/pipes
* @memberof Server
* @public
*/
get port() {
return this._port;
}
/**
* @description Change the port/pipe of the server <strong>without affecting the server instance</strong>.<br>
* <em style="color: red">Use this method at your own risk!</em>
* @param {(number|string)} value New port/pipe
* @memberof Server
* @public
*/
set port(value) {
this._port = value;
}
/**
* @description Get the server's name.
* @return {string} Name
* @memberof Server
* @public
*/
get name() {
return this._name;
}
/**
* @description Change the server's name.
* @param {string} value New name
* @memberof Server
* @public
*/
set name(value) {
this._name = value;
}
/**
* @description See if whether or not the server is using HTTPS.
* @return {boolean} S flag
* @memberof Server
* @public
*/
get useHttps() {
return this._useHttps;
}
/**
* @description Changes the HTTP<strong>S</strong> flag <strong>without affecting the server instance</strong>.<br>
* <em style="color: red">Use this method at your own risk!</em>
* @param {boolean} value New flag
* @memberof Server
* @public
*/
set useHttps(value) {
this._useHttps = value;
}
/**
* @description See if whether or not the server is using HTTP/2.
* @return {boolean} Version 2 flag
* @memberof Server
* @public
*/
get useHttp2() {
return this._useHttp2;
}
/**
* @description Changes the HTTP version flag <strong>without affecting the server instance</strong>.<br>
* <em style="color: red">Use this method at your own risk!</em>
* @param {boolean} value New flag
* @memberof Server
* @public
*/
set useHttp2(value) {
this._useHttp2 = value;
}
/**
* @description Get the server options (that is the ones used for the HTTPS and HTTP/2 mode).
* @return {Object} Options
* @memberof Server
* @public
*/
get options() {
return this._options;
}
/**
* @description Change the server options <strong>without affecting the server instance</strong>.<br>
* <em style="color: red">Use this method at your own risk!</em>
* @param {Object} value New options
* @memberof Server
* @public
*/
set options(value) {
this._options = value;
}
/**
* @description Get the server's instance.
* @return {(http.Server|https.Server|http2.Server)} Server instance
* @memberof Server
* @public
*/
get server() {
return this._server;
}
/**
* @description Change the server's instance.<br>
* <em>However it's recommended to restart the server otherwise the event handlers won't work properly.
* @param {(http.Server|https.Server|http2.Server)} value New server instance
* @memberof Server
* @public
*/
set server(value) {
this._server = value;
}
/**
* @description Get the silent flag.
* @return {boolean} Server's silence
* @memberof Server
* @public
*/
get silent() {
return this._silent;
}
/**
* @description Change the server' silence.
* @param {boolean} value New silence mode
* @memberof Server
* @public
*/
set silent(value) {
this._silent = value;
}
/**
* @description Get the HTTP protocol.
* @returns {string} Protocol
*/
get protocol() {
return (this._useHttps || this._useHttp2) ? 'https' : 'http'
}
/**
* @description Get the server's address.
* @returns {string} Address
*/
get address() {
const ipAddress = this._server.address();
const location = typeof ipAddress === 'string' ?
`pipe ${ipAddress}` :
`${this.protocol}://${(ipAddress.address === '::') ? 'localhost' : ipAddress.address}:${ipAddress.port}`;
return location
}
/**
* @description Run/start the server.
* @memberof Server
* @returns {Server} Server
* @throws {Error} Running error
* @public
*/
async run() {
try {
let server = await this._server.listen(this._port, this._handler);
if (this._showPublicIP) {
let ip = await getPublicIP();
info(`Public IP: ${use('spec', ip)}`);
}
return this;
} catch (err) {
this.onError(err);
}
}
/**
* @description Event listener for HTTP server "error" event.
* @param {Error} error Error to handle
* @memberof Server
* @public
* @returns {function(Error)} Error handler
* @throws {Error} EACCES/EADDRINUSE/ENOENT errors
*/
onError(error) {
/*
ERR_SERVER_ALREADY_LISTEN (listen method called more than once w/o closing)
ERR_SERVER_NOT_RUNNING (Server is not running or in Node 8 "Not running")
...
*/
if (error.syscall !== 'listen') throw error;
const port = this.address().port;
const bind = (typeof port === 'string') ? `Pipe ${port}` : `Port ${port}`;
//Handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
throw new Error(`${bind} requires elevated privileges`);
case 'EADDRINUSE':
throw new Error(`${bind} is already in use`);
default:
throw error;
}
};
/**
* @description Gracefully close the server.
* @returns {Promise} Closure promise
* @throws {Error} Closing error
* @memberof Server
* @public
*/
async close() {
try {
let closed = await new Promise((resolve, reject) => {
this._server.close((err) => {
if (err) reject(err);
if (!this._silent) info(`Closing the server ${use('out', this.name)}...`);
resolve(true);
});
});
if (!this._silent) info(`${use('out', this.name)} is now closed.`);
return closed;
} catch (err) {
error(`Server closure of ${use('out', this.name)} led to:`, err);
this.onError(err);
}
}
/**
* @description Textual representation of a Server object.
* @return {string} Server object in text
* @memberof Server
* @public
*/
toString() {
return `Server(name='${this.name}', port=${this.port}, app=${this.app}, useHttps=${this.useHttps}, useHttp2=${this.useHttp2}, options=${JSON.stringify(this.options)})`
}
}
module.exports = Server;