-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
367 lines (311 loc) · 10.6 KB
/
server.js
File metadata and controls
367 lines (311 loc) · 10.6 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
// ============================================================================
// NodeHookR
//
// This is the main service entry point. It loads the configuration file,
// sets up the logger service, and then initializes the router. When are
// request comes in, it matches it with a route and if it passes, then
// executes the route.
// ============================================================================
const fs = require('fs');
const winston = require('winston');
const http = require('http');
const url = require('url');
const Router = require('./router');
const Mailer = require('./mailer');
const autobind = require('auto-bind');
const Errors = require('./errors');
const AppError = Errors.AppError;
const RequestError = Errors.RequestError;
require('winston-daily-rotate-file');
require('http-shutdown').extend();
class Server {
constructor() {
// Set default values
this._configFile = './config.json';
this._loggers = [];
autobind(this);
}
// -----------------------------------------------------
// Return configuration
// -----------------------------------------------------
get config() {
// Only read the config file once, but allow the config
// file to be re-loaded at a later time (i.e. file watch)
// at some future release.
if (!this._configData) {
if (fs.existsSync(this._configFile)) {
let content = fs.readFileSync(this._configFile);
this._configData = JSON.parse(content);
} else {
this._configData = { log: [] };
}
}
return this._configData;
}
// -----------------------------------------------------
// START Server
// -----------------------------------------------------
start(file) {
// Set the config file if specified, otherwise, use the default
// It is done this way because later config() will be hot loaded
this._configFile = file || './config.json';
// Initialize loggers
for (let [ log, opts ] of Object.entries(this.config.log)) {
// Only initialize valid logger entries
if (log.match(/\bservice|\brouter|\bplugins|\brequests/)) {
if (opts.enabled === undefined || opts.enabled) this._loggers[log] = this._createLogger(log, opts);
}
}
this._servicelog('info', 'NodeHookR initializing');
// Setup catch-all for async functions in plugins that are not
// properly catching the exceptions
process.on('unhandledRejection', (err) => {
this._handleAppError(
new AppError('Unhandled promise rejection. Plugin may not be properly handling error.', err)
);
});
// Set up the callbacks for the plugins
this._hooks = {
config: this.config,
sendmail: this._sendmail.bind(this),
logger: this._pluginlog.bind(this)
};
// Initialize the router and mailer. If any errors
// occur during initalization, the service will quit
this._router = new Router(this._hooks, this._routerlog, this._handleAppError);
this._mailer = new Mailer(this._hooks, this._mailerlog);
// Set the default server port
const port = this.config.port || '3000';
// Initialize the http server
this._server = http
.createServer((req, res) => {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Request-Method', '*');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, PATCH, DELETE');
res.setHeader('Access-Control-Allow-Headers', '*');
try {
this._handleRequest(req, res);
} catch (err) {
if (err.name === 'RequestError') this._handleRequestError(err, req, res);
else this._handleAppError(err);
}
})
.listen(port)
.withShutdown();
const msg = 'Server running on port ' + port + '. Using config [' + this._configFile + ']';
console.log(msg);
this._servicelog('info', msg);
return this;
}
// -----------------------------------------------------
// Shutdown server
// -----------------------------------------------------
shutdown() {
this._servicelog('info', 'NodeHookR shutting down');
if (!this._server) return;
this._server.shutdown(() => {
this._servicelog('info', 'NodeHookR shutdown complete');
});
return this;
}
// -----------------------------------------------------
// Sendmail
// -----------------------------------------------------
_sendmail(opts) {
this._mailer.send(opts).catch((err) => {
this._handleAppError(err);
});
}
// -----------------------------------------------------
// Log handlers
// -----------------------------------------------------
_servicelog(severity, message, data) {
this._log('service', severity, message, data);
}
_requestslog(severity, message, data) {
this._log('requests', severity, message, data);
}
_routerlog(severity, message, data) {
this._log('router', severity, message, data);
}
_pluginlog(severity, message, data) {
this._log('plugins', severity, message, data);
}
_log(logname, severity, message, data) {
if (!logname) return;
const logger = this._loggers[logname];
if (!logger) return;
let sev = 'info';
if (!!severity) sev = severity.toLowerCase();
switch (sev) {
case 'info':
logger.info(message, data);
break;
case 'warn':
logger.warn(message, data);
break;
case 'error':
logger.error(message, data);
break;
}
}
// -----------------------------------------------------
// Application error
//
// These errors are things that have occured in the
// server which are internal and should not be reported
// to the user (such as bad plugins, error sending mail,
// etc.)
// -----------------------------------------------------
_handleAppError(err) {
this._servicelog('error', err.message, err);
if (this.config.mailer && this.config.mailer.apperrors) {
// Omitting the enabled means to still do error notification
// only if it is explicitly disabled, do we not send
if (this.config.mailer.enabled !== false) {
const cfg = this.config.mailer.apperrors;
let body = 'Error:\n\n' + err.message + '\n\nStack Trace:\n\n' + err.stack;
// If there is an inner exception, we want to show it as well.
if (err.inner)
body += '\n\nInner Error:\n\n' + JSON.stringify(err.inner, null, 2) + '\n\n' + err.inner.stack;
let opts = Object.assign(
{ text: body, subject: (cfg.prefix || '[NodeHookR ERROR] ') + err.message },
cfg
);
this._mailer.send(opts).catch((err) => {
// Well, if we get an error while handling an error,
// all we can do is log it. Emails are broken
this._servicelog('error', err.message, { error: err, stack: err.stack });
});
}
}
}
// -----------------------------------------------------
// Request errors
//
// These errors are reserved for those which the user
// should know about, such as invalid routes or
// bad parameters being specified.
// -----------------------------------------------------
_handleRequestError(err, request, response) {
const url_parts = url.parse(request.url, true);
let payload = '';
if (!!request.post) payload = request.post.Value;
this._requestslog('error', err.message, { error: err, client: url_parts, payload: payload });
response.writeHead(err.code, { 'Content-Type': 'text/plain' });
response.write(err.message);
response.end();
}
// -----------------------------------------------------
// Handle http requests
// -----------------------------------------------------
_handleRequest(request, response) {
// Split out the URL
const url_parts = url.parse(request.url, true);
this._requestslog('info', 'HTTP Request', { client: url_parts });
const path = url_parts.pathname;
if (path === '/favicon.ico') {
response.writeHead(204, { 'Content-Type': 'text/plain' });
response.end();
return;
}
// If we didn't find a matching route, return an error
if (!this._router.exists(path)) {
throw new RequestError(422, 'Invalid or no route supplied');
}
// If the route doesn't match the type, then return an error
if (!this._router.matches(request.method, path)) {
throw new RequestError(405, 'Specified route does not support ' + request.method);
}
this._processRequest(request, response);
}
// -----------------------------------------------------
// Process request data
// -----------------------------------------------------
_processRequest(request, response) {
let queryData = '';
request.on('data', (data) => {
queryData += data;
if (queryData.length > 1e6) {
queryData = '';
response.writeHead(413, { 'Content-Type': 'text/plain' }).end();
request.connection.destroy();
}
});
request.on('end', async () => {
let payload = '';
if (this._isJSON(queryData)) payload = JSON.parse(queryData);
else payload = queryData;
const url_parts = url.parse(request.url, true);
const params = url_parts.query;
response.remoteAddress = request.connection.remoteAddress;
response.remoteHost = request.headers.host;
response.remoteOrigin = request.headers.origin;
response.remoteUserAgent = request.headers['user-agent'];
try {
let results = await this._router.exec(request.method, url_parts.pathname, params, payload);
this._writeResponse(response, results);
} catch (err) {
if (err.name === 'RequestError') {
this._handleRequestError(err, request, response);
return;
}
this._handleAppError(err);
response.writeHead(500, { 'Content-Type': 'text/plain' });
response.end();
}
});
}
_isJSON(str) {
try {
const json = JSON.parse(str);
return typeof json === 'object';
} catch (e) {
return false;
}
}
_writeResponse(response, content) {
if (!content) {
response.writeHead(204, { 'Content-Type': 'text/plain' });
} else if (typeof content === 'string') {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.write(content);
} else {
const json = JSON.stringify(content);
response.writeHead(200, { 'Content-Type': 'application/json' });
response.write(json);
}
response.end();
}
_createLogger(logprefix, opts) {
// Logs always goes to console
let transports = [];
if (opts.console) transports.push(new winston.transports.Console());
// Use /logs as default unless specified
const path = opts.path || './logs';
// If we can't find the specified path, try to create it
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
if (!!opts) {
const filename = path + '/' + (opts.prefix || logprefix + '_') + '%DATE%.log';
transports.push(
new winston.transports.DailyRotateFile(
Object.assign(
{
filename: filename,
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: opts.maxSize || '20m',
maxFiles: opts.maxFiles || '14d'
},
opts
)
)
);
}
return winston.createLogger({ transports: transports });
}
}
module.exports = Server;