forked from salesforce/refocus
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
363 lines (304 loc) · 11.6 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
/**
* Copyright (c) 2016, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or
* https://opensource.org/licenses/BSD-3-Clause
*/
/**
* ./index.js
*
* Main module to start the express server (web process). To just start the
* web process use "node index.js". To start both the web and the clock process
* use "heroku local"
*/
/* eslint-disable global-require */
/* eslint-disable no-process-env */
const throng = require('throng');
const DEFAULT_WEB_CONCURRENCY = 1;
const WORKERS = process.env.WEB_CONCURRENCY || DEFAULT_WEB_CONCURRENCY;
const sampleStore = require('./cache/sampleStoreInit');
const conf = require('./config');
const signal = require('./signal/signal');
/**
* Entry point for each clustered process.
*
* @param {Number} clusterProcessId - process id if called from throng,
* otherwise 0
*/
function start(clusterProcessId = 0) { // eslint-disable-line max-statements
console.log(`Started node process ${clusterProcessId}`);
/*
* Heroku support suggested we use segfault-handler but it's not available
* for node 8 yet.
*/
// const SegfaultHandler = require('segfault-handler');
// SegfaultHandler.registerHandler('crash.log');
const featureToggles = require('feature-toggles');
const logEnvVars = require('./utils/logEnvVars');
if (conf.newRelicKey) {
require('newrelic');
}
const helmet = require('helmet');
const swaggerTools = require('swagger-tools');
const errorHandler = require('./api/v1/errorHandler');
const path = require('path');
const fs = require('fs');
const yaml = require('js-yaml');
const ipfilter = require('express-ipfilter');
const rejectMultipleXForwardedFor =
require('./config/rejectMultipleXForwardedFor');
const bodyParser = require('body-parser');
const env = conf.environment[conf.nodeEnv];
const ENCODING = 'utf8';
const compress = require('compression');
const cors = require('cors');
const etag = require('etag');
// set up server side socket.io and redis publisher
const express = require('express');
const enforcesSSL = require('express-enforces-ssl');
const app = express();
/*
* If clusterProcessId is 0, we're running in single-process mode (i.e.
* non-throng), so log the env vars out.
* If clusterProcessId > 0, we have multiple throng workers, and in that case
* we only want to log out the env vars once, since they're all the same
* across all the throng workers, so in this case just do the logging if
* clusterProcessId is 1.
*/
if (clusterProcessId < 2) logEnvVars.log(process.env);
/*
* Call this *before* the static pages and the API routes so that both the
* static pages *and* the API responses are compressed (gzip).
*/
app.use(compress());
const httpServer = require('http').Server(app);
const io = require('socket.io')(httpServer);
const socketIOSetup = require('./realtime/setupSocketIO');
// modules for authentication
const passportModule = require('passport');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const rstore = new RedisStore({ url: conf.redis.instanceUrl.session });
socketIOSetup.init(io, rstore);
require('./realtime/redisSubscriber')(io);
// pass passport for configuration
require('./config/passportconfig')(passportModule);
// middleware for checking api token
const jwtUtil = require('./utils/jwtUtil');
// middleware for api rate limits
const rateLimit = require('./rateLimit');
// set up httpServer params
const listening = 'Listening on port';
const isDevelopment = (process.env.NODE_ENV === 'development');
const PORT = process.env.PORT || conf.port;
app.set('port', PORT);
/*
* If http is disabled, if a GET request comes in over http, automatically
* attempt to do a redirect 301 to https. Reject all other requests (DELETE,
* PATCH, POST, PUT, etc.) with a 403.
*/
if (featureToggles.isFeatureEnabled('requireHttps')) {
app.enable('trust proxy');
app.use(enforcesSSL());
}
// Reject (401) requests with multiple X-Forwarded-For values
if (featureToggles.isFeatureEnabled('rejectMultipleXForwardedFor')) {
app.use(rejectMultipleXForwardedFor);
}
// Set the IP restricitions defined in config.js
app.use(ipfilter(env.ipWhitelist, { mode: 'allow', log: false }));
if (isDevelopment) {
const webpack = require('webpack');
const webpackConfig = require('./webpack.config');
const compiler = webpack(webpackConfig);
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: webpackConfig.output.publicPath,
}));
app.use(require('webpack-hot-middleware')(compiler));
app.listen(PORT, () => {
console.log(listening, PORT); // eslint-disable-line no-console
});
} else {
httpServer.listen(PORT, () => {
console.log(listening, PORT); // eslint-disable-line no-console
});
}
/*
* Based on the change of state of the "enableSampleStore" feature flag
* populate the data into the cache or dump the data from the cache into the
* db
*/
sampleStore.init();
/*
* If the clock dyno is NOT enabled, schedule all the scheduled jobs right
* from here.
*/
if (!featureToggles.isFeatureEnabled('enableClockProcess')) {
require('./clock/index'); // eslint-disable-line global-require
}
// View engine setup
app.set('views', path.join(__dirname, 'view'));
app.set('view engine', 'pug');
// Initialize the Swagger middleware
const swaggerFile = fs // eslint-disable-line no-sync
.readFileSync(conf.api.swagger.doc, ENCODING);
const swaggerDoc = yaml.safeLoad(swaggerFile);
swaggerTools.initializeMiddleware(swaggerDoc, (mw) => {
/*
* Custom middleware to add timestamp and node cluster worker id to the
* request.
*/
app.use((req, res, next) => {
req.timestamp = Date.now();
req.clusterProcessId = clusterProcessId;
// Add "request_id" if header is set by host system, e.g. heroku.
if (req.headers && req.headers['x-request-id']) {
req.request_id = req.headers['x-request-id'];
}
/*
* Add dyno and/or clusterProcessId to request--helpful to have when
* logging. "process.env.DYNO" is only available when deployed to heroku,
* so ignore if it's not available. "clusterProcessId" represents the
* cluster worker id if the process is started from throng (it is zero if
* the process is not started from throng).
*/
if (process.env.DYNO) req.dyno = process.env.DYNO;
req.process = (req.dyno ? req.dyno + ':' : '') + clusterProcessId;
next();
});
const staticOptions = {
etag: true,
setHeaders(res, path, stat) {
res.set('ETag', etag(stat));
// give me the latest copy unless I already have the latest copy.
res.set('Cache-Control', 'public, max-age=0');
},
};
app.use('/static', express.static(path.join(__dirname, 'public'),
staticOptions));
// Set the X-XSS-Protection HTTP header as a basic protection against XSS
app.use(helmet.xssFilter());
/*
* Allow specified routes to be accessed from Javascript outside of Refocus
* through cross-origin resource sharing
* e.g. A bot that needs to get current botData from Refocus
*/
conf.corsRoutes.forEach((rte) => app.use(rte, cors()));
// Only let me be framed by people of the same origin
app.use(helmet.frameguard()); // Same-origin by default
// Remove the X-Powered-By header (which is on by default in Express)
app.use(helmet.hidePoweredBy());
// Keep browsers from sniffing mimetypes
app.use(helmet.noSniff());
/*
* Redirect '/' to the application landing page, which right now is the
* default perspective (or the first perspective in alphabetical order if
* no perspective is defined as the default).
*/
app.get('/', (req, res) => res.redirect('/perspectives'));
// Set the JSON payload limit.
app.use(bodyParser.json({ limit: conf.payloadLimit }));
/*
* Interpret Swagger resources and attach metadata to request - must be
* first in swagger-tools middleware chain.
*/
app.use(mw.swaggerMetadata());
// Use token security in swagger api routes
app.use(mw.swaggerSecurity({
jwt: (req, authOrSecDef, scopes, cb) => {
jwtUtil.verifyToken(req, cb);
},
}));
/*
* Set up API rate limits. Note that we are doing this *after* the
* swaggerSecurity middleware so that jwtUtil.verifyToken will already
* have been executed so that all of the request headers it adds are
* available for the express-limiter "lookup".
* Set the "lookup" attribute to a string or array to do a value lookup on
* the request object. For example, if we wanted to apply API request
* limits by user name and IP address, we could set lookup to
* ['headers.UserName', 'headers.x-forwarded-for'].
*/
const methods = conf.expressLimiterMethod;
const paths = conf.expressLimiterPath;
methods.forEach((method) => {
method = method.toLowerCase();
if (paths && paths.length && app[method]) {
try {
app[method](paths, rateLimit);
} catch (err) {
console.error(`Failed to initialize limiter for ${method} ${paths}`);
console.error(err);
}
}
});
// Validate Swagger requests
app.use(mw.swaggerValidator(conf.api.swagger.validator));
/*
* Route validated requests to appropriate controller. Since Swagger Router
* will actually return a response, it should be as close to the end of your
* middleware chain as possible.
*/
app.use(mw.swaggerRouter(conf.api.swagger.router));
// Serve the Swagger documents and Swagger UI
app.use(mw.swaggerUi({
apiDocs: swaggerDoc.basePath + '/api-docs', // API documetation as JSON
swaggerUi: swaggerDoc.basePath + '/docs', // API documentation as HTML
}));
// Handle Errors
app.use(errorHandler);
});
// Setup for session
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
store: rstore,
secret: conf.api.sessionSecret,
resave: false,
saveUninitialized: false,
}));
// Initialize passport and use passport for session
app.use(passportModule.initialize());
app.use(passportModule.session());
// create app routes
require('./view/loadView').loadView(app, passportModule, '/v1');
if (featureToggles.isFeatureEnabled('enableSigtermEvent')) {
/*
After receiving SIGTERM Heroku will give 30 seconds to shutdown cleanly.
If any processes remain after that time period, Dyno manager will terminate
them forcefully with SIGKILL logging 'Error R12' to indicate that the
shutdown process is not behaving correctly.
Steps:
- Stop accepting new requests;
- Handling pending resources;
- If not receive any SIGKILL a timeout will be applied killing the app
avoiding zombie process.
@see more about server.close callback:
https://nodejs.org/docs/latest-v8.x/api/net.html#net_server_close_callback
*/
process.on('SIGTERM', () => {
httpServer.close(() => {
signal.gracefulShutdown();
signal.forceShutdownTimeout();
});
});
}
module.exports = { app, passportModule };
} // start
function startMaster() {
console.log('Started node cluster master');
} // startMaster
const isProd = (process.env.NODE_ENV === 'production');
if (isProd) {
throng({
lifetime: Infinity,
master: startMaster,
start,
workers: WORKERS,
});
} else {
start();
}