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
package/lib/ldp.js ADDED
@@ -0,0 +1,620 @@
1
+ /* eslint-disable node/no-deprecated-api */
2
+
3
+ const { join, dirname } = require('path')
4
+ const intoStream = require('into-stream')
5
+ const url = require('url')
6
+ const fs = require('fs')
7
+ const $rdf = require('rdflib')
8
+ const mkdirp = require('fs-extra').mkdirp
9
+ const uuid = require('uuid')
10
+ const debug = require('./debug')
11
+ const error = require('./http-error')
12
+ const stringToStream = require('./utils').stringToStream
13
+ const serialize = require('./utils').serialize
14
+ const overQuota = require('./utils').overQuota
15
+ const getContentType = require('./utils').getContentType
16
+ const extend = require('extend')
17
+ const rimraf = require('rimraf')
18
+ const ldpContainer = require('./ldp-container')
19
+ const parse = require('./utils').parse
20
+ const fetch = require('node-fetch')
21
+ const { promisify } = require('util')
22
+ const URL = require('url')
23
+ const withLock = require('./lock')
24
+ const utilPath = require('path')
25
+ const { clearAclCache } = require('./acl-checker')
26
+
27
+ const RDF_MIME_TYPES = new Set([
28
+ 'text/turtle', // .ttl
29
+ 'text/n3', // .n3
30
+ 'text/html', // RDFa
31
+ 'application/xhtml+xml', // RDFa
32
+ 'application/n3',
33
+ 'application/nquads',
34
+ 'application/n-quads',
35
+ 'application/rdf+xml', // .rdf
36
+ 'application/ld+json', // .jsonld
37
+ 'application/x-turtle'
38
+ ])
39
+
40
+ const suffixAcl = '.acl'
41
+ const suffixMeta = '.meta'
42
+ const AUXILIARY_RESOURCES = [suffixAcl, suffixMeta]
43
+
44
+ class LDP {
45
+ constructor (argv = {}) {
46
+ extend(this, argv)
47
+
48
+ // Suffixes
49
+ if (!this.suffixAcl) {
50
+ this.suffixAcl = suffixAcl
51
+ }
52
+ if (!this.suffixMeta) {
53
+ this.suffixMeta = suffixMeta
54
+ }
55
+
56
+ // Error pages folder
57
+ this.errorPages = null
58
+ if (!this.noErrorPages) {
59
+ this.errorPages = argv.errorPages
60
+ if (!this.errorPages) {
61
+ // TODO: For now disable error pages if errorPages parameter is not explicitly passed
62
+ this.noErrorPages = true
63
+ } else if (!this.errorPages.endsWith('/')) {
64
+ this.errorPages += '/'
65
+ }
66
+ }
67
+
68
+ if (this.skin !== false) {
69
+ this.skin = true
70
+ }
71
+
72
+ if (this.corsProxy && this.corsProxy[0] !== '/') {
73
+ this.corsProxy = '/' + this.corsProxy
74
+ }
75
+
76
+ return this
77
+ }
78
+
79
+ async stat (file) {
80
+ return new Promise((resolve, reject) => {
81
+ fs.stat(file, (err, stats) => {
82
+ if (err) return reject(error(err, "Can't read metadata of " + file))
83
+ resolve(stats)
84
+ })
85
+ })
86
+ }
87
+
88
+ async readResource (url) {
89
+ try {
90
+ const { path } = await this.resourceMapper.mapUrlToFile({ url })
91
+ return await withLock(path, () => promisify(fs.readFile)(path, { encoding: 'utf8' }))
92
+ } catch (err) {
93
+ throw error(err.status, err.message)
94
+ }
95
+ }
96
+
97
+ async readContainerMeta (url) {
98
+ if (url[url.length - 1] !== '/') {
99
+ url += '/'
100
+ }
101
+ return this.readResource(url + this.suffixMeta)
102
+ }
103
+
104
+ async listContainer (container, reqUri, containerData, hostname) {
105
+ const resourceGraph = $rdf.graph()
106
+
107
+ try {
108
+ $rdf.parse(containerData, resourceGraph, reqUri, 'text/turtle')
109
+ } catch (err) {
110
+ debug.handlers('GET -- Error parsing data: ' + err)
111
+ throw error(500, "Can't parse container")
112
+ }
113
+
114
+ try {
115
+ // add container stats
116
+ await ldpContainer.addContainerStats(this, reqUri, container, resourceGraph)
117
+ // read directory
118
+ const files = await ldpContainer.readdir(container)
119
+ // iterate through all the files
120
+ await Promise.all(files.map(async file => {
121
+ const { url: fileUri } = await this.resourceMapper.mapFileToUrl(
122
+ { path: join(container, file), hostname })
123
+ return await ldpContainer.addFile(this, resourceGraph, reqUri, fileUri, container, file)
124
+ }))
125
+ } catch (err) {
126
+ throw error(500, "Can't list container")
127
+ }
128
+
129
+ // TODO 'text/turtle' is fixed, should be contentType instead
130
+ // This forces one more translation turtle -> desired
131
+ try {
132
+ return await serialize(resourceGraph, reqUri, 'text/turtle')
133
+ } catch (err) {
134
+ debug.handlers('GET -- Error serializing container: ' + err)
135
+ throw error(500, "Can't serialize container")
136
+ }
137
+ }
138
+
139
+ async post (hostname, containerPath, stream, { container, slug, extension, contentType }) {
140
+ // POST without content type is forbidden
141
+ if (!contentType) {
142
+ throw error(400,
143
+ 'POSTrequest requires a content-type via the Content-Type header')
144
+ }
145
+
146
+ const ldp = this
147
+ debug.handlers('POST -- On parent: ' + containerPath)
148
+ // prepare slug
149
+ if (slug) {
150
+ if (this.isAuxResource(slug, extension)) throw error(403, 'POST is not allowed for auxiliary resources')
151
+ slug = decodeURIComponent(slug)
152
+ if (slug.match(/\/|\||:/)) {
153
+ throw error(400, 'The name of new file POSTed may not contain : | or /')
154
+ }
155
+ }
156
+ // Containers should not receive an extension
157
+ if (container) {
158
+ extension = ''
159
+ }
160
+ // Check for file or folder with same name
161
+ let testName, fileName
162
+ try {
163
+ // check for defaulted slug in NSS POST (slug with extension)
164
+ fileName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
165
+ fileName = container ? fileName : fileName + '/'
166
+ const { url: testUrl } = await this.resourceMapper.mapFileToUrl({ hostname, path: containerPath + fileName })
167
+ const { path: testPath } = await this.resourceMapper.mapUrlToFile({ url: testUrl })
168
+ testName = container ? fs.lstatSync(testPath).isFile() : fs.lstatSync(testPath).isDirectory()
169
+ } catch (err) { testName = false }
170
+
171
+ if (testName) {
172
+ throw error(404, 'Container and resource cannot have the same name in URI')
173
+ }
174
+
175
+ // TODO: possibly package this in ldp.post
176
+ let resourceUrl = await ldp.getAvailableUrl(hostname, containerPath, { slug, extension })
177
+ debug.handlers('POST -- Will create at: ' + resourceUrl)
178
+ let originalUrl = resourceUrl
179
+
180
+ if (container) {
181
+ // Create directory by an LDP PUT to the container's .meta resource
182
+ resourceUrl = `${resourceUrl}${resourceUrl.endsWith('/') ? '' : '/'}` // ${ldp.suffixMeta}`
183
+ if (originalUrl && !originalUrl.endsWith('/')) {
184
+ originalUrl += '/'
185
+ }
186
+ }
187
+ // const { url: putUrl } = await this.resourceMapper.mapFileToUrl({ path: resourceUrl, hostname })
188
+
189
+ await ldp.put(resourceUrl, stream, contentType)
190
+ return URL.parse(originalUrl).path
191
+ }
192
+
193
+ isAuxResource (slug, extension) {
194
+ let test = false
195
+ for (const item in AUXILIARY_RESOURCES) {
196
+ if (slug.endsWith(AUXILIARY_RESOURCES[item]) || extension === AUXILIARY_RESOURCES[item]) { test = true; break }
197
+ }
198
+ return test
199
+ }
200
+
201
+ /**
202
+ * Serializes and writes a graph to the given uri, and returns the original
203
+ * (non-serialized) graph.
204
+ * Usage:
205
+ *
206
+ * ```
207
+ * ldp.putGraph('https://localhost:8443/contacts/resource1.ttl', graph)
208
+ * .then(graph => {
209
+ * // success
210
+ * })
211
+ * ```
212
+ *
213
+ * @param graph {Graph}
214
+ * @param uri {string}
215
+ * @param [contentType] {string}
216
+ *
217
+ * @return {Promise<Graph>}
218
+ */
219
+ async putGraph (graph, uri, contentType) {
220
+ const { path } = url.parse(uri)
221
+ const content = await serialize(graph, uri, contentType)
222
+ const stream = stringToStream(content)
223
+ return await this.put(path, stream, contentType)
224
+ }
225
+
226
+ isValidRdf (body, requestUri, contentType) {
227
+ const resourceGraph = $rdf.graph()
228
+ try {
229
+ $rdf.parse(body, resourceGraph, requestUri, contentType)
230
+ } catch (err) {
231
+ debug.ldp('VALIDATE -- Error parsing data: ' + err)
232
+ return false
233
+ }
234
+ return true
235
+ }
236
+
237
+ async put (url, stream, contentType) {
238
+ const container = (url.url || url).endsWith('/')
239
+
240
+ // PUT without content type is forbidden, unless PUTting container
241
+ if (!contentType && !container) {
242
+ throw error(400,
243
+ 'PUT request requires a content-type via the Content-Type header')
244
+ }
245
+
246
+ // check if a folder or file with same name exists
247
+ await this.checkItemName(url)
248
+
249
+ // First check if we are above quota
250
+ let isOverQuota
251
+ // Someone had a reason to make url actually a req sometimes but not
252
+ // all the time. So now we have to account for that, as done below.
253
+ const hostname = typeof url !== 'string' ? url.hostname : URL.parse(url).hostname
254
+ try {
255
+ isOverQuota = await overQuota(this.resourceMapper.resolveFilePath(hostname), this.serverUri)
256
+ } catch (err) {
257
+ throw error(500, 'Error finding user quota')
258
+ }
259
+ if (isOverQuota) {
260
+ throw error(413, 'User has exceeded their storage quota')
261
+ }
262
+
263
+ // Set url using folder/.meta. This is Hack to find folder path
264
+ if (container) {
265
+ if (typeof url !== 'string') {
266
+ url.url = url.url + suffixMeta
267
+ } else {
268
+ url = url + suffixMeta
269
+ }
270
+ contentType = 'text/turtle'
271
+ }
272
+
273
+ const { path } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true })
274
+ // debug.handlers(container + ' item ' + (url.url || url) + ' ' + contentType + ' ' + path)
275
+ // check if file exists, and in that case that it has the same extension
276
+ if (!container) { await this.checkFileExtension(url, path) }
277
+
278
+ // Create the enclosing directory, if necessary, do not create pubsub if PUT create container
279
+ await this.createDirectory(path, hostname, !container)
280
+
281
+ // clear cache
282
+ if (path.endsWith(this.suffixAcl)) {
283
+ const { url: aclUrl } = await this.resourceMapper.mapFileToUrl({ path, hostname })
284
+ clearAclCache(aclUrl)
285
+ // clearAclCache()
286
+ }
287
+
288
+ // Directory created, now write the file
289
+ return withLock(path, () => new Promise((resolve, reject) => {
290
+ // HACK: the middleware in webid-oidc.js uses body-parser, thus ending the stream of data
291
+ // for JSON bodies. So, the stream needs to be reset
292
+ if (contentType.includes('application/json')) {
293
+ stream = intoStream(JSON.stringify(stream.body))
294
+ }
295
+ const file = stream.pipe(fs.createWriteStream(path))
296
+ file.on('error', function () {
297
+ reject(error(500, 'Error writing data'))
298
+ })
299
+ file.on('finish', function () {
300
+ debug.handlers('PUT -- Wrote data to: ' + path)
301
+ resolve()
302
+ })
303
+ }))
304
+ }
305
+
306
+ /**
307
+ * Create directory if not exists
308
+ * Add pubsub if creating intermediate directory to a non-container
309
+ *
310
+ * @param {*} path
311
+ * @param {*} hostname
312
+ * @param {*} nonContainer
313
+ */
314
+ async createDirectory (path, hostname, nonContainer = true) {
315
+ try {
316
+ const dirName = dirname(path)
317
+ if (!fs.existsSync(dirName)) {
318
+ await promisify(mkdirp)(dirName)
319
+ if (this.live && nonContainer) {
320
+ // Get parent for the directory made
321
+ const parentDirectoryPath = utilPath.dirname(dirName) + utilPath.sep
322
+
323
+ // Get the url for the parent
324
+ const parentDirectoryUrl = (await this.resourceMapper.mapFileToUrl({
325
+ path: parentDirectoryPath,
326
+ hostname
327
+ })).url
328
+ // Update websockets
329
+ this.live(URL.parse(parentDirectoryUrl).pathname)
330
+ }
331
+ }
332
+ } catch (err) {
333
+ debug.handlers('PUT -- Error creating directory: ' + err)
334
+ throw error(err,
335
+ 'Failed to create the path to the new resource')
336
+ }
337
+ }
338
+
339
+ async checkFileExtension (url, path) {
340
+ try {
341
+ const { path: existingPath } = await this.resourceMapper.mapUrlToFile({ url })
342
+ if (path !== existingPath) {
343
+ try {
344
+ await withLock(existingPath, () => promisify(fs.unlink)(existingPath))
345
+ } catch (err) { throw error(err, 'Failed to delete resource') }
346
+ }
347
+ } catch (err) { }
348
+ }
349
+
350
+ async checkItemName (url) {
351
+ let testName
352
+ const itemUrl = (url.url || url)
353
+ const container = itemUrl.endsWith('/')
354
+ try {
355
+ const testUrl = container ? itemUrl.slice(0, -1) : itemUrl + '/'
356
+ const { path: testPath } = await this.resourceMapper.mapUrlToFile({ url: testUrl })
357
+ // testName = fs.lstatSync(testPath).isDirectory()
358
+ testName = container ? fs.lstatSync(testPath).isFile() : fs.lstatSync(testPath).isDirectory()
359
+ } catch (err) { testName = false }
360
+
361
+ if (testName) {
362
+ throw error(200, 'Container and resource cannot have the same name in URI')
363
+ }
364
+ }
365
+
366
+ async exists (hostname, path, searchIndex = true) {
367
+ const options = { hostname, path, includeBody: false, searchIndex }
368
+ return await this.get(options, searchIndex)
369
+ }
370
+
371
+ /**
372
+ * Remotely loads the graph at a given uri, parses it and and returns it.
373
+ * Usage:
374
+ *
375
+ * ```
376
+ * ldp.fetchGraph('https://example.com/contacts/card1.ttl')
377
+ * .then(graph => {
378
+ * // const matches = graph.match(...)
379
+ * })
380
+ * ```
381
+ *
382
+ * @param uri {string} Fully qualified uri of the request.
383
+ *
384
+ * @param [options] {object} Options hashmap, passed through to fetchGraph
385
+ *
386
+ * @return {Promise<Graph>}
387
+ */
388
+ async fetchGraph (uri, options = {}) {
389
+ const response = await fetch(uri)
390
+ if (!response.ok) {
391
+ const error = new Error(
392
+ `Error fetching ${uri}: ${response.status} ${response.statusText}`
393
+ )
394
+ error.statusCode = response.status || 400
395
+ throw error
396
+ }
397
+ const body = await response.text()
398
+
399
+ return parse(body, uri, getContentType(response.headers))
400
+ }
401
+
402
+ /**
403
+ * Loads from fs the graph at a given uri, parses it and and returns it.
404
+ * Usage:
405
+ *
406
+ * ```
407
+ * ldp.getGraph('https://localhost:8443/contacts/card1.ttl')
408
+ * .then(graph => {
409
+ * // let matches = graph.match(...)
410
+ * })
411
+ * ```
412
+ *
413
+ * @param uri {string} Fully qualified uri of the request.
414
+ * Note that the protocol part is needed, to provide a base URI to pass on
415
+ * to the graph parser.
416
+ * @param [contentType] {string}
417
+ *
418
+ * @return {Promise<Graph>}
419
+ */
420
+ getGraph (uri, contentType) {
421
+ return this.graph(uri, uri, contentType)
422
+ }
423
+
424
+ async graph (url, baseUri, contentType) {
425
+ const body = await this.readResource(url)
426
+ if (!contentType) {
427
+ ({ contentType } = await this.resourceMapper.mapUrlToFile({ url }))
428
+ }
429
+ return new Promise((resolve, reject) => {
430
+ const graph = $rdf.graph()
431
+ $rdf.parse(body, graph, baseUri, contentType,
432
+ err => err ? reject(err) : resolve(graph))
433
+ })
434
+ }
435
+
436
+ // this is a hack to replace solid:owner, using solid:account in /.meta to avoid NSS migration
437
+ // this /.meta has no functionality in actual NSS
438
+ // comment https://github.com/solid/node-solid-server/pull/1604#discussion_r652903546
439
+ async getOwner (hostname) {
440
+ // const ldp = req.app.locals.ldp
441
+ const rootUrl = this.resourceMapper.resolveUrl(hostname)
442
+ let graph
443
+ try {
444
+ // TODO check for permission ?? Owner is a MUST
445
+ graph = await this.getGraph(rootUrl + '/.meta')
446
+ const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#')
447
+ const owner = await graph.any(null, SOLID('account'), $rdf.sym(rootUrl + '/'))
448
+ return owner.uri
449
+ } catch (error) {
450
+ throw new Error(`Failed to get owner from ${rootUrl}/.meta, got ` + error)
451
+ }
452
+ }
453
+
454
+ async get (options, searchIndex = true) {
455
+ let path, contentType, stats
456
+ try {
457
+ ({ path, contentType } = await this.resourceMapper.mapUrlToFile({ url: options, searchIndex }))
458
+ stats = await this.stat(path)
459
+ } catch (err) {
460
+ throw error(404, 'Can\'t find file requested: ' + options)
461
+ }
462
+
463
+ // Just return, since resource exists
464
+ if (!options.includeBody) {
465
+ return { stream: stats, contentType, container: stats.isDirectory() }
466
+ }
467
+
468
+ // Found a container
469
+ if (stats.isDirectory()) {
470
+ const { url: absContainerUri } = await this.resourceMapper
471
+ .mapFileToUrl({ path, hostname: options.hostname })
472
+ const metaFile = await this.readContainerMeta(absContainerUri)
473
+ .catch(() => '') // Default to an empty meta file if it is missing
474
+ let data
475
+ try {
476
+ data = await this.listContainer(path, absContainerUri, metaFile, options.hostname)
477
+ } catch (err) {
478
+ debug.handlers('GET container -- Read error:' + err.message)
479
+ throw err
480
+ }
481
+ const stream = stringToStream(data)
482
+ // TODO 'text/turtle' is fixed, should be contentType instead
483
+ // This forces one more translation turtle -> desired
484
+ return { stream, contentType: 'text/turtle', container: true }
485
+ } else {
486
+ let chunksize, contentRange, start, end
487
+ if (options.range) {
488
+ const total = fs.statSync(path).size
489
+ const parts = options.range.replace(/bytes=/, '').split('-')
490
+ const partialstart = parts[0]
491
+ const partialend = parts[1]
492
+ start = parseInt(partialstart, 10)
493
+ end = partialend ? parseInt(partialend, 10) : total - 1
494
+ chunksize = (end - start) + 1
495
+ contentRange = 'bytes ' + start + '-' + end + '/' + total
496
+ }
497
+ return withLock(path, () => new Promise((resolve, reject) => {
498
+ const stream = fs.createReadStream(path, start && end ? { start, end } : {})
499
+ stream
500
+ .on('error', function (err) {
501
+ debug.handlers(`GET -- error reading ${path}: ${err.message}`)
502
+ return reject(error(err, "Can't read file " + err))
503
+ })
504
+ .on('open', function () {
505
+ debug.handlers(`GET -- Reading ${path}`)
506
+ return resolve({ stream, contentType, container: false, contentRange, chunksize })
507
+ })
508
+ }))
509
+ }
510
+ }
511
+
512
+ async delete (url) {
513
+ // First check if the path points to a valid file
514
+ let path, stats
515
+ try {
516
+ ({ path } = await this.resourceMapper.mapUrlToFile({ url }))
517
+ stats = await this.stat(path)
518
+ } catch (err) {
519
+ throw error(404, "Can't find " + err)
520
+ }
521
+
522
+ // delete aclCache
523
+ let aclUrl = typeof url !== 'string' ? this.resourceMapper.getRequestUrl(url) : url
524
+ aclUrl = aclUrl.endsWith(this.suffixAcl) ? aclUrl : aclUrl + this.suffixAcl
525
+ debug.handlers('DELETE ACL CACHE ' + aclUrl)
526
+ clearAclCache(aclUrl)
527
+
528
+ // If so, delete the directory or file
529
+ if (stats.isDirectory()) {
530
+ // DELETE method not allowed on podRoot
531
+ if ((url.url || url) === '/') {
532
+ throw error(405, 'DELETE of PodRoot is not allowed')
533
+ }
534
+ return this.deleteContainer(path)
535
+ } else {
536
+ // DELETE method not allowed on podRoot/.acl
537
+ if ((url.url || url) === '/' + this.suffixAcl) {
538
+ throw error(405, 'DELETE of PodRoot/.acl is not allowed')
539
+ }
540
+ return this.deleteDocument(path)
541
+ }
542
+ }
543
+
544
+ async deleteContainer (directory) {
545
+ if (directory[directory.length - 1] !== '/') {
546
+ directory += '/'
547
+ }
548
+
549
+ // Ensure the container exists
550
+ let list
551
+ try {
552
+ list = await promisify(fs.readdir)(directory)
553
+ } catch (err) {
554
+ throw error(404, 'The container does not exist')
555
+ }
556
+
557
+ // Ensure the container is empty (we ignore .meta and .acl)
558
+ if (list.some(file => !file.endsWith(this.suffixMeta) && !file.endsWith(this.suffixAcl))) {
559
+ throw error(409, 'Container is not empty')
560
+ }
561
+
562
+ // Delete the directory recursively
563
+ try {
564
+ await promisify(rimraf)(directory)
565
+ } catch (err) {
566
+ throw error(err, 'Failed to delete the container')
567
+ }
568
+ }
569
+
570
+ // delete document (resource with acl and meta links)
571
+ async deleteDocument (filePath) {
572
+ const linkPath = this.resourceMapper._removeDollarExtension(filePath)
573
+ try {
574
+ // first delete file, then links with write permission only (atomic delete)
575
+ await withLock(filePath, () => promisify(fs.unlink)(filePath))
576
+
577
+ const aclPath = `${linkPath}${this.suffixAcl}`
578
+ if (await promisify(fs.exists)(aclPath)) {
579
+ await withLock(aclPath, () => promisify(fs.unlink)(aclPath))
580
+ }
581
+ const metaPath = `${linkPath}${this.suffixMeta}`
582
+ if (await promisify(fs.exists)(metaPath)) {
583
+ await withLock(metaPath, () => promisify(fs.unlink)(metaPath))
584
+ }
585
+ } catch (err) {
586
+ debug.container('DELETE -- unlink() error: ' + err)
587
+ throw error(err, 'Failed to delete resource')
588
+ }
589
+ }
590
+
591
+ async getAvailableUrl (hostname, containerURI, { slug = uuid.v1(), extension }) {
592
+ let requestUrl = this.resourceMapper.resolveUrl(hostname, containerURI)
593
+ requestUrl = requestUrl.replace(/\/*$/, '/')
594
+
595
+ const { path: containerFilePath } = await this.resourceMapper.mapUrlToFile({ url: requestUrl })
596
+ let fileName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
597
+ if (await promisify(fs.exists)(utilPath.join(containerFilePath, fileName))) {
598
+ fileName = `${uuid.v1()}-${fileName}`
599
+ }
600
+
601
+ return requestUrl + fileName
602
+ }
603
+
604
+ getTrustedOrigins (req) {
605
+ const trustedOrigins = [this.resourceMapper.resolveUrl(req.hostname)].concat(this.trustedOrigins)
606
+ if (this.multiuser) {
607
+ trustedOrigins.push(this.serverUri)
608
+ }
609
+ return trustedOrigins
610
+ }
611
+
612
+ static mimeTypeIsRdf (mimeType) {
613
+ return RDF_MIME_TYPES.has(mimeType)
614
+ }
615
+
616
+ static mimeTypesAsArray () {
617
+ return Array.from(RDF_MIME_TYPES)
618
+ }
619
+ }
620
+ module.exports = LDP
package/lib/lock.js ADDED
@@ -0,0 +1,10 @@
1
+ const AsyncLock = require('async-lock')
2
+
3
+ const lock = new AsyncLock({ timeout: 30 * 1000 })
4
+
5
+ // Obtains a lock on the path, and maintains it until the task finishes
6
+ async function withLock (path, executeTask) {
7
+ return await lock.acquire(path, executeTask)
8
+ }
9
+
10
+ module.exports = withLock
@@ -0,0 +1,10 @@
1
+ exports.Metadata = Metadata
2
+
3
+ function Metadata () {
4
+ this.filename = ''
5
+ this.isResource = false
6
+ this.isSourceResource = false
7
+ this.isContainer = false
8
+ this.isBasicContainer = false
9
+ this.isDirectContainer = false
10
+ }