-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathexpress.js
323 lines (274 loc) · 9.64 KB
/
express.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
/**
* Copyright (c) 2019, 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
*/
/**
* ./express.js
*
* Set up the express server.
*/
/* eslint-disable global-require */
/* eslint-disable no-process-env */
const logger = require('@salesforce/refocus-logging-client');
const conf = require('./config');
const signal = require('./signal/signal');
const ONE = 1;
const featureToggles = require('feature-toggles');
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');
const ipAddressUtils = require('./utils/ipAddressUtils');
const ipWhitelistUtils = require('./utils/ipWhitelistUtils');
// set up server side socket.io and redis publisher
const express = require('express');
const enforcesSSL = require('express-enforces-ssl');
const app = express();
/*
* 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,
tls: {
rejectUnauthorized: false,
requestCert: true
}
}
);
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());
}
/*
* Set req.locals.ipAddress. Make sure this comes *before* the custom
* rejectMultipleXForwardedFor middleware!
*/
app.use(ipAddressUtils.middleware);
// Reject (401) requests with multiple X-Forwarded-For values
if (featureToggles.isFeatureEnabled('rejectMultipleXForwardedFor')) {
app.use(rejectMultipleXForwardedFor);
}
// Set the IP restricitions defined in config.js
if (conf.ipWhitelistService) {
app.use(ipWhitelistUtils.middleware);
} else {
app.use(ipfilter(env.ipWhitelist, { mode: 'allow', log: false }));
}
let serverApp;
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));
serverApp = app.listen(PORT, () => {
logger.info(listening, PORT);
});
} else {
serverApp = httpServer.listen(PORT, () => {
logger.info(listening, PORT);
});
}
// 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);
if (featureToggles.isFeatureEnabled('hideRoutes')) {
for (let _path in swaggerDoc.paths) {
if (swaggerDoc.paths.hasOwnProperty(_path)) {
if (conf.hiddenRoutes.includes(_path.split('/')[ONE])) {
delete swaggerDoc.paths[_path];
}
}
}
}
swaggerTools.initializeMiddleware(swaggerDoc, (mw) => {
/*
* Custom middleware to add timestamp, request id, and dyno name to the
* request, for use in logging.
*/
app.use((req, res, next) => {
// timestamp
req.timestamp = Date.now();
// request id (heroku only)
if (req.headers && req.headers['x-request-id']) {
req.request_id = req.headers['x-request-id'];
}
// dyno name (heroku only)
if (process.env.DYNO) {
req.dyno = process.env.DYNO;
}
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());
/*
* NOTE: this is a *temporary* hack which will change once we implement UX
* designs.
*
* Redirect '/' to the application landing page, which is the environment
* variable `LANDING_PAGE_URL` if it is defined. If it is not defined then
* use the default perspective (or the first perspective in alphabetical
* order if no perspective is defined as the default).
*/
app.get('/', (req, res) =>
res.redirect(process.env.LANDING_PAGE_URL || '/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) {
logger.error(`Failed to initialize limiter for ${method} ${paths}`);
logger.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, serverApp, passportModule };