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,203 @@
1
+ // Express handler for LDP PATCH requests
2
+
3
+ module.exports = handler
4
+
5
+ const bodyParser = require('body-parser')
6
+ const fs = require('fs')
7
+ const debug = require('../debug').handlers
8
+ const error = require('../http-error')
9
+ const $rdf = require('rdflib')
10
+ const crypto = require('crypto')
11
+ const { overQuota, getContentType } = require('../utils')
12
+ const withLock = require('../lock')
13
+
14
+ // Patch parsers by request body content type
15
+ const PATCH_PARSERS = {
16
+ 'application/sparql-update': require('./patch/sparql-update-parser.js'),
17
+ 'application/sparql-update-single-match': require('./patch/sparql-update-parser.js'),
18
+ 'text/n3': require('./patch/n3-patch-parser.js')
19
+ }
20
+
21
+ const DEFAULT_FOR_NEW_CONTENT_TYPE = 'text/turtle'
22
+
23
+ // Handles a PATCH request
24
+ async function patchHandler (req, res, next) {
25
+ debug(`PATCH -- ${req.originalUrl}`)
26
+ res.header('MS-Author-Via', 'SPARQL')
27
+ try {
28
+ // Obtain details of the target resource
29
+ const ldp = req.app.locals.ldp
30
+ let path, contentType
31
+ let resourceExists = true
32
+ try {
33
+ // First check if the file already exists
34
+ ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req }))
35
+ } catch (err) {
36
+ // If the file doesn't exist, request one to be created with the default content type
37
+ ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile(
38
+ { url: req, createIfNotExists: true, contentType: DEFAULT_FOR_NEW_CONTENT_TYPE }))
39
+ // check if a folder with same name exists
40
+ await ldp.checkItemName(req)
41
+ resourceExists = false
42
+ }
43
+ const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname })
44
+ const resource = { path, contentType, url }
45
+ debug('PATCH -- Target <%s> (%s)', url, contentType)
46
+
47
+ // Obtain details of the patch document
48
+ const patch = {}
49
+ patch.text = req.body ? req.body.toString() : ''
50
+ patch.uri = `${url}#patch-${hash(patch.text)}`
51
+ patch.contentType = getContentType(req.headers)
52
+ debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType)
53
+ const parsePatch = PATCH_PARSERS[patch.contentType]
54
+ if (!parsePatch) {
55
+ throw error(415, `Unsupported patch content type: ${patch.contentType}`)
56
+ }
57
+
58
+ // Parse the patch document and verify permissions
59
+ const patchObject = await parsePatch(url, patch.uri, patch.text)
60
+ await checkPermission(req, patchObject, resourceExists)
61
+
62
+ // Create the enclosing directory, if necessary
63
+ await ldp.createDirectory(path, req.hostname)
64
+
65
+ // Patch the graph and write it back to the file
66
+ const result = await withLock(path, async () => {
67
+ const graph = await readGraph(resource)
68
+ await applyPatch(patchObject, graph, url)
69
+ return writeGraph(graph, resource, ldp.resourceMapper.resolveFilePath(req.hostname), ldp.serverUri)
70
+ })
71
+
72
+ // Send the result to the client
73
+ res.send(result)
74
+ } catch (err) {
75
+ return next(err)
76
+ }
77
+ return next()
78
+ }
79
+
80
+ // Reads the request body and calls the actual patch handler
81
+ function handler (req, res, next) {
82
+ readEntity(req, res, () => patchHandler(req, res, next))
83
+ }
84
+
85
+ const readEntity = bodyParser.text({ type: () => true })
86
+
87
+ // Reads the RDF graph in the given resource
88
+ function readGraph (resource) {
89
+ // Read the resource's file
90
+ return new Promise((resolve, reject) =>
91
+ fs.readFile(resource.path, { encoding: 'utf8' }, function (err, fileContents) {
92
+ if (err) {
93
+ // If the file does not exist, assume empty contents
94
+ // (it will be created after a successful patch)
95
+ if (err.code === 'ENOENT') {
96
+ fileContents = ''
97
+ // Fail on all other errors
98
+ } else {
99
+ return reject(error(500, `Original file read error: ${err}`))
100
+ }
101
+ }
102
+ debug('PATCH -- Read target file (%d bytes)', fileContents.length)
103
+ resolve(fileContents)
104
+ })
105
+ )
106
+ // Parse the resource's file contents
107
+ .then((fileContents) => {
108
+ const graph = $rdf.graph()
109
+ debug('PATCH -- Reading %s with content type %s', resource.url, resource.contentType)
110
+ try {
111
+ $rdf.parse(fileContents, graph, resource.url, resource.contentType)
112
+ } catch (err) {
113
+ throw error(500, `Patch: Target ${resource.contentType} file syntax error: ${err}`)
114
+ }
115
+ debug('PATCH -- Parsed target file')
116
+ return graph
117
+ })
118
+ }
119
+
120
+ // Verifies whether the user is allowed to perform the patch on the target
121
+ async function checkPermission (request, patchObject, resourceExists) {
122
+ // If no ACL object was passed down, assume permissions are okay.
123
+ if (!request.acl) return Promise.resolve(patchObject)
124
+ // At this point, we already assume append access,
125
+ // as this can be checked upfront before parsing the patch.
126
+ // Now that we know the details of the patch,
127
+ // we might need to perform additional checks.
128
+ let modes = []
129
+ // acl:default Write is required for create
130
+ if (!resourceExists) modes = ['Write']
131
+ const { acl, session: { userId } } = request
132
+ // Read access is required for DELETE and WHERE.
133
+ // If we would allows users without read access,
134
+ // they could use DELETE or WHERE to trigger 200 or 409,
135
+ // and thereby guess the existence of certain triples.
136
+ // DELETE additionally requires write access.
137
+ if (patchObject.delete) {
138
+ // ACTUALLY Read not needed by solid/test-suite only Write
139
+ modes = ['Read', 'Write']
140
+ // checks = [acl.can(userId, 'Read'), acl.can(userId, 'Write')]
141
+ } else if (patchObject.where) {
142
+ modes = modes.concat(['Read'])
143
+ // checks = [acl.can(userId, 'Read')]
144
+ }
145
+ const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode, request.method, resourceExists)))
146
+ const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true)
147
+ if (!allAllowed) {
148
+ // check owner with Control
149
+ const ldp = request.app.locals.ldp
150
+ if (request.path.endsWith('.acl') && userId === await ldp.getOwner(request.hostname)) return Promise.resolve(patchObject)
151
+
152
+ const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode)))
153
+ const error = errors.filter(error => !!error)
154
+ .reduce((prevErr, err) => prevErr.status > err.status ? prevErr : err, { status: 0 })
155
+ return Promise.reject(error)
156
+ }
157
+ return Promise.resolve(patchObject)
158
+ }
159
+
160
+ // Applies the patch to the RDF graph
161
+ function applyPatch (patchObject, graph, url) {
162
+ debug('PATCH -- Applying patch')
163
+ return new Promise((resolve, reject) =>
164
+ graph.applyPatch(patchObject, graph.sym(url), (err) => {
165
+ if (err) {
166
+ const message = err.message || err // returns string at the moment
167
+ debug(`PATCH -- FAILED. Returning 409. Message: '${message}'`)
168
+ return reject(error(409, `The patch could not be applied. ${message}`))
169
+ }
170
+ resolve(graph)
171
+ })
172
+ )
173
+ }
174
+
175
+ // Writes the RDF graph to the given resource
176
+ function writeGraph (graph, resource, root, serverUri) {
177
+ debug('PATCH -- Writing patched file')
178
+ return new Promise((resolve, reject) => {
179
+ const resourceSym = graph.sym(resource.url)
180
+ const serialized = $rdf.serialize(resourceSym, graph, resource.url, resource.contentType)
181
+
182
+ // First check if we are above quota
183
+ overQuota(root, serverUri).then((isOverQuota) => {
184
+ if (isOverQuota) {
185
+ return reject(error(413,
186
+ 'User has exceeded their storage quota'))
187
+ }
188
+
189
+ fs.writeFile(resource.path, serialized, { encoding: 'utf8' }, function (err) {
190
+ if (err) {
191
+ return reject(error(500, `Failed to write file after patch: ${err}`))
192
+ }
193
+ debug('PATCH -- applied successfully')
194
+ resolve('Patch applied successfully.\n')
195
+ })
196
+ }).catch(() => reject(error(500, 'Error finding user quota')))
197
+ })
198
+ }
199
+
200
+ // Creates a hash of the given text
201
+ function hash (text) {
202
+ return crypto.createHash('md5').update(text).digest('hex')
203
+ }
@@ -0,0 +1,99 @@
1
+ module.exports = handler
2
+
3
+ const Busboy = require('busboy')
4
+ const debug = require('debug')('solid:post')
5
+ const path = require('path')
6
+ const header = require('../header')
7
+ const patch = require('./patch')
8
+ const error = require('../http-error')
9
+ const { extensions } = require('mime-types')
10
+ const getContentType = require('../utils').getContentType
11
+
12
+ async function handler (req, res, next) {
13
+ const ldp = req.app.locals.ldp
14
+ const contentType = getContentType(req.headers)
15
+ debug('content-type is ', contentType)
16
+ // Handle SPARQL(-update?) query
17
+ if (contentType === 'application/sparql' ||
18
+ contentType === 'application/sparql-update') {
19
+ debug('switching to sparql query')
20
+ return patch(req, res, next)
21
+ }
22
+
23
+ // Handle container path
24
+ let containerPath = req.path
25
+ if (containerPath[containerPath.length - 1] !== '/') {
26
+ containerPath += '/'
27
+ }
28
+
29
+ // Check if container exists
30
+ let stats
31
+ try {
32
+ const ret = await ldp.exists(req.hostname, containerPath, false)
33
+ if (ret) {
34
+ stats = ret.stream
35
+ }
36
+ } catch (err) {
37
+ return next(error(err, 'Container not valid'))
38
+ }
39
+
40
+ // Check if container is a directory
41
+ if (stats && !stats.isDirectory()) {
42
+ debug('Path is not a container, 405!')
43
+ return next(error(405, 'Requested resource is not a container'))
44
+ }
45
+
46
+ // Dispatch to the right handler
47
+ if (req.is('multipart/form-data')) {
48
+ multi()
49
+ } else {
50
+ one()
51
+ }
52
+
53
+ function multi () {
54
+ debug('receving multiple files')
55
+
56
+ const busboy = new Busboy({ headers: req.headers })
57
+ busboy.on('file', async function (fieldname, file, filename, encoding, mimetype) {
58
+ debug('One file received via multipart: ' + filename)
59
+ const { url: putUrl } = await ldp.resourceMapper.mapFileToUrl(
60
+ { path: ldp.resourceMapper._rootPath + path.join(containerPath, filename), hostname: req.hostname })
61
+ try {
62
+ await ldp.put(putUrl, file, mimetype)
63
+ } catch (err) {
64
+ busboy.emit('error', err)
65
+ }
66
+ })
67
+ busboy.on('error', function (err) {
68
+ debug('Error receiving the file: ' + err.message)
69
+ next(error(500, 'Error receiving the file'))
70
+ })
71
+
72
+ // Handled by backpressure of streams!
73
+ busboy.on('finish', function () {
74
+ debug('Done storing files')
75
+ res.sendStatus(200)
76
+ next()
77
+ })
78
+ req.pipe(busboy)
79
+ }
80
+
81
+ function one () {
82
+ debug('Receving one file')
83
+ const { slug, link, 'content-type': contentType } = req.headers
84
+ const links = header.parseMetadataFromHeader(link)
85
+ const mimeType = contentType ? contentType.replace(/\s*;.*/, '') : ''
86
+ const extension = mimeType in extensions ? `.${extensions[mimeType][0]}` : ''
87
+
88
+ ldp.post(req.hostname, containerPath, req,
89
+ { slug, extension, container: links.isBasicContainer, contentType }).then(
90
+ resourcePath => {
91
+ debug('File stored in ' + resourcePath)
92
+ header.addLinks(res, links)
93
+ res.set('Location', resourcePath)
94
+ res.sendStatus(201)
95
+ next()
96
+ },
97
+ err => next(err))
98
+ }
99
+ }
@@ -0,0 +1,56 @@
1
+ module.exports = handler
2
+
3
+ const bodyParser = require('body-parser')
4
+ const debug = require('debug')('solid:put')
5
+ const getContentType = require('../utils').getContentType
6
+ const HTTPError = require('../http-error')
7
+ const { stringToStream } = require('../utils')
8
+
9
+ async function handler (req, res, next) {
10
+ debug(req.originalUrl)
11
+ res.header('MS-Author-Via', 'SPARQL')
12
+
13
+ const contentType = req.get('content-type')
14
+ if (isAuxiliary(req)) {
15
+ if (contentType === 'text/turtle') {
16
+ return bodyParser.text({ type: () => true })(req, res, () => putAuxiliary(req, res, next))
17
+ } else return next(new HTTPError(415, 'RDF file contains invalid syntax'))
18
+ }
19
+ return putStream(req, res, next)
20
+ }
21
+
22
+ // TODO could be renamed as putResource (it now covers container and non-container)
23
+ async function putStream (req, res, next, stream = req) {
24
+ const ldp = req.app.locals.ldp
25
+ try {
26
+ debug('test ' + req.get('content-type') + getContentType(req.headers))
27
+ await ldp.put(req, stream, getContentType(req.headers))
28
+ debug('succeded putting the file/folder')
29
+ res.sendStatus(201)
30
+ return next()
31
+ } catch (err) {
32
+ debug('error putting the file/folder:' + err.message)
33
+ err.message = 'Can\'t write file/folder: ' + err.message
34
+ return next(err)
35
+ }
36
+ }
37
+
38
+ // needed to avoid breaking access with bad acl
39
+ // or breaking containement triples for meta
40
+ function putAuxiliary (req, res, next) {
41
+ const ldp = req.app.locals.ldp
42
+ const contentType = req.get('content-type')
43
+ const requestUri = ldp.resourceMapper.getRequestUrl(req)
44
+
45
+ if (ldp.isValidRdf(req.body, requestUri, contentType)) {
46
+ const stream = stringToStream(req.body)
47
+ return putStream(req, res, next, stream)
48
+ }
49
+ next(new HTTPError(400, 'RDF file contains invalid syntax'))
50
+ }
51
+
52
+ function isAuxiliary (req) {
53
+ const originalUrlParts = req.originalUrl.split('.')
54
+ const ext = originalUrlParts[originalUrlParts.length - 1]
55
+ return (ext === 'acl' || ext === 'meta')
56
+ }
@@ -0,0 +1,13 @@
1
+ const HTTPError = require('../http-error')
2
+
3
+ module.exports = function (req, res, next) {
4
+ const locals = req.app.locals
5
+ const ldp = locals.ldp
6
+ const serverUri = locals.host.serverUri
7
+ const hostname = ldp.resourceMapper.resolveUrl(req.hostname)
8
+ if (hostname === serverUri) {
9
+ return next()
10
+ }
11
+ const isLoggedIn = !!(req.session && req.session.userId)
12
+ return next(new HTTPError(isLoggedIn ? 403 : 401, 'Not allowed to access top-level APIs on accounts'))
13
+ }
package/lib/header.js ADDED
@@ -0,0 +1,136 @@
1
+ module.exports.addLink = addLink
2
+ module.exports.addLinks = addLinks
3
+ module.exports.parseMetadataFromHeader = parseMetadataFromHeader
4
+ module.exports.linksHandler = linksHandler
5
+ module.exports.addPermissions = addPermissions
6
+
7
+ const li = require('li')
8
+ const path = require('path')
9
+ const metadata = require('./metadata.js')
10
+ const debug = require('./debug.js')
11
+ const utils = require('./utils.js')
12
+ const error = require('./http-error')
13
+
14
+ const MODES = ['Read', 'Write', 'Append', 'Control']
15
+ const PERMISSIONS = MODES.map(m => m.toLowerCase())
16
+
17
+ function addLink (res, value, rel) {
18
+ const oldLink = res.get('Link')
19
+ if (oldLink === undefined) {
20
+ res.set('Link', '<' + value + '>; rel="' + rel + '"')
21
+ } else {
22
+ res.set('Link', oldLink + ', ' + '<' + value + '>; rel="' + rel + '"')
23
+ }
24
+ }
25
+
26
+ function addLinks (res, fileMetadata) {
27
+ if (fileMetadata.isResource) {
28
+ addLink(res, 'http://www.w3.org/ns/ldp#Resource', 'type')
29
+ }
30
+ if (fileMetadata.isSourceResource) {
31
+ addLink(res, 'http://www.w3.org/ns/ldp#RDFSource', 'type')
32
+ }
33
+ if (fileMetadata.isContainer) {
34
+ addLink(res, 'http://www.w3.org/ns/ldp#Container', 'type')
35
+ }
36
+ if (fileMetadata.isBasicContainer) {
37
+ addLink(res, 'http://www.w3.org/ns/ldp#BasicContainer', 'type')
38
+ }
39
+ if (fileMetadata.isDirectContainer) {
40
+ addLink(res, 'http://www.w3.org/ns/ldp#DirectContainer', 'type')
41
+ }
42
+ }
43
+
44
+ async function linksHandler (req, res, next) {
45
+ const ldp = req.app.locals.ldp
46
+ let filename
47
+ try {
48
+ // Hack: createIfNotExists is set to true for PUT or PATCH requests
49
+ // because the file might not exist yet at this point.
50
+ // But it will be created afterwards.
51
+ // This should be improved with the new server architecture.
52
+ ({ path: filename } = await ldp.resourceMapper
53
+ .mapUrlToFile({ url: req, createIfNotExists: req.method === 'PUT' || req.method === 'PATCH' }))
54
+ } catch (e) {
55
+ // Silently ignore errors here
56
+ // Later handlers will error as well, but they will be able to given a more concrete error message (like 400 or 404)
57
+ return next()
58
+ }
59
+
60
+ if (path.extname(filename) === ldp.suffixMeta) {
61
+ debug.metadata('Trying to access metadata file as regular file.')
62
+
63
+ return next(error(404, 'Trying to access metadata file as regular file'))
64
+ }
65
+ const fileMetadata = new metadata.Metadata()
66
+ if (filename.endsWith('/')) {
67
+ fileMetadata.isContainer = true
68
+ fileMetadata.isBasicContainer = true
69
+ } else {
70
+ fileMetadata.isResource = true
71
+ }
72
+ // Add LDP-required Accept-Post header for OPTIONS request to containers
73
+ if (fileMetadata.isContainer && req.method === 'OPTIONS') {
74
+ res.header('Accept-Post', '*/*')
75
+ }
76
+ // Add ACL and Meta Link in header
77
+ addLink(res, utils.pathBasename(req.path) + ldp.suffixAcl, 'acl')
78
+ addLink(res, utils.pathBasename(req.path) + ldp.suffixMeta, 'describedBy')
79
+ // Add other Link headers
80
+ addLinks(res, fileMetadata)
81
+ next()
82
+ }
83
+
84
+ function parseMetadataFromHeader (linkHeader) {
85
+ const fileMetadata = new metadata.Metadata()
86
+ if (linkHeader === undefined) {
87
+ return fileMetadata
88
+ }
89
+ const links = linkHeader.split(',')
90
+ for (const linkIndex in links) {
91
+ const link = links[linkIndex]
92
+ const parsedLinks = li.parse(link)
93
+ for (const rel in parsedLinks) {
94
+ if (rel === 'type') {
95
+ if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#Resource') {
96
+ fileMetadata.isResource = true
97
+ } else if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#RDFSource') {
98
+ fileMetadata.isSourceResource = true
99
+ } else if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#Container') {
100
+ fileMetadata.isContainer = true
101
+ } else if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#BasicContainer') {
102
+ fileMetadata.isBasicContainer = true
103
+ } else if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#DirectContainer') {
104
+ fileMetadata.isDirectContainer = true
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return fileMetadata
110
+ }
111
+
112
+ // Adds a header that describes the user's permissions
113
+ async function addPermissions (req, res, next) {
114
+ const { acl, session } = req
115
+ if (!acl) return next()
116
+
117
+ // Turn permissions for the public and the user into a header
118
+ const ldp = req.app.locals.ldp
119
+ const resource = ldp.resourceMapper.resolveUrl(req.hostname, req.path)
120
+ let [publicPerms, userPerms] = await Promise.all([
121
+ getPermissionsFor(acl, null, req),
122
+ getPermissionsFor(acl, session.userId, req)
123
+ ])
124
+ if (resource.endsWith('.acl') && userPerms === '' && session.userId === await ldp.getOwner(req.hostname)) userPerms = 'control'
125
+ debug.ACL(`Permissions on ${resource} for ${session.userId || '(none)'}: ${userPerms}`)
126
+ debug.ACL(`Permissions on ${resource} for public: ${publicPerms}`)
127
+ res.set('WAC-Allow', `user="${userPerms}",public="${publicPerms}"`)
128
+ next()
129
+ }
130
+
131
+ // Gets the permissions string for the given user and resource
132
+ async function getPermissionsFor (acl, user, req) {
133
+ const accesses = MODES.map(mode => acl.can(user, mode))
134
+ const allowed = await Promise.all(accesses)
135
+ return PERMISSIONS.filter((mode, i) => allowed[i]).join(' ')
136
+ }
@@ -0,0 +1,34 @@
1
+ module.exports = HTTPError
2
+
3
+ function HTTPError (status, message) {
4
+ if (!(this instanceof HTTPError)) {
5
+ return new HTTPError(status, message)
6
+ }
7
+
8
+ // Error.captureStackTrace(this, this.constructor)
9
+ this.name = this.constructor.name
10
+
11
+ // If status is an object it will be of the form:
12
+ // {status: , message: }
13
+ if (typeof status === 'number') {
14
+ this.message = message || 'Error occurred'
15
+ this.status = status
16
+ } else {
17
+ const err = status
18
+ let _status
19
+ let _code
20
+ let _message
21
+ if (err && err.status) {
22
+ _status = err.status
23
+ }
24
+ if (err && err.code) {
25
+ _code = err.code
26
+ }
27
+ if (err && err.message) {
28
+ _message = err.message
29
+ }
30
+ this.message = message || _message
31
+ this.status = _status || _code === 'ENOENT' ? 404 : 500
32
+ }
33
+ }
34
+ require('util').inherits(module.exports, Error)