solid-server 5.6.9-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.acl +10 -0
- package/.github/workflows/ci.yml +47 -0
- package/.nvmrc +1 -0
- package/.snyk +35 -0
- package/.well-known/.acl +15 -0
- package/CHANGELOG.md +198 -0
- package/CONTRIBUTING.md +139 -0
- package/CONTRIBUTORS.md +36 -0
- package/Dockerfile +22 -0
- package/LICENSE.md +23 -0
- package/README.md +453 -0
- package/bin/lib/cli-utils.js +85 -0
- package/bin/lib/cli.js +39 -0
- package/bin/lib/init.js +94 -0
- package/bin/lib/invalidUsernames.js +148 -0
- package/bin/lib/migrateLegacyResources.js +69 -0
- package/bin/lib/options.js +399 -0
- package/bin/lib/start.js +148 -0
- package/bin/lib/updateIndex.js +56 -0
- package/bin/solid +3 -0
- package/bin/solid-test +12 -0
- package/bin/solid.js +3 -0
- package/common/css/solid.css +58 -0
- package/common/fonts/glyphicons-halflings-regular.eot +0 -0
- package/common/fonts/glyphicons-halflings-regular.svg +288 -0
- package/common/fonts/glyphicons-halflings-regular.ttf +0 -0
- package/common/fonts/glyphicons-halflings-regular.woff +0 -0
- package/common/fonts/glyphicons-halflings-regular.woff2 +0 -0
- package/common/img/.gitkeep +0 -0
- package/common/js/auth-buttons.js +65 -0
- package/common/js/solid.js +454 -0
- package/common/well-known/security.txt +2 -0
- package/config/defaults.js +25 -0
- package/config/usernames-blacklist.json +4 -0
- package/config.json-default +22 -0
- package/default-templates/emails/delete-account.js +49 -0
- package/default-templates/emails/invalid-username.js +30 -0
- package/default-templates/emails/reset-password.js +49 -0
- package/default-templates/emails/welcome.js +39 -0
- package/default-templates/new-account/.acl +26 -0
- package/default-templates/new-account/.meta +5 -0
- package/default-templates/new-account/.meta.acl +25 -0
- package/default-templates/new-account/.well-known/.acl +19 -0
- package/default-templates/new-account/favicon.ico +0 -0
- package/default-templates/new-account/favicon.ico.acl +26 -0
- package/default-templates/new-account/inbox/.acl +26 -0
- package/default-templates/new-account/private/.acl +10 -0
- package/default-templates/new-account/profile/.acl +19 -0
- package/default-templates/new-account/profile/card$.ttl +25 -0
- package/default-templates/new-account/public/.acl +19 -0
- package/default-templates/new-account/robots.txt +3 -0
- package/default-templates/new-account/robots.txt.acl +26 -0
- package/default-templates/new-account/settings/.acl +20 -0
- package/default-templates/new-account/settings/prefs.ttl +15 -0
- package/default-templates/new-account/settings/privateTypeIndex.ttl +4 -0
- package/default-templates/new-account/settings/publicTypeIndex.ttl +4 -0
- package/default-templates/new-account/settings/publicTypeIndex.ttl.acl +25 -0
- package/default-templates/new-account/settings/serverSide.ttl.acl +13 -0
- package/default-templates/new-account/settings/serverSide.ttl.inactive +12 -0
- package/default-templates/server/.acl +10 -0
- package/default-templates/server/.well-known/.acl +15 -0
- package/default-templates/server/favicon.ico +0 -0
- package/default-templates/server/favicon.ico.acl +15 -0
- package/default-templates/server/index.html +55 -0
- package/default-templates/server/robots.txt +3 -0
- package/default-templates/server/robots.txt.acl +15 -0
- package/default-views/account/account-deleted.hbs +17 -0
- package/default-views/account/delete-confirm.hbs +51 -0
- package/default-views/account/delete-link-sent.hbs +17 -0
- package/default-views/account/delete.hbs +51 -0
- package/default-views/account/invalid-username.hbs +22 -0
- package/default-views/account/register-disabled.hbs +6 -0
- package/default-views/account/register-form.hbs +132 -0
- package/default-views/account/register.hbs +24 -0
- package/default-views/auth/auth-hidden-fields.hbs +8 -0
- package/default-views/auth/change-password.hbs +58 -0
- package/default-views/auth/goodbye.hbs +23 -0
- package/default-views/auth/login-required.hbs +34 -0
- package/default-views/auth/login-tls.hbs +11 -0
- package/default-views/auth/login-username-password.hbs +28 -0
- package/default-views/auth/login.hbs +55 -0
- package/default-views/auth/no-permission.hbs +29 -0
- package/default-views/auth/password-changed.hbs +27 -0
- package/default-views/auth/reset-link-sent.hbs +21 -0
- package/default-views/auth/reset-password.hbs +52 -0
- package/default-views/auth/sharing.hbs +49 -0
- package/default-views/shared/create-account.hbs +8 -0
- package/default-views/shared/error.hbs +5 -0
- package/docs/how-to-delete-your-account.md +56 -0
- package/docs/login-and-grant-access-to-application.md +32 -0
- package/examples/custom-error-handling.js +31 -0
- package/examples/ldp-with-webid.js +12 -0
- package/examples/simple-express-app.js +20 -0
- package/examples/simple-ldp-server.js +8 -0
- package/favicon.ico +0 -0
- package/favicon.ico.acl +15 -0
- package/index.html +48 -0
- package/index.js +3 -0
- package/lib/acl-checker.js +274 -0
- package/lib/api/accounts/user-accounts.js +88 -0
- package/lib/api/authn/force-user.js +21 -0
- package/lib/api/authn/index.js +5 -0
- package/lib/api/authn/webid-oidc.js +202 -0
- package/lib/api/authn/webid-tls.js +69 -0
- package/lib/api/index.js +6 -0
- package/lib/capability-discovery.js +54 -0
- package/lib/common/fs-utils.js +43 -0
- package/lib/common/template-utils.js +50 -0
- package/lib/common/user-utils.js +28 -0
- package/lib/create-app.js +322 -0
- package/lib/create-server.js +107 -0
- package/lib/debug.js +17 -0
- package/lib/handlers/allow.js +82 -0
- package/lib/handlers/auth-proxy.js +63 -0
- package/lib/handlers/copy.js +39 -0
- package/lib/handlers/cors-proxy.js +95 -0
- package/lib/handlers/delete.js +23 -0
- package/lib/handlers/error-pages.js +212 -0
- package/lib/handlers/get.js +219 -0
- package/lib/handlers/index.js +42 -0
- package/lib/handlers/options.js +33 -0
- package/lib/handlers/patch/n3-patch-parser.js +49 -0
- package/lib/handlers/patch/sparql-update-parser.js +16 -0
- package/lib/handlers/patch.js +203 -0
- package/lib/handlers/post.js +99 -0
- package/lib/handlers/put.js +56 -0
- package/lib/handlers/restrict-to-top-domain.js +13 -0
- package/lib/header.js +136 -0
- package/lib/http-error.js +34 -0
- package/lib/ldp-container.js +161 -0
- package/lib/ldp-copy.js +73 -0
- package/lib/ldp-middleware.js +32 -0
- package/lib/ldp.js +620 -0
- package/lib/lock.js +10 -0
- package/lib/metadata.js +10 -0
- package/lib/models/account-manager.js +603 -0
- package/lib/models/account-template.js +152 -0
- package/lib/models/authenticator.js +333 -0
- package/lib/models/oidc-manager.js +53 -0
- package/lib/models/solid-host.js +131 -0
- package/lib/models/user-account.js +112 -0
- package/lib/models/webid-tls-certificate.js +184 -0
- package/lib/payment-pointer-discovery.js +83 -0
- package/lib/requests/add-cert-request.js +138 -0
- package/lib/requests/auth-request.js +234 -0
- package/lib/requests/create-account-request.js +468 -0
- package/lib/requests/delete-account-confirm-request.js +170 -0
- package/lib/requests/delete-account-request.js +144 -0
- package/lib/requests/login-request.js +205 -0
- package/lib/requests/password-change-request.js +201 -0
- package/lib/requests/password-reset-email-request.js +199 -0
- package/lib/requests/sharing-request.js +259 -0
- package/lib/resource-mapper.js +198 -0
- package/lib/server-config.js +167 -0
- package/lib/services/blacklist-service.js +33 -0
- package/lib/services/email-service.js +162 -0
- package/lib/services/token-service.js +47 -0
- package/lib/utils.js +254 -0
- package/lib/webid/index.js +13 -0
- package/lib/webid/lib/get.js +27 -0
- package/lib/webid/lib/parse.js +12 -0
- package/lib/webid/tls/index.js +185 -0
- package/package.json +172 -0
- package/renovate.json +5 -0
- package/robots.txt +3 -0
- package/robots.txt.acl +15 -0
- package/static/account-recovery.html +78 -0
- package/static/popup-redirect.html +1 -0
- package/static/signup.html +108 -0
- package/static/signup.html.acl +14 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const AuthRequest = require('./auth-request')
|
|
4
|
+
const debug = require('./../debug').accounts
|
|
5
|
+
|
|
6
|
+
class PasswordResetEmailRequest extends AuthRequest {
|
|
7
|
+
/**
|
|
8
|
+
* @constructor
|
|
9
|
+
* @param options {Object}
|
|
10
|
+
* @param options.accountManager {AccountManager}
|
|
11
|
+
* @param options.response {ServerResponse} express response object
|
|
12
|
+
* @param [options.returnToUrl] {string}
|
|
13
|
+
* @param [options.username] {string} Username / account name (e.g. 'alice')
|
|
14
|
+
*/
|
|
15
|
+
constructor (options) {
|
|
16
|
+
super(options)
|
|
17
|
+
|
|
18
|
+
this.returnToUrl = options.returnToUrl
|
|
19
|
+
this.username = options.username
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Factory method, returns an initialized instance of PasswordResetEmailRequest
|
|
24
|
+
* from an incoming http request.
|
|
25
|
+
*
|
|
26
|
+
* @param req {IncomingRequest}
|
|
27
|
+
* @param res {ServerResponse}
|
|
28
|
+
*
|
|
29
|
+
* @return {PasswordResetEmailRequest}
|
|
30
|
+
*/
|
|
31
|
+
static fromParams (req, res) {
|
|
32
|
+
const locals = req.app.locals
|
|
33
|
+
const accountManager = locals.accountManager
|
|
34
|
+
|
|
35
|
+
const returnToUrl = this.parseParameter(req, 'returnToUrl')
|
|
36
|
+
const username = this.parseParameter(req, 'username')
|
|
37
|
+
|
|
38
|
+
const options = {
|
|
39
|
+
accountManager,
|
|
40
|
+
returnToUrl,
|
|
41
|
+
username,
|
|
42
|
+
response: res
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new PasswordResetEmailRequest(options)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Handles a Reset Password GET request on behalf of a middleware handler.
|
|
50
|
+
* Usage:
|
|
51
|
+
*
|
|
52
|
+
* ```
|
|
53
|
+
* app.get('/password/reset', PasswordResetEmailRequest.get)
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @param req {IncomingRequest}
|
|
57
|
+
* @param res {ServerResponse}
|
|
58
|
+
*/
|
|
59
|
+
static get (req, res) {
|
|
60
|
+
const request = PasswordResetEmailRequest.fromParams(req, res)
|
|
61
|
+
|
|
62
|
+
request.renderForm()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handles a Reset Password POST request on behalf of a middleware handler.
|
|
67
|
+
* Usage:
|
|
68
|
+
*
|
|
69
|
+
* ```
|
|
70
|
+
* app.get('/password/reset', PasswordResetEmailRequest.get)
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @param req {IncomingRequest}
|
|
74
|
+
* @param res {ServerResponse}
|
|
75
|
+
*/
|
|
76
|
+
static post (req, res) {
|
|
77
|
+
const request = PasswordResetEmailRequest.fromParams(req, res)
|
|
78
|
+
|
|
79
|
+
debug(`User '${request.username}' requested to be sent a password reset email`)
|
|
80
|
+
|
|
81
|
+
return PasswordResetEmailRequest.handlePost(request)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Performs a 'send me a password reset email' request operation, after the
|
|
86
|
+
* user has entered an email into the reset form.
|
|
87
|
+
*
|
|
88
|
+
* @param request {PasswordResetEmailRequest}
|
|
89
|
+
*
|
|
90
|
+
* @return {Promise}
|
|
91
|
+
*/
|
|
92
|
+
static handlePost (request) {
|
|
93
|
+
return Promise.resolve()
|
|
94
|
+
.then(() => request.validate())
|
|
95
|
+
.then(() => request.loadUser())
|
|
96
|
+
.then(userAccount => request.sendResetLink(userAccount))
|
|
97
|
+
.then(() => request.renderSuccess())
|
|
98
|
+
.catch(error => request.error(error))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validates the request parameters, and throws an error if any
|
|
103
|
+
* validation fails.
|
|
104
|
+
*
|
|
105
|
+
* @throws {Error}
|
|
106
|
+
*/
|
|
107
|
+
validate () {
|
|
108
|
+
if (this.accountManager.multiuser && !this.username) {
|
|
109
|
+
throw new Error('Username required')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns a user account instance for the submitted username.
|
|
115
|
+
*
|
|
116
|
+
* @throws {Error} Rejects if user account does not exist for the username
|
|
117
|
+
*
|
|
118
|
+
* @returns {Promise<UserAccount>}
|
|
119
|
+
*/
|
|
120
|
+
loadUser () {
|
|
121
|
+
const username = this.username
|
|
122
|
+
|
|
123
|
+
return this.accountManager.accountExists(username)
|
|
124
|
+
.then(exists => {
|
|
125
|
+
if (!exists) {
|
|
126
|
+
throw new Error('Account not found for that username')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const userData = { username }
|
|
130
|
+
|
|
131
|
+
return this.accountManager.userAccountFrom(userData)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Loads the account recovery email for a given user and sends out a
|
|
137
|
+
* password request email.
|
|
138
|
+
*
|
|
139
|
+
* @param userAccount {UserAccount}
|
|
140
|
+
*
|
|
141
|
+
* @return {Promise}
|
|
142
|
+
*/
|
|
143
|
+
sendResetLink (userAccount) {
|
|
144
|
+
const accountManager = this.accountManager
|
|
145
|
+
|
|
146
|
+
return accountManager.loadAccountRecoveryEmail(userAccount)
|
|
147
|
+
.then(recoveryEmail => {
|
|
148
|
+
userAccount.email = recoveryEmail
|
|
149
|
+
|
|
150
|
+
debug('Sending recovery email to:', recoveryEmail)
|
|
151
|
+
|
|
152
|
+
return accountManager
|
|
153
|
+
.sendPasswordResetEmail(userAccount, this.returnToUrl)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Renders the 'send password reset link' form along with the provided error.
|
|
159
|
+
* Serves as an error handler for this request workflow.
|
|
160
|
+
*
|
|
161
|
+
* @param error {Error}
|
|
162
|
+
*/
|
|
163
|
+
error (error) {
|
|
164
|
+
const res = this.response
|
|
165
|
+
|
|
166
|
+
debug(error)
|
|
167
|
+
|
|
168
|
+
const params = {
|
|
169
|
+
error: error.message,
|
|
170
|
+
returnToUrl: this.returnToUrl,
|
|
171
|
+
multiuser: this.accountManager.multiuser
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
res.status(error.statusCode || 400)
|
|
175
|
+
|
|
176
|
+
res.render('auth/reset-password', params)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Renders the 'send password reset link' form
|
|
181
|
+
*/
|
|
182
|
+
renderForm () {
|
|
183
|
+
const params = {
|
|
184
|
+
returnToUrl: this.returnToUrl,
|
|
185
|
+
multiuser: this.accountManager.multiuser
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.response.render('auth/reset-password', params)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Displays the 'your reset link has been sent' success message view
|
|
193
|
+
*/
|
|
194
|
+
renderSuccess () {
|
|
195
|
+
this.response.render('auth/reset-link-sent')
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = PasswordResetEmailRequest
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/* eslint-disable no-mixed-operators, no-async-promise-executor */
|
|
3
|
+
|
|
4
|
+
const debug = require('./../debug').authentication
|
|
5
|
+
|
|
6
|
+
const AuthRequest = require('./auth-request')
|
|
7
|
+
|
|
8
|
+
const url = require('url')
|
|
9
|
+
const intoStream = require('into-stream')
|
|
10
|
+
|
|
11
|
+
const $rdf = require('rdflib')
|
|
12
|
+
const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Models a local Login request
|
|
16
|
+
*/
|
|
17
|
+
class SharingRequest extends AuthRequest {
|
|
18
|
+
/**
|
|
19
|
+
* @constructor
|
|
20
|
+
* @param options {Object}
|
|
21
|
+
*
|
|
22
|
+
* @param [options.response] {ServerResponse} middleware `res` object
|
|
23
|
+
* @param [options.session] {Session} req.session
|
|
24
|
+
* @param [options.userStore] {UserStore}
|
|
25
|
+
* @param [options.accountManager] {AccountManager}
|
|
26
|
+
* @param [options.returnToUrl] {string}
|
|
27
|
+
* @param [options.authQueryParams] {Object} Key/value hashmap of parsed query
|
|
28
|
+
* parameters that will be passed through to the /authorize endpoint.
|
|
29
|
+
* @param [options.authenticator] {Authenticator} Auth strategy by which to
|
|
30
|
+
* log in
|
|
31
|
+
*/
|
|
32
|
+
constructor (options) {
|
|
33
|
+
super(options)
|
|
34
|
+
|
|
35
|
+
this.authenticator = options.authenticator
|
|
36
|
+
this.authMethod = options.authMethod
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Factory method, returns an initialized instance of LoginRequest
|
|
41
|
+
* from an incoming http request.
|
|
42
|
+
*
|
|
43
|
+
* @param req {IncomingRequest}
|
|
44
|
+
* @param res {ServerResponse}
|
|
45
|
+
* @param authMethod {string}
|
|
46
|
+
*
|
|
47
|
+
* @return {LoginRequest}
|
|
48
|
+
*/
|
|
49
|
+
static fromParams (req, res) {
|
|
50
|
+
const options = AuthRequest.requestOptions(req, res)
|
|
51
|
+
|
|
52
|
+
return new SharingRequest(options)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handles a Login GET request on behalf of a middleware handler, displays
|
|
57
|
+
* the Login page.
|
|
58
|
+
* Usage:
|
|
59
|
+
*
|
|
60
|
+
* ```
|
|
61
|
+
* app.get('/login', LoginRequest.get)
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @param req {IncomingRequest}
|
|
65
|
+
* @param res {ServerResponse}
|
|
66
|
+
*/
|
|
67
|
+
static async get (req, res) {
|
|
68
|
+
const request = SharingRequest.fromParams(req, res)
|
|
69
|
+
|
|
70
|
+
const appUrl = request.getAppUrl()
|
|
71
|
+
const appOrigin = appUrl.origin
|
|
72
|
+
const serverUrl = new url.URL(req.app.locals.ldp.serverUri)
|
|
73
|
+
|
|
74
|
+
// Check if is already registered or is data browser or the webId is not on this machine
|
|
75
|
+
if (request.isUserLoggedIn()) {
|
|
76
|
+
if (
|
|
77
|
+
!request.isSubdomain(serverUrl.host, new url.URL(request.session.subject._id).host) ||
|
|
78
|
+
(appUrl && request.isSubdomain(serverUrl.host, appUrl.host) && appUrl.protocol === serverUrl.protocol) ||
|
|
79
|
+
await request.isAppRegistered(req.app.locals.ldp, appOrigin, request.session.subject._id)
|
|
80
|
+
) {
|
|
81
|
+
request.setUserShared(appOrigin)
|
|
82
|
+
request.redirectPostSharing()
|
|
83
|
+
} else {
|
|
84
|
+
request.renderForm(null, req, appOrigin)
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
request.redirectPostSharing()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Performs the login operation -- loads and validates the
|
|
93
|
+
* appropriate user, inits the session with credentials, and redirects the
|
|
94
|
+
* user to continue their auth flow.
|
|
95
|
+
*
|
|
96
|
+
* @param request {LoginRequest}
|
|
97
|
+
*
|
|
98
|
+
* @return {Promise}
|
|
99
|
+
*/
|
|
100
|
+
static async share (req, res) {
|
|
101
|
+
let accessModes = []
|
|
102
|
+
let consented = false
|
|
103
|
+
if (req.body) {
|
|
104
|
+
accessModes = req.body.access_mode || []
|
|
105
|
+
if (!Array.isArray(accessModes)) {
|
|
106
|
+
accessModes = [accessModes]
|
|
107
|
+
}
|
|
108
|
+
consented = req.body.consent
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const request = SharingRequest.fromParams(req, res)
|
|
112
|
+
|
|
113
|
+
if (request.isUserLoggedIn()) {
|
|
114
|
+
const appUrl = request.getAppUrl()
|
|
115
|
+
const appOrigin = `${appUrl.protocol}//${appUrl.host}`
|
|
116
|
+
debug('Sharing App')
|
|
117
|
+
|
|
118
|
+
if (consented) {
|
|
119
|
+
await request.registerApp(req.app.locals.ldp, appOrigin, accessModes, request.session.subject._id)
|
|
120
|
+
request.setUserShared(appOrigin)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Redirect once that's all done
|
|
124
|
+
request.redirectPostSharing()
|
|
125
|
+
} else {
|
|
126
|
+
request.redirectPostSharing()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isSubdomain (domain, subdomain) {
|
|
131
|
+
const domainArr = domain.split('.')
|
|
132
|
+
const subdomainArr = subdomain.split('.')
|
|
133
|
+
for (let i = 1; i <= domainArr.length; i++) {
|
|
134
|
+
if (subdomainArr[subdomainArr.length - i] !== domainArr[domainArr.length - i]) {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setUserShared (appOrigin) {
|
|
142
|
+
if (!this.session.consentedOrigins) {
|
|
143
|
+
this.session.consentedOrigins = []
|
|
144
|
+
}
|
|
145
|
+
if (!this.session.consentedOrigins.includes(appOrigin)) {
|
|
146
|
+
this.session.consentedOrigins.push(appOrigin)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
isUserLoggedIn () {
|
|
151
|
+
// Ensure the user arrived here by logging in
|
|
152
|
+
return !!(this.session.subject && this.session.subject._id)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getAppUrl () {
|
|
156
|
+
return new url.URL(this.authQueryParams.redirect_uri)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async getProfileGraph (ldp, webId) {
|
|
160
|
+
return await new Promise(async (resolve, reject) => {
|
|
161
|
+
const store = $rdf.graph()
|
|
162
|
+
const profileText = await ldp.readResource(webId)
|
|
163
|
+
$rdf.parse(profileText.toString(), store, this.getWebIdFile(webId), 'text/turtle', (error, kb) => {
|
|
164
|
+
if (error) {
|
|
165
|
+
reject(error)
|
|
166
|
+
} else {
|
|
167
|
+
resolve(kb)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async saveProfileGraph (ldp, store, webId) {
|
|
174
|
+
const text = $rdf.serialize(undefined, store, this.getWebIdFile(webId), 'text/turtle')
|
|
175
|
+
await ldp.put(webId, intoStream(text), 'text/turtle')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getWebIdFile (webId) {
|
|
179
|
+
const webIdurl = new url.URL(webId)
|
|
180
|
+
return `${webIdurl.origin}${webIdurl.pathname}`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async isAppRegistered (ldp, appOrigin, webId) {
|
|
184
|
+
const store = await this.getProfileGraph(ldp, webId)
|
|
185
|
+
return store.each($rdf.sym(webId), ACL('trustedApp')).find((app) => {
|
|
186
|
+
return store.each(app, ACL('origin')).find(rdfAppOrigin => rdfAppOrigin.value === appOrigin)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async registerApp (ldp, appOrigin, accessModes, webId) {
|
|
191
|
+
debug(`Registering app (${appOrigin}) with accessModes ${accessModes} for webId ${webId}`)
|
|
192
|
+
const store = await this.getProfileGraph(ldp, webId)
|
|
193
|
+
const origin = $rdf.sym(appOrigin)
|
|
194
|
+
// remove existing statements on same origin - if it exists
|
|
195
|
+
store.statementsMatching(null, ACL('origin'), origin).forEach(st => {
|
|
196
|
+
store.removeStatements([...store.statementsMatching(null, ACL('trustedApp'), st.subject)])
|
|
197
|
+
store.removeStatements([...store.statementsMatching(st.subject)])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// add new triples
|
|
201
|
+
const application = new $rdf.BlankNode()
|
|
202
|
+
store.add($rdf.sym(webId), ACL('trustedApp'), application, new $rdf.NamedNode(webId))
|
|
203
|
+
store.add(application, ACL('origin'), origin, new $rdf.NamedNode(webId))
|
|
204
|
+
|
|
205
|
+
accessModes.forEach(mode => {
|
|
206
|
+
store.add(application, ACL('mode'), ACL(mode))
|
|
207
|
+
})
|
|
208
|
+
await this.saveProfileGraph(ldp, store, webId)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Returns a URL to redirect the user to after login.
|
|
213
|
+
* Either uses the provided `redirect_uri` auth query param, or simply
|
|
214
|
+
* returns the user profile URI if none was provided.
|
|
215
|
+
*
|
|
216
|
+
* @param validUser {UserAccount}
|
|
217
|
+
*
|
|
218
|
+
* @return {string}
|
|
219
|
+
*/
|
|
220
|
+
postSharingUrl () {
|
|
221
|
+
return this.authorizeUrl()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Redirects the Login request to continue on the OIDC auth workflow.
|
|
226
|
+
*/
|
|
227
|
+
redirectPostSharing () {
|
|
228
|
+
const uri = this.postSharingUrl()
|
|
229
|
+
debug('Login successful, redirecting to ', uri)
|
|
230
|
+
this.response.redirect(uri)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Renders the login form
|
|
235
|
+
*/
|
|
236
|
+
renderForm (error, req, appOrigin) {
|
|
237
|
+
const queryString = req && req.url && req.url.replace(/[^?]+\?/, '') || ''
|
|
238
|
+
const params = Object.assign({}, this.authQueryParams,
|
|
239
|
+
{
|
|
240
|
+
registerUrl: this.registerUrl(),
|
|
241
|
+
returnToUrl: this.returnToUrl,
|
|
242
|
+
enablePassword: this.localAuth.password,
|
|
243
|
+
enableTls: this.localAuth.tls,
|
|
244
|
+
tlsUrl: `/login/tls?${encodeURIComponent(queryString)}`,
|
|
245
|
+
app_origin: appOrigin
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
if (error) {
|
|
249
|
+
params.error = error.message
|
|
250
|
+
this.response.status(error.statusCode)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.response.render('auth/sharing', params)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
SharingRequest
|
|
259
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* eslint-disable node/no-deprecated-api, no-mixed-operators */
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const URL = require('url')
|
|
5
|
+
const { promisify } = require('util')
|
|
6
|
+
const { types, extensions } = require('mime-types')
|
|
7
|
+
const readdir = promisify(fs.readdir)
|
|
8
|
+
const HTTPError = require('./http-error')
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* A ResourceMapper maintains the mapping between HTTP URLs and server filenames,
|
|
12
|
+
* following the principles of the "sweet spot" discussed in
|
|
13
|
+
* https://www.w3.org/DesignIssues/HTTPFilenameMapping.html
|
|
14
|
+
*
|
|
15
|
+
* This class implements this mapping in a single place
|
|
16
|
+
* such that all components use the exact same logic.
|
|
17
|
+
*
|
|
18
|
+
* There are few public methods, and we STRONGLY suggest not to create more.
|
|
19
|
+
* Exposing too much of the internals would likely give other components
|
|
20
|
+
* too much knowledge about the mapping, voiding the purpose of this class.
|
|
21
|
+
*/
|
|
22
|
+
class ResourceMapper {
|
|
23
|
+
constructor ({
|
|
24
|
+
rootUrl,
|
|
25
|
+
rootPath,
|
|
26
|
+
includeHost = false,
|
|
27
|
+
defaultContentType = 'application/octet-stream',
|
|
28
|
+
indexFilename = 'index.html',
|
|
29
|
+
overrideTypes = { acl: 'text/turtle', meta: 'text/turtle' }
|
|
30
|
+
}) {
|
|
31
|
+
this._rootUrl = this._removeTrailingSlash(rootUrl)
|
|
32
|
+
this._rootPath = this._removeTrailingSlash(rootPath).replace(/\\/g, '/')
|
|
33
|
+
this._includeHost = includeHost
|
|
34
|
+
this._readdir = readdir
|
|
35
|
+
this._defaultContentType = defaultContentType
|
|
36
|
+
this._types = { ...types, ...overrideTypes }
|
|
37
|
+
this._indexFilename = indexFilename
|
|
38
|
+
this._indexContentType = this._getContentTypeFromExtension(indexFilename)
|
|
39
|
+
|
|
40
|
+
// If the host needs to be replaced on every call, pre-split the root URL
|
|
41
|
+
if (includeHost) {
|
|
42
|
+
const { protocol, port, pathname } = URL.parse(rootUrl)
|
|
43
|
+
this._protocol = protocol
|
|
44
|
+
this._port = port === null ? '' : `:${port}`
|
|
45
|
+
this._rootUrl = this._removeTrailingSlash(pathname)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Returns the URL of the given HTTP request
|
|
50
|
+
getRequestUrl (req) {
|
|
51
|
+
const { hostname, pathname } = this._parseUrl(req)
|
|
52
|
+
return this.resolveUrl(hostname, pathname)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Returns the URL corresponding to the relative path on the pod
|
|
56
|
+
resolveUrl (hostname, pathname = '') {
|
|
57
|
+
return !this._includeHost
|
|
58
|
+
? `${this._rootUrl}${pathname}`
|
|
59
|
+
: `${this._protocol}//${hostname}${this._port}${this._rootUrl}${pathname}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Returns the file path corresponding to the relative file path on the pod
|
|
63
|
+
resolveFilePath (hostname, filePath = '') {
|
|
64
|
+
return !this._includeHost
|
|
65
|
+
? `${this._rootPath}${filePath}`
|
|
66
|
+
: `${this._rootPath}/${hostname}${filePath}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Maps a given server file to a URL
|
|
70
|
+
async mapFileToUrl ({ path, hostname }) {
|
|
71
|
+
// Remove the root path if specified
|
|
72
|
+
path = path.replace(/\\/g, '/')
|
|
73
|
+
if (path.startsWith(this._rootPath)) {
|
|
74
|
+
path = path.substring(this._rootPath.length)
|
|
75
|
+
}
|
|
76
|
+
if (this._includeHost) {
|
|
77
|
+
if (!path.startsWith(`/${hostname}/`)) {
|
|
78
|
+
throw new Error(`Path must start with hostname (/${hostname})`)
|
|
79
|
+
}
|
|
80
|
+
path = path.substring(hostname.length + 1)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Determine the URL by chopping off everything after the dollar sign
|
|
84
|
+
const pathname = this._removeDollarExtension(path)
|
|
85
|
+
const url = `${this.resolveUrl(hostname)}${
|
|
86
|
+
pathname.split('/').map((component) => encodeURIComponent(component)).join('/')
|
|
87
|
+
}`
|
|
88
|
+
return { url, contentType: this._getContentTypeFromExtension(path) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Maps the request for a given resource and representation format to a server file
|
|
92
|
+
// Will look for an index file if a folder is given and searchIndex is true
|
|
93
|
+
async mapUrlToFile ({ url, contentType, createIfNotExists, searchIndex = true }) {
|
|
94
|
+
// map contentType to mimeType part
|
|
95
|
+
contentType = contentType ? contentType.replace(/\s*;.*/, '') : ''
|
|
96
|
+
// Parse the URL and find the base file path
|
|
97
|
+
const { pathname, hostname } = this._parseUrl(url)
|
|
98
|
+
const filePath = this.resolveFilePath(hostname, decodeURIComponent(pathname))
|
|
99
|
+
if (filePath.indexOf('/..') >= 0) {
|
|
100
|
+
throw new Error('Disallowed /.. segment in URL')
|
|
101
|
+
}
|
|
102
|
+
const isFolder = filePath.endsWith('/')
|
|
103
|
+
const isIndex = searchIndex && filePath.endsWith('/')
|
|
104
|
+
|
|
105
|
+
// Create the path for a new resource
|
|
106
|
+
let path
|
|
107
|
+
if (createIfNotExists) {
|
|
108
|
+
path = filePath
|
|
109
|
+
// Append index filename if needed
|
|
110
|
+
if (isIndex) {
|
|
111
|
+
if (contentType !== this._indexContentType) {
|
|
112
|
+
throw new Error(`Index file needs to have ${this._indexContentType} as content type`)
|
|
113
|
+
}
|
|
114
|
+
path += this._indexFilename
|
|
115
|
+
}
|
|
116
|
+
// If the extension is not correct for the content type, append the correct extension
|
|
117
|
+
if (!isFolder) {
|
|
118
|
+
path = this._addContentTypeExtension(path, contentType)
|
|
119
|
+
}
|
|
120
|
+
// Determine the path of an existing file
|
|
121
|
+
} else {
|
|
122
|
+
// Read all files in the corresponding folder
|
|
123
|
+
const filename = filePath.substr(filePath.lastIndexOf('/') + 1)
|
|
124
|
+
const folder = filePath.substr(0, filePath.length - filename.length)
|
|
125
|
+
|
|
126
|
+
// Find a file with the same name (minus the dollar extension)
|
|
127
|
+
let match = ''
|
|
128
|
+
if (match === '') { // always true to keep indentation
|
|
129
|
+
const files = await this._readdir(folder)
|
|
130
|
+
// Search for files with the same name (disregarding a dollar extension)
|
|
131
|
+
if (!isFolder) {
|
|
132
|
+
match = files.find(f => this._removeDollarExtension(f) === filename)
|
|
133
|
+
// Check if the index file exists
|
|
134
|
+
} else if (searchIndex && files.includes(this._indexFilename)) {
|
|
135
|
+
match = this._indexFilename
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Error if no match was found (unless URL ends with '/', then fall back to the folder)
|
|
139
|
+
if (match === undefined) {
|
|
140
|
+
if (isIndex) {
|
|
141
|
+
match = ''
|
|
142
|
+
} else {
|
|
143
|
+
throw new HTTPError(404, `Resource not found: ${pathname}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
path = `${folder}${match}`
|
|
147
|
+
contentType = this._getContentTypeFromExtension(match)
|
|
148
|
+
}
|
|
149
|
+
return { path, contentType: contentType || this._defaultContentType }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Parses a URL into hostname and pathname
|
|
153
|
+
_parseUrl (url) {
|
|
154
|
+
// URL specified as string
|
|
155
|
+
if (typeof url === 'string') {
|
|
156
|
+
return URL.parse(url)
|
|
157
|
+
}
|
|
158
|
+
// URL specified as Express request object
|
|
159
|
+
if (!url.pathname && url.path) {
|
|
160
|
+
const { hostname, path } = url
|
|
161
|
+
return { hostname, pathname: path.replace(/[?#].*/, '') }
|
|
162
|
+
}
|
|
163
|
+
// URL specified as object
|
|
164
|
+
return url
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Gets the expected content type based on the extension of the path
|
|
168
|
+
_getContentTypeFromExtension (path) {
|
|
169
|
+
const extension = /\.([^/.]+)$/.exec(path)
|
|
170
|
+
return extension && this._types[extension[1].toLowerCase()] || this._defaultContentType
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Appends an extension for the specific content type, if needed
|
|
174
|
+
_addContentTypeExtension (path, contentType) {
|
|
175
|
+
// If we would guess the wrong content type from the extension, try appending a better one
|
|
176
|
+
const contentTypeFromExtension = this._getContentTypeFromExtension(path)
|
|
177
|
+
if (contentTypeFromExtension !== contentType) {
|
|
178
|
+
// Some extensions fit multiple content types, so only switch if there's an improvement
|
|
179
|
+
const newExtension = contentType in extensions ? extensions[contentType][0] : 'unknown'
|
|
180
|
+
if (this._types[newExtension] !== contentTypeFromExtension) {
|
|
181
|
+
path += `$.${newExtension}`
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return path
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Removes possible trailing slashes from a path
|
|
188
|
+
_removeTrailingSlash (path) {
|
|
189
|
+
return path.replace(/\/+$/, '')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Removes dollar extensions from files (index$.html becomes index)
|
|
193
|
+
_removeDollarExtension (path) {
|
|
194
|
+
return path.replace(/\$\.[^$]*$/, '')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = ResourceMapper
|