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
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
|