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.
Files changed (170) hide show
  1. package/.acl +10 -0
  2. package/.github/workflows/ci.yml +47 -0
  3. package/.nvmrc +1 -0
  4. package/.snyk +35 -0
  5. package/.well-known/.acl +15 -0
  6. package/CHANGELOG.md +198 -0
  7. package/CONTRIBUTING.md +139 -0
  8. package/CONTRIBUTORS.md +36 -0
  9. package/Dockerfile +22 -0
  10. package/LICENSE.md +23 -0
  11. package/README.md +453 -0
  12. package/bin/lib/cli-utils.js +85 -0
  13. package/bin/lib/cli.js +39 -0
  14. package/bin/lib/init.js +94 -0
  15. package/bin/lib/invalidUsernames.js +148 -0
  16. package/bin/lib/migrateLegacyResources.js +69 -0
  17. package/bin/lib/options.js +399 -0
  18. package/bin/lib/start.js +148 -0
  19. package/bin/lib/updateIndex.js +56 -0
  20. package/bin/solid +3 -0
  21. package/bin/solid-test +12 -0
  22. package/bin/solid.js +3 -0
  23. package/common/css/solid.css +58 -0
  24. package/common/fonts/glyphicons-halflings-regular.eot +0 -0
  25. package/common/fonts/glyphicons-halflings-regular.svg +288 -0
  26. package/common/fonts/glyphicons-halflings-regular.ttf +0 -0
  27. package/common/fonts/glyphicons-halflings-regular.woff +0 -0
  28. package/common/fonts/glyphicons-halflings-regular.woff2 +0 -0
  29. package/common/img/.gitkeep +0 -0
  30. package/common/js/auth-buttons.js +65 -0
  31. package/common/js/solid.js +454 -0
  32. package/common/well-known/security.txt +2 -0
  33. package/config/defaults.js +25 -0
  34. package/config/usernames-blacklist.json +4 -0
  35. package/config.json-default +22 -0
  36. package/default-templates/emails/delete-account.js +49 -0
  37. package/default-templates/emails/invalid-username.js +30 -0
  38. package/default-templates/emails/reset-password.js +49 -0
  39. package/default-templates/emails/welcome.js +39 -0
  40. package/default-templates/new-account/.acl +26 -0
  41. package/default-templates/new-account/.meta +5 -0
  42. package/default-templates/new-account/.meta.acl +25 -0
  43. package/default-templates/new-account/.well-known/.acl +19 -0
  44. package/default-templates/new-account/favicon.ico +0 -0
  45. package/default-templates/new-account/favicon.ico.acl +26 -0
  46. package/default-templates/new-account/inbox/.acl +26 -0
  47. package/default-templates/new-account/private/.acl +10 -0
  48. package/default-templates/new-account/profile/.acl +19 -0
  49. package/default-templates/new-account/profile/card$.ttl +25 -0
  50. package/default-templates/new-account/public/.acl +19 -0
  51. package/default-templates/new-account/robots.txt +3 -0
  52. package/default-templates/new-account/robots.txt.acl +26 -0
  53. package/default-templates/new-account/settings/.acl +20 -0
  54. package/default-templates/new-account/settings/prefs.ttl +15 -0
  55. package/default-templates/new-account/settings/privateTypeIndex.ttl +4 -0
  56. package/default-templates/new-account/settings/publicTypeIndex.ttl +4 -0
  57. package/default-templates/new-account/settings/publicTypeIndex.ttl.acl +25 -0
  58. package/default-templates/new-account/settings/serverSide.ttl.acl +13 -0
  59. package/default-templates/new-account/settings/serverSide.ttl.inactive +12 -0
  60. package/default-templates/server/.acl +10 -0
  61. package/default-templates/server/.well-known/.acl +15 -0
  62. package/default-templates/server/favicon.ico +0 -0
  63. package/default-templates/server/favicon.ico.acl +15 -0
  64. package/default-templates/server/index.html +55 -0
  65. package/default-templates/server/robots.txt +3 -0
  66. package/default-templates/server/robots.txt.acl +15 -0
  67. package/default-views/account/account-deleted.hbs +17 -0
  68. package/default-views/account/delete-confirm.hbs +51 -0
  69. package/default-views/account/delete-link-sent.hbs +17 -0
  70. package/default-views/account/delete.hbs +51 -0
  71. package/default-views/account/invalid-username.hbs +22 -0
  72. package/default-views/account/register-disabled.hbs +6 -0
  73. package/default-views/account/register-form.hbs +132 -0
  74. package/default-views/account/register.hbs +24 -0
  75. package/default-views/auth/auth-hidden-fields.hbs +8 -0
  76. package/default-views/auth/change-password.hbs +58 -0
  77. package/default-views/auth/goodbye.hbs +23 -0
  78. package/default-views/auth/login-required.hbs +34 -0
  79. package/default-views/auth/login-tls.hbs +11 -0
  80. package/default-views/auth/login-username-password.hbs +28 -0
  81. package/default-views/auth/login.hbs +55 -0
  82. package/default-views/auth/no-permission.hbs +29 -0
  83. package/default-views/auth/password-changed.hbs +27 -0
  84. package/default-views/auth/reset-link-sent.hbs +21 -0
  85. package/default-views/auth/reset-password.hbs +52 -0
  86. package/default-views/auth/sharing.hbs +49 -0
  87. package/default-views/shared/create-account.hbs +8 -0
  88. package/default-views/shared/error.hbs +5 -0
  89. package/docs/how-to-delete-your-account.md +56 -0
  90. package/docs/login-and-grant-access-to-application.md +32 -0
  91. package/examples/custom-error-handling.js +31 -0
  92. package/examples/ldp-with-webid.js +12 -0
  93. package/examples/simple-express-app.js +20 -0
  94. package/examples/simple-ldp-server.js +8 -0
  95. package/favicon.ico +0 -0
  96. package/favicon.ico.acl +15 -0
  97. package/index.html +48 -0
  98. package/index.js +3 -0
  99. package/lib/acl-checker.js +274 -0
  100. package/lib/api/accounts/user-accounts.js +88 -0
  101. package/lib/api/authn/force-user.js +21 -0
  102. package/lib/api/authn/index.js +5 -0
  103. package/lib/api/authn/webid-oidc.js +202 -0
  104. package/lib/api/authn/webid-tls.js +69 -0
  105. package/lib/api/index.js +6 -0
  106. package/lib/capability-discovery.js +54 -0
  107. package/lib/common/fs-utils.js +43 -0
  108. package/lib/common/template-utils.js +50 -0
  109. package/lib/common/user-utils.js +28 -0
  110. package/lib/create-app.js +322 -0
  111. package/lib/create-server.js +107 -0
  112. package/lib/debug.js +17 -0
  113. package/lib/handlers/allow.js +82 -0
  114. package/lib/handlers/auth-proxy.js +63 -0
  115. package/lib/handlers/copy.js +39 -0
  116. package/lib/handlers/cors-proxy.js +95 -0
  117. package/lib/handlers/delete.js +23 -0
  118. package/lib/handlers/error-pages.js +212 -0
  119. package/lib/handlers/get.js +219 -0
  120. package/lib/handlers/index.js +42 -0
  121. package/lib/handlers/options.js +33 -0
  122. package/lib/handlers/patch/n3-patch-parser.js +49 -0
  123. package/lib/handlers/patch/sparql-update-parser.js +16 -0
  124. package/lib/handlers/patch.js +203 -0
  125. package/lib/handlers/post.js +99 -0
  126. package/lib/handlers/put.js +56 -0
  127. package/lib/handlers/restrict-to-top-domain.js +13 -0
  128. package/lib/header.js +136 -0
  129. package/lib/http-error.js +34 -0
  130. package/lib/ldp-container.js +161 -0
  131. package/lib/ldp-copy.js +73 -0
  132. package/lib/ldp-middleware.js +32 -0
  133. package/lib/ldp.js +620 -0
  134. package/lib/lock.js +10 -0
  135. package/lib/metadata.js +10 -0
  136. package/lib/models/account-manager.js +603 -0
  137. package/lib/models/account-template.js +152 -0
  138. package/lib/models/authenticator.js +333 -0
  139. package/lib/models/oidc-manager.js +53 -0
  140. package/lib/models/solid-host.js +131 -0
  141. package/lib/models/user-account.js +112 -0
  142. package/lib/models/webid-tls-certificate.js +184 -0
  143. package/lib/payment-pointer-discovery.js +83 -0
  144. package/lib/requests/add-cert-request.js +138 -0
  145. package/lib/requests/auth-request.js +234 -0
  146. package/lib/requests/create-account-request.js +468 -0
  147. package/lib/requests/delete-account-confirm-request.js +170 -0
  148. package/lib/requests/delete-account-request.js +144 -0
  149. package/lib/requests/login-request.js +205 -0
  150. package/lib/requests/password-change-request.js +201 -0
  151. package/lib/requests/password-reset-email-request.js +199 -0
  152. package/lib/requests/sharing-request.js +259 -0
  153. package/lib/resource-mapper.js +198 -0
  154. package/lib/server-config.js +167 -0
  155. package/lib/services/blacklist-service.js +33 -0
  156. package/lib/services/email-service.js +162 -0
  157. package/lib/services/token-service.js +47 -0
  158. package/lib/utils.js +254 -0
  159. package/lib/webid/index.js +13 -0
  160. package/lib/webid/lib/get.js +27 -0
  161. package/lib/webid/lib/parse.js +12 -0
  162. package/lib/webid/tls/index.js +185 -0
  163. package/package.json +172 -0
  164. package/renovate.json +5 -0
  165. package/robots.txt +3 -0
  166. package/robots.txt.acl +15 -0
  167. package/static/account-recovery.html +78 -0
  168. package/static/popup-redirect.html +1 -0
  169. package/static/signup.html +108 -0
  170. 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