/
middleware.js
274 lines (239 loc) · 10.1 KB
/
middleware.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
// # Custom Middleware
// The following custom middleware functions are all unit testable, and have accompanying unit tests in
// middleware_spec.js
var _ = require('lodash'),
express = require('express'),
busboy = require('./ghost-busboy'),
config = require('../config'),
path = require('path'),
api = require('../api'),
passport = require('passport'),
errors = require('../errors'),
utils = require('../utils'),
middleware,
blogApp,
oauthServer,
loginSecurity = [],
forgottenSecurity = [];
function isBlackListedFileType(file) {
var blackListedFileTypes = ['.hbs', '.md', '.json'],
ext = path.extname(file);
return _.contains(blackListedFileTypes, ext);
}
function cacheBlogApp(app) {
blogApp = app;
}
function cacheOauthServer(server) {
oauthServer = server;
}
middleware = {
// ### Authenticate Middleware
// authentication has to be done for /ghost/* routes with
// exceptions for signin, signout, signup, forgotten, reset only
// api and frontend use different authentication mechanisms atm
authenticate: function (req, res, next) {
var path,
subPath;
// SubPath is the url path starting after any default subdirectories
// it is stripped of anything after the two levels `/ghost/.*?/` as the reset link has an argument
path = req.path;
/*jslint regexp:true, unparam:true*/
subPath = path.replace(/^(\/.*?\/.*?\/)(.*)?/, function (match, a) {
return a;
});
// ember-cli-ghost-app hack: removes protection from posts &
function isPostsAPI(path) {
return /^\/ghost\/api\/v0\.1\/posts(?:\/(?=$))?$/i.test(path)
|| /^\/ghost\/api\/v0\.1\/posts\/([^\\/]+?)(?:\/(?=$))?$/i.test(path)
|| /^\/ghost\/api\/v0\.1\/posts\/slug\/([^\\/]+?)(?:\/(?=$))?$/i.test(path);
}
if (subPath.indexOf('/ghost/api/') === 0
&& path.indexOf('/ghost/api/v0.1/authentication/') !== 0
&& !isPostsAPI(path)) {
return passport.authenticate('bearer', {session: false, failWithError: true},
function (err, user, info) {
if (err) {
return next(err); // will generate a 500 error
}
// Generate a JSON response reflecting authentication status
if (!user) {
var msg = {
type: 'error',
message: 'Please Sign In',
status: 'passive'
};
res.status(401);
return res.send(msg);
}
// TODO: figure out, why user & authInfo is lost
req.authInfo = info;
req.user = user;
return next(null, user, info);
}
)(req, res, next);
}
next();
},
// ### CacheControl Middleware
// provide sensible cache control headers
cacheControl: function (options) {
/*jslint unparam:true*/
var profiles = {
public: 'public, max-age=0',
private: 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
},
output;
if (_.isString(options) && profiles.hasOwnProperty(options)) {
output = profiles[options];
}
return function cacheControlHeaders(req, res, next) {
if (output) {
res.set({'Cache-Control': output});
}
next();
};
},
// ### whenEnabled Middleware
// Selectively use middleware
// From https://github.com/senchalabs/connect/issues/676#issuecomment-9569658
whenEnabled: function (setting, fn) {
return function settingEnabled(req, res, next) {
// Set from server/middleware/index.js for now
if (blogApp.enabled(setting)) {
fn(req, res, next);
} else {
next();
}
};
},
staticTheme: function () {
return function blackListStatic(req, res, next) {
if (isBlackListedFileType(req.url)) {
return next();
}
return middleware.forwardToExpressStatic(req, res, next);
};
},
// to allow unit testing
forwardToExpressStatic: function (req, res, next) {
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) {
var activeTheme = response.settings[0];
express['static'](path.join(config.paths.themePath, activeTheme.value), {maxAge: utils.ONE_YEAR_MS})(req, res, next);
});
},
// ### Spam prevention Middleware
// limit signin requests to ten failed requests per IP per hour
spamSigninPrevention: function (req, res, next) {
var currentTime = process.hrtime()[0],
remoteAddress = req.connection.remoteAddress,
deniedRateLimit = '',
ipCount = '',
message = 'Too many attempts.',
rateSigninPeriod = config.rateSigninPeriod || 3600,
rateSigninAttempts = config.rateSigninAttempts || 10;
if (req.body.username && req.body.grant_type === 'password') {
loginSecurity.push({ip: remoteAddress, time: currentTime, email: req.body.username});
} else if (req.body.grant_type === 'refresh_token') {
return next();
} else {
return next(new errors.BadRequestError('No username.'));
}
// filter entries that are older than rateSigninPeriod
loginSecurity = _.filter(loginSecurity, function (logTime) {
return (logTime.time + rateSigninPeriod > currentTime);
});
// check number of tries per IP address
ipCount = _.chain(loginSecurity).countBy('ip').value();
deniedRateLimit = (ipCount[remoteAddress] > rateSigninAttempts);
if (deniedRateLimit) {
errors.logError(
'Only ' + rateSigninAttempts + ' tries per IP address every ' + rateSigninPeriod + ' seconds.',
'Too many login attempts.'
);
message += rateSigninPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later';
return next(new errors.UnauthorizedError(message));
}
next();
},
// ### Spam prevention Middleware
// limit forgotten password requests to five requests per IP per hour for different email addresses
// limit forgotten password requests to five requests per email address
spamForgottenPrevention: function (req, res, next) {
var currentTime = process.hrtime()[0],
remoteAddress = req.connection.remoteAddress,
rateForgottenPeriod = config.rateForgottenPeriod || 3600,
rateForgottenAttempts = config.rateForgottenAttempts || 5,
email = req.body.passwordreset[0].email,
ipCount = '',
deniedRateLimit = '',
deniedEmailRateLimit = '',
message = 'Too many attempts.',
index = _.findIndex(forgottenSecurity, function (logTime) {
return (logTime.ip === remoteAddress && logTime.email === email);
});
if (email) {
if (index !== -1) {
forgottenSecurity[index].count = forgottenSecurity[index].count + 1;
} else {
forgottenSecurity.push({ip: remoteAddress, time: currentTime, email: email, count: 0});
}
} else {
return next(new errors.BadRequestError('No email.'));
}
// filter entries that are older than rateForgottenPeriod
forgottenSecurity = _.filter(forgottenSecurity, function (logTime) {
return (logTime.time + rateForgottenPeriod > currentTime);
});
// check number of tries with different email addresses per IP
ipCount = _.chain(forgottenSecurity).countBy('ip').value();
deniedRateLimit = (ipCount[remoteAddress] > rateForgottenAttempts);
if (index !== -1) {
deniedEmailRateLimit = (forgottenSecurity[index].count > rateForgottenAttempts);
}
if (deniedEmailRateLimit) {
errors.logError(
'Only ' + rateForgottenAttempts + ' forgotten password attempts per email every ' +
rateForgottenPeriod + ' seconds.',
'Forgotten password reset attempt failed'
);
}
if (deniedRateLimit) {
errors.logError(
'Only ' + rateForgottenAttempts + ' tries per IP address every ' + rateForgottenPeriod + ' seconds.',
'Forgotten password reset attempt failed'
);
}
if (deniedEmailRateLimit || deniedRateLimit) {
message += rateForgottenPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later';
return next(new errors.UnauthorizedError(message));
}
next();
},
resetSpamCounter: function (email) {
loginSecurity = _.filter(loginSecurity, function (logTime) {
return (logTime.email !== email);
});
},
// work around to handle missing client_secret
// oauth2orize needs it, but untrusted clients don't have it
addClientSecret: function (req, res, next) {
if (!req.body.client_secret) {
req.body.client_secret = 'not_available';
}
next();
},
// ### Authenticate Client Middleware
// authenticate client that is asking for an access token
authenticateClient: function (req, res, next) {
return passport.authenticate(['oauth2-client-password'], {session: false})(req, res, next);
},
// ### Generate access token Middleware
// register the oauth2orize middleware for password and refresh token grants
generateAccessToken: function (req, res, next) {
return oauthServer.token()(req, res, next);
},
busboy: busboy
};
module.exports = middleware;
module.exports.cacheBlogApp = cacheBlogApp;
module.exports.cacheOauthServer = cacheOauthServer;