-
Notifications
You must be signed in to change notification settings - Fork 303
/
create-app.js
352 lines (306 loc) · 11.5 KB
/
create-app.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
module.exports = createApp
const express = require('express')
const session = require('express-session')
const handlebars = require('express-handlebars')
const uuid = require('uuid')
const cors = require('cors')
const LDP = require('./ldp')
const LdpMiddleware = require('./ldp-middleware')
const corsProxy = require('./handlers/cors-proxy')
const authProxy = require('./handlers/auth-proxy')
const SolidHost = require('./models/solid-host')
const AccountManager = require('./models/account-manager')
const AccountTemplate = require('./models/account-template')
const vhost = require('vhost')
const EmailService = require('./services/email-service')
const TokenService = require('./services/token-service')
const capabilityDiscovery = require('./capability-discovery')
const paymentPointerDiscovery = require('./payment-pointer-discovery')
const API = require('./api')
const errorPages = require('./handlers/error-pages')
const config = require('./server-config')
const defaults = require('../config/defaults')
const options = require('./handlers/options')
const debug = require('./debug')
const path = require('path')
const { routeResolvedFile } = require('./utils')
const ResourceMapper = require('./resource-mapper')
const aclCheck = require('@solid/acl-check')
const { version } = require('../package.json')
const corsSettings = cors({
methods: [
'OPTIONS', 'HEAD', 'GET', 'PATCH', 'POST', 'PUT', 'DELETE'
],
exposedHeaders: 'Authorization, User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Accept-Put, Updates-Via, Allow, WAC-Allow, Content-Length, WWW-Authenticate, MS-Author-Via, X-Powered-By',
credentials: true,
maxAge: 1728000,
origin: true,
preflightContinue: true
})
function createApp (argv = {}) {
// Override default configs (defaults) with passed-in params (argv)
argv = Object.assign({}, defaults, argv)
argv.host = SolidHost.from(argv)
argv.resourceMapper = new ResourceMapper({
rootUrl: argv.serverUri,
rootPath: path.resolve(argv.root || process.cwd()),
includeHost: argv.multiuser,
defaultContentType: argv.defaultContentType
})
AccountTemplate.registerHostname(argv.serverUri)
const configPath = config.initConfigDir(argv)
argv.templates = config.initTemplateDirs(configPath)
config.printDebugInfo(argv)
const ldp = new LDP(argv)
const app = express()
initAppLocals(app, argv, ldp)
initHeaders(app)
initViews(app, configPath)
initLoggers()
// Serve the public 'common' directory (for shared CSS files, etc)
app.use('/common', express.static(path.join(__dirname, '../common')))
app.use('/', express.static(path.dirname(require.resolve('mashlib/dist/databrowser.html')), { index: false }))
routeResolvedFile(app, '/common/js/', 'solid-auth-client/dist-lib/solid-auth-client.bundle.js')
routeResolvedFile(app, '/common/js/', 'solid-auth-client/dist-lib/solid-auth-client.bundle.js.map')
app.use('/.well-known', express.static(path.join(__dirname, '../common/well-known')))
// Serve bootstrap from it's node_module directory
routeResolvedFile(app, '/common/css/', 'bootstrap/dist/css/bootstrap.min.css')
routeResolvedFile(app, '/common/css/', 'bootstrap/dist/css/bootstrap.min.css.map')
routeResolvedFile(app, '/common/fonts/', 'bootstrap/dist/fonts/glyphicons-halflings-regular.eot')
routeResolvedFile(app, '/common/fonts/', 'bootstrap/dist/fonts/glyphicons-halflings-regular.svg')
routeResolvedFile(app, '/common/fonts/', 'bootstrap/dist/fonts/glyphicons-halflings-regular.ttf')
routeResolvedFile(app, '/common/fonts/', 'bootstrap/dist/fonts/glyphicons-halflings-regular.woff')
routeResolvedFile(app, '/common/fonts/', 'bootstrap/dist/fonts/glyphicons-halflings-regular.woff2')
// Serve OWASP password checker from it's node_module directory
routeResolvedFile(app, '/common/js/', 'owasp-password-strength-test/owasp-password-strength-test.js')
// Serve the TextEncoder polyfill
routeResolvedFile(app, '/common/js/', 'text-encoder-lite/text-encoder-lite.min.js')
// Add CORS proxy
if (argv.proxy) {
console.warn('The proxy configuration option has been renamed to corsProxy.')
argv.corsProxy = argv.corsProxy || argv.proxy
delete argv.proxy
}
if (argv.corsProxy) {
corsProxy(app, argv.corsProxy)
}
// Options handler
app.options('/*', options)
// Set up API
if (argv.apiApps) {
app.use('/api/apps', express.static(argv.apiApps))
}
// Authenticate the user
if (argv.webid) {
initWebId(argv, app, ldp)
}
// Add Auth proxy (requires authentication)
if (argv.authProxy) {
authProxy(app, argv.authProxy)
}
// Attach the LDP middleware
app.use('/', LdpMiddleware(corsSettings))
// https://stackoverflow.com/questions/51741383/nodejs-express-return-405-for-un-supported-method
app.use(function (req, res, next) {
const AllLayers = app._router.stack
const Layers = AllLayers.filter(x => x.name === 'bound dispatch' && x.regexp.test(req.path))
const Methods = []
Layers.forEach(layer => {
for (const method in layer.route.methods) {
if (layer.route.methods[method] === true) {
Methods.push(method.toUpperCase())
}
}
})
if (Layers.length !== 0 && !Methods.includes(req.method)) {
// res.setHeader('Allow', Methods.join(','))
if (req.method === 'OPTIONS') {
return res.send(Methods.join(', '))
} else {
return res.status(405).send()
}
} else {
next()
}
})
// Errors
app.use(errorPages.handler)
return app
}
/**
* Initializes `app.locals` parameters for downstream use (typically by route
* handlers).
*
* @param app {Function} Express.js app instance
* @param argv {Object} Config options hashmap
* @param ldp {LDP}
*/
function initAppLocals (app, argv, ldp) {
app.locals.ldp = ldp
app.locals.appUrls = argv.apps // used for service capability discovery
app.locals.host = argv.host
app.locals.authMethod = argv.auth
app.locals.localAuth = argv.localAuth
app.locals.tokenService = new TokenService()
app.locals.enforceToc = argv.enforceToc
app.locals.tocUri = argv.tocUri
app.locals.disablePasswordChecks = argv.disablePasswordChecks
if (argv.email && argv.email.host) {
app.locals.emailService = new EmailService(argv.templates.email, argv.email)
}
}
/**
* Sets up headers common to all Solid requests (CORS-related, Allow, etc).
*
* @param app {Function} Express.js app instance
*/
function initHeaders (app) {
app.use(corsSettings)
app.use((req, res, next) => {
res.set('X-Powered-By', 'solid-server/' + version)
// Cors lib adds Vary: Origin automatically, but inreliably
res.set('Vary', 'Accept, Authorization, Origin')
// Set default Allow methods
res.set('Allow', 'OPTIONS, HEAD, GET, PATCH, POST, PUT, DELETE')
next()
})
app.use('/', capabilityDiscovery())
app.use('/', paymentPointerDiscovery())
}
/**
* Sets up the express rendering engine and views directory.
*
* @param app {Function} Express.js app
* @param configPath {string}
*/
function initViews (app, configPath) {
const viewsPath = config.initDefaultViews(configPath)
app.set('views', viewsPath)
app.engine('.hbs', handlebars({
extname: '.hbs',
partialsDir: viewsPath,
defaultLayout: null
}))
app.set('view engine', '.hbs')
}
/**
* Sets up WebID-related functionality (account creation and authentication)
*
* @param argv {Object}
* @param app {Function}
* @param ldp {LDP}
*/
function initWebId (argv, app, ldp) {
config.ensureWelcomePage(argv)
// Store the user's session key in a cookie
// (for same-domain browsing by people only)
const useSecureCookies = !!argv.sslKey // use secure cookies when over HTTPS
const sessionHandler = session(sessionSettings(useSecureCookies, argv.host))
app.use(sessionHandler)
// Reject cookies from third-party applications.
// Otherwise, when a user is logged in to their Solid server,
// any third-party application could perform authenticated requests
// without permission by including the credentials set by the Solid server.
app.use((req, res, next) => {
const origin = req.get('origin')
const trustedOrigins = ldp.getTrustedOrigins(req)
const userId = req.session.userId
// Exception: allow logout requests from all third-party apps
// such that OIDC client can log out via cookie auth
// TODO: remove this exception when OIDC clients
// use Bearer token to authenticate instead of cookie
// (https://github.com/solid/node-solid-server/pull/835#issuecomment-426429003)
//
// Authentication cookies are an optimization:
// instead of going through the process of
// fully validating authentication on every request,
// we go through this process once,
// and store its successful result in a cookie
// that will be reused upon the next request.
// However, that cookie can then be sent by any server,
// even servers that have not gone through the proper authentication mechanism.
// However, if trusted origins are enabled,
// then any origin is allowed to take the shortcut route,
// since malicious origins will be banned at the ACL checking phase.
// https://github.com/solid/node-solid-server/issues/1117
if (!argv.strictOrigin && !argv.host.allowsSessionFor(userId, origin, trustedOrigins) && !isLogoutRequest(req)) {
debug.authentication(`Rejecting session for ${userId} from ${origin}`)
// Destroy session data
delete req.session.userId
// Ensure this modified session is not saved
req.session.save = (done) => done()
}
if (isLogoutRequest(req)) {
delete req.session.userId
}
next()
})
const accountManager = AccountManager.from({
authMethod: argv.auth,
emailService: app.locals.emailService,
tokenService: app.locals.tokenService,
host: argv.host,
accountTemplatePath: argv.templates.account,
store: ldp,
multiuser: argv.multiuser
})
app.locals.accountManager = accountManager
// Account Management API (create account, new cert)
app.use('/', API.accounts.middleware(accountManager))
// Set up authentication-related API endpoints and app.locals
initAuthentication(app, argv)
if (argv.multiuser) {
app.use(vhost('*', LdpMiddleware(corsSettings)))
}
}
function initLoggers () {
aclCheck.configureLogger(debug.ACL)
}
/**
* Determines whether the given request is a logout request
*/
function isLogoutRequest (req) {
// TODO: this is a hack that hard-codes OIDC paths,
// this code should live in the OIDC module
return req.path === '/logout' || req.path === '/goodbye'
}
/**
* Sets up authentication-related routes and handlers for the app.
*
* @param app {Object} Express.js app instance
* @param argv {Object} Config options hashmap
*/
function initAuthentication (app, argv) {
const auth = argv.forceUser ? 'forceUser' : argv.auth
if (!(auth in API.authn)) {
throw new Error(`Unsupported authentication scheme: ${auth}`)
}
API.authn[auth].initialize(app, argv)
}
/**
* Returns a settings object for Express.js sessions.
*
* @param secureCookies {boolean}
* @param host {SolidHost}
*
* @return {Object} `express-session` settings object
*/
function sessionSettings (secureCookies, host) {
const sessionSettings = {
name: 'nssidp.sid',
secret: uuid.v4(),
saveUninitialized: false,
resave: false,
rolling: true,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}
// Cookies should set to be secure if https is on
if (secureCookies) {
sessionSettings.cookie.secure = true
}
// Determine the cookie domain
sessionSettings.cookie.domain = host.cookieDomain
return sessionSettings
}