solid-server 5.7.9-alpha → 5.7.10
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/.github/workflows/ci.yml +1 -1
- package/.nvmrc +1 -1
- package/README.md +9 -0
- package/default-views/auth/reset-link-sent.hbs +1 -1
- package/lib/acl-checker.js +99 -38
- package/lib/create-app.js +28 -1
- package/lib/handlers/allow.js +2 -2
- package/lib/handlers/get.js +10 -4
- package/lib/handlers/options.js +5 -3
- package/lib/handlers/patch.js +4 -2
- package/lib/handlers/put.js +9 -2
- package/lib/ldp.js +37 -15
- package/lib/models/account-manager.js +1 -1
- package/lib/models/authenticator.js +6 -2
- package/lib/requests/create-account-request.js +1 -1
- package/lib/requests/password-reset-email-request.js +6 -3
- package/lib/requests/sharing-request.js +3 -1
- package/lib/resource-mapper.js +5 -2
- package/package.json +13 -12
package/.github/workflows/ci.yml
CHANGED
package/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
v18.19.0
|
package/README.md
CHANGED
|
@@ -69,6 +69,15 @@ $ solid start --root path/to/folder --port 8443 --ssl-key path/to/ssl-key.pem --
|
|
|
69
69
|
# Solid server (solid v0.2.24) running on https://localhost:8443/
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
By default, `solid` runs in `debug all` mode. To stop the debug logs, use `-q`, the quiet parameter.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
$ DEBUG="solid:*" solid start -q
|
|
76
|
+
# use quiet mode and set debug to all
|
|
77
|
+
# DEBUG="solid:ACL" logs only debug.ACL's
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
72
81
|
### Running in development environments
|
|
73
82
|
|
|
74
83
|
Solid requires SSL certificates to be valid, so you cannot use self-signed certificates. To switch off this security feature in development environments, you can use the `bin/solid-test` executable, which unsets the `NODE_TLS_REJECT_UNAUTHORIZED` flag and sets the `rejectUnauthorized` option.
|
package/lib/acl-checker.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const { dirname } = require('path')
|
|
5
5
|
const rdf = require('rdflib')
|
|
6
6
|
const debug = require('./debug').ACL
|
|
7
|
-
const debugCache = require('./debug').cache
|
|
7
|
+
// const debugCache = require('./debug').cache
|
|
8
8
|
const HTTPError = require('./http-error')
|
|
9
9
|
const aclCheck = require('@solid/acl-check')
|
|
10
10
|
const { URL } = require('url')
|
|
@@ -55,7 +55,8 @@ class ACLChecker {
|
|
|
55
55
|
}
|
|
56
56
|
this.messagesCached[cacheKey] = this.messagesCached[cacheKey] || []
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// for method DELETE nearestACL and ACL from parent resource
|
|
59
|
+
const acl = await this.getNearestACL(method).catch(err => {
|
|
59
60
|
this.messagesCached[cacheKey].push(new HTTPError(err.status || 500, err.message || err))
|
|
60
61
|
})
|
|
61
62
|
if (!acl) {
|
|
@@ -63,21 +64,23 @@ class ACLChecker {
|
|
|
63
64
|
return this.aclCached[cacheKey]
|
|
64
65
|
}
|
|
65
66
|
let resource = rdf.sym(this.resource)
|
|
67
|
+
let parentResource = resource
|
|
68
|
+
if (!this.resource.endsWith('/')) { parentResource = rdf.sym(ACLChecker.getDirectory(this.resource)) }
|
|
66
69
|
if (this.resource.endsWith('/' + this.suffix)) {
|
|
67
70
|
resource = rdf.sym(ACLChecker.getDirectory(this.resource))
|
|
71
|
+
parentResource = resource
|
|
68
72
|
}
|
|
69
73
|
// If this is an ACL, Control mode must be present for any operations
|
|
70
74
|
if (this.isAcl(this.resource)) {
|
|
71
75
|
mode = 'Control'
|
|
72
|
-
|
|
76
|
+
const thisResource = this.resource.substring(0, this.resource.length - this.suffix.length)
|
|
77
|
+
resource = rdf.sym(thisResource)
|
|
78
|
+
parentResource = resource
|
|
79
|
+
if (!thisResource.endsWith('/')) parentResource = rdf.sym(ACLChecker.getDirectory(thisResource))
|
|
73
80
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return this.aclCached[cacheKey]
|
|
78
|
-
} */
|
|
79
|
-
const directory = acl.isContainer ? rdf.sym(ACLChecker.getDirectory(acl.acl)) : null
|
|
80
|
-
const aclFile = rdf.sym(acl.acl)
|
|
81
|
+
const directory = acl.isContainer ? rdf.sym(ACLChecker.getDirectory(acl.docAcl)) : null
|
|
82
|
+
const aclFile = rdf.sym(acl.docAcl)
|
|
83
|
+
const aclGraph = acl.docGraph
|
|
81
84
|
const agent = user ? rdf.sym(user) : null
|
|
82
85
|
const modes = [ACL(mode)]
|
|
83
86
|
const agentOrigin = this.agentOrigin
|
|
@@ -85,38 +88,65 @@ class ACLChecker {
|
|
|
85
88
|
let originTrustedModes = []
|
|
86
89
|
try {
|
|
87
90
|
this.fetch(aclFile.doc().value)
|
|
88
|
-
originTrustedModes = await aclCheck.getTrustedModesForOrigin(
|
|
89
|
-
return this.fetch(uriNode.doc().value,
|
|
91
|
+
originTrustedModes = await aclCheck.getTrustedModesForOrigin(aclGraph, resource, directory, aclFile, agentOrigin, (uriNode) => {
|
|
92
|
+
return this.fetch(uriNode.doc().value, aclGraph)
|
|
90
93
|
})
|
|
91
94
|
} catch (e) {
|
|
92
95
|
// FIXME: https://github.com/solid/acl-check/issues/23
|
|
93
96
|
// console.error(e.message)
|
|
94
97
|
}
|
|
95
|
-
let accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
|
|
96
98
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
+
function resourceAccessDenied (modes) {
|
|
100
|
+
return aclCheck.accessDenied(aclGraph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
|
|
101
|
+
}
|
|
102
|
+
function accessDeniedForAccessTo (modes) {
|
|
103
|
+
const accessDeniedAccessTo = aclCheck.accessDenied(aclGraph, directory, null, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
|
|
99
104
|
const accessResult = !accessDenied && !accessDeniedAccessTo
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
return accessResult ? false : accessDenied || accessDeniedAccessTo
|
|
106
|
+
}
|
|
107
|
+
async function accessdeniedFromParent (modes) {
|
|
108
|
+
const parentAclDirectory = ACLChecker.getDirectory(acl.parentAcl)
|
|
109
|
+
const parentDirectory = parentResource === parentAclDirectory ? null : rdf.sym(parentAclDirectory)
|
|
110
|
+
const accessDeniedParent = aclCheck.accessDenied(acl.parentGraph, parentResource, parentDirectory, rdf.sym(acl.parentAcl), agent, modes, agentOrigin, trustedOrigins, originTrustedModes)
|
|
111
|
+
const accessResult = !accessDenied && !accessDeniedParent
|
|
112
|
+
return accessResult ? false : accessDenied || accessDeniedParent
|
|
102
113
|
}
|
|
114
|
+
|
|
115
|
+
let accessDenied = resourceAccessDenied(modes)
|
|
116
|
+
// debugCache('accessDenied resource ' + accessDenied)
|
|
117
|
+
|
|
103
118
|
// For create and update HTTP methods
|
|
104
|
-
if ((method === 'PUT' || method === 'PATCH' || method === 'COPY')
|
|
119
|
+
if ((method === 'PUT' || method === 'PATCH' || method === 'COPY')) {
|
|
105
120
|
// if resource and acl have same parent container,
|
|
106
121
|
// and resource does not exist, then accessTo Append from parent is required
|
|
107
|
-
if (directory.value === dirname(aclFile.value) + '/' && !resourceExists) {
|
|
108
|
-
accessDeniedForAccessTo('Append')
|
|
122
|
+
if (directory && directory.value === dirname(aclFile.value) + '/' && !resourceExists) {
|
|
123
|
+
accessDenied = accessDeniedForAccessTo([ACL('Append')])
|
|
109
124
|
}
|
|
125
|
+
// debugCache('accessDenied PUT/PATCH ' + accessDenied)
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
// For delete HTTP method
|
|
113
|
-
if ((method === 'DELETE')
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
if ((method === 'DELETE')) {
|
|
130
|
+
if (resourceExists) {
|
|
131
|
+
// deleting a Container
|
|
132
|
+
// without Read, the response code will reveal whether a Container is empty or not
|
|
133
|
+
if (directory && this.resource.endsWith('/')) accessDenied = resourceAccessDenied([ACL('Read'), ACL('Write')])
|
|
134
|
+
// if resource and acl have same parent container,
|
|
135
|
+
// then both Read and Write on parent is required
|
|
136
|
+
else if (!directory && aclFile.value.endsWith(`/${this.suffix}`)) accessDenied = await accessdeniedFromParent([ACL('Read'), ACL('Write')])
|
|
137
|
+
|
|
138
|
+
// deleting a Document
|
|
139
|
+
else if (directory && directory.value === dirname(aclFile.value) + '/') {
|
|
140
|
+
accessDenied = accessDeniedForAccessTo([ACL('Write')])
|
|
141
|
+
} else {
|
|
142
|
+
accessDenied = await accessdeniedFromParent([ACL('Write')])
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// https://github.com/solid/specification/issues/14#issuecomment-1712773516
|
|
146
|
+
} else { accessDenied = true }
|
|
147
|
+
// debugCache('accessDenied DELETE ' + accessDenied)
|
|
119
148
|
}
|
|
149
|
+
|
|
120
150
|
if (accessDenied && user) {
|
|
121
151
|
this.messagesCached[cacheKey].push(HTTPError(403, accessDenied))
|
|
122
152
|
} else if (accessDenied) {
|
|
@@ -140,14 +170,21 @@ class ACLChecker {
|
|
|
140
170
|
return `${parts.join('/')}/`
|
|
141
171
|
}
|
|
142
172
|
|
|
143
|
-
// Gets
|
|
144
|
-
|
|
173
|
+
// Gets any ACLs that apply to the resource
|
|
174
|
+
// DELETE uses docAcl when docAcl is parent to the resource
|
|
175
|
+
// or docAcl and parentAcl when docAcl is the ACL of the Resource
|
|
176
|
+
async getNearestACL (method) {
|
|
145
177
|
const { resource } = this
|
|
146
178
|
let isContainer = false
|
|
147
179
|
const possibleACLs = this.getPossibleACLs()
|
|
148
180
|
const acls = [...possibleACLs]
|
|
149
181
|
let returnAcl = null
|
|
150
|
-
|
|
182
|
+
let returnParentAcl = null
|
|
183
|
+
let parentAcl = null
|
|
184
|
+
let parentGraph = null
|
|
185
|
+
let docAcl = null
|
|
186
|
+
let docGraph = null
|
|
187
|
+
while (possibleACLs.length > 0 && !returnParentAcl) {
|
|
151
188
|
const acl = possibleACLs.shift()
|
|
152
189
|
let graph
|
|
153
190
|
try {
|
|
@@ -155,28 +192,52 @@ class ACLChecker {
|
|
|
155
192
|
graph = await this.requests[acl]
|
|
156
193
|
} catch (err) {
|
|
157
194
|
if (err && (err.code === 'ENOENT' || err.status === 404)) {
|
|
158
|
-
isContainer
|
|
195
|
+
// only set isContainer before docAcl
|
|
196
|
+
if (!docAcl) isContainer = true
|
|
159
197
|
continue
|
|
160
198
|
}
|
|
161
199
|
debug(err)
|
|
162
200
|
throw err
|
|
163
201
|
}
|
|
164
|
-
const relative = resource.replace(acl.replace(/[^/]+$/, ''), './')
|
|
165
|
-
debug(`Using ACL ${acl} for ${relative}`)
|
|
166
|
-
|
|
202
|
+
// const relative = resource.replace(acl.replace(/[^/]+$/, ''), './')
|
|
203
|
+
// debug(`Using ACL ${acl} for ${relative}`)
|
|
204
|
+
if (!docAcl) {
|
|
205
|
+
docAcl = acl
|
|
206
|
+
docGraph = graph
|
|
207
|
+
// parentAcl is only needed for DELETE
|
|
208
|
+
if (method !== 'DELETE') returnParentAcl = true
|
|
209
|
+
} else {
|
|
210
|
+
parentAcl = acl
|
|
211
|
+
parentGraph = graph
|
|
212
|
+
returnParentAcl = true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
returnAcl = { docAcl, docGraph, isContainer, parentAcl, parentGraph }
|
|
167
216
|
}
|
|
168
217
|
if (!returnAcl) {
|
|
169
218
|
throw new HTTPError(500, `No ACL found for ${resource}, searched in \n- ${acls.join('\n- ')}`)
|
|
170
219
|
}
|
|
171
|
-
|
|
172
|
-
|
|
220
|
+
// fetch group
|
|
221
|
+
let groupNodes = returnAcl.docGraph.statementsMatching(null, ACL('agentGroup'), null)
|
|
222
|
+
let groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
|
|
173
223
|
await Promise.all(groupUrls.map(async groupUrl => {
|
|
174
224
|
try {
|
|
175
|
-
const
|
|
176
|
-
this.requests[groupUrl] = this.requests[groupUrl] ||
|
|
225
|
+
const docGraph = await this.fetch(groupUrl, returnAcl.docGraph)
|
|
226
|
+
this.requests[groupUrl] = this.requests[groupUrl] || docGraph
|
|
177
227
|
} catch (e) {} // failed to fetch groupUrl
|
|
178
228
|
}))
|
|
229
|
+
if (parentAcl) {
|
|
230
|
+
groupNodes = returnAcl.parentGraph.statementsMatching(null, ACL('agentGroup'), null)
|
|
231
|
+
groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
|
|
232
|
+
await Promise.all(groupUrls.map(async groupUrl => {
|
|
233
|
+
try {
|
|
234
|
+
const docGraph = await this.fetch(groupUrl, returnAcl.parentGraph)
|
|
235
|
+
this.requests[groupUrl] = this.requests[groupUrl] || docGraph
|
|
236
|
+
} catch (e) {} // failed to fetch groupUrl
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
179
239
|
|
|
240
|
+
// debugAccounts('ALAIN returnACl ' + '\ndocAcl ' + returnAcl.docAcl + '\nparentAcl ' + returnAcl.parentAcl)
|
|
180
241
|
return returnAcl
|
|
181
242
|
}
|
|
182
243
|
|
|
@@ -264,7 +325,7 @@ function fetchLocalOrRemote (mapper, serverUri) {
|
|
|
264
325
|
// debugCache('Expunging from cache', url)
|
|
265
326
|
delete temporaryCache[url]
|
|
266
327
|
if (Object.keys(temporaryCache).length === 0) {
|
|
267
|
-
debugCache('Cache is empty again')
|
|
328
|
+
// debugCache('Cache is empty again')
|
|
268
329
|
}
|
|
269
330
|
}, EXPIRY_MS),
|
|
270
331
|
promise: doFetch(url)
|
package/lib/create-app.js
CHANGED
|
@@ -32,7 +32,7 @@ const corsSettings = cors({
|
|
|
32
32
|
methods: [
|
|
33
33
|
'OPTIONS', 'HEAD', 'GET', 'PATCH', 'POST', 'PUT', 'DELETE'
|
|
34
34
|
],
|
|
35
|
-
exposedHeaders: 'Authorization, User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Updates-Via, Allow, WAC-Allow, Content-Length, WWW-Authenticate, MS-Author-Via, X-Powered-By',
|
|
35
|
+
exposedHeaders: 'Authorization, User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Accept-Put, Updates-Via, Allow, WAC-Allow, Content-Length, WWW-Authenticate, MS-Author-Via, X-Powered-By',
|
|
36
36
|
credentials: true,
|
|
37
37
|
maxAge: 1728000,
|
|
38
38
|
origin: true,
|
|
@@ -117,6 +117,33 @@ function createApp (argv = {}) {
|
|
|
117
117
|
// Attach the LDP middleware
|
|
118
118
|
app.use('/', LdpMiddleware(corsSettings))
|
|
119
119
|
|
|
120
|
+
// https://stackoverflow.com/questions/51741383/nodejs-express-return-405-for-un-supported-method
|
|
121
|
+
app.use(function (req, res, next) {
|
|
122
|
+
const AllLayers = app._router.stack
|
|
123
|
+
const Layers = AllLayers.filter(x => x.name === 'bound dispatch' && x.regexp.test(req.path))
|
|
124
|
+
|
|
125
|
+
const Methods = []
|
|
126
|
+
Layers.forEach(layer => {
|
|
127
|
+
for (const method in layer.route.methods) {
|
|
128
|
+
if (layer.route.methods[method] === true) {
|
|
129
|
+
Methods.push(method.toUpperCase())
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (Layers.length !== 0 && !Methods.includes(req.method)) {
|
|
135
|
+
// res.setHeader('Allow', Methods.join(','))
|
|
136
|
+
|
|
137
|
+
if (req.method === 'OPTIONS') {
|
|
138
|
+
return res.send(Methods.join(', '))
|
|
139
|
+
} else {
|
|
140
|
+
return res.status(405).send()
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
next()
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
120
147
|
// Errors
|
|
121
148
|
app.use(errorPages.handler)
|
|
122
149
|
|
package/lib/handlers/allow.js
CHANGED
|
@@ -2,7 +2,7 @@ module.exports = allow
|
|
|
2
2
|
|
|
3
3
|
// const path = require('path')
|
|
4
4
|
const ACL = require('../acl-checker')
|
|
5
|
-
const debug = require('../debug.js').ACL
|
|
5
|
+
// const debug = require('../debug.js').ACL
|
|
6
6
|
// const error = require('../http-error')
|
|
7
7
|
|
|
8
8
|
function allow (mode) {
|
|
@@ -77,7 +77,7 @@ function allow (mode) {
|
|
|
77
77
|
if (resourceUrl.endsWith('.acl') && (await ldp.isOwner(userId, req.hostname))) return next()
|
|
78
78
|
} catch (err) {}
|
|
79
79
|
const error = req.authError || await req.acl.getError(userId, mode)
|
|
80
|
-
debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`)
|
|
80
|
+
// debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`)
|
|
81
81
|
next(error)
|
|
82
82
|
}
|
|
83
83
|
}
|
package/lib/handlers/get.js
CHANGED
|
@@ -27,8 +27,13 @@ async function handler (req, res, next) {
|
|
|
27
27
|
const requestedType = negotiator.mediaType()
|
|
28
28
|
const possibleRDFType = negotiator.mediaType(RDFs)
|
|
29
29
|
|
|
30
|
+
// deprecated kept for compatibility
|
|
30
31
|
res.header('MS-Author-Via', 'SPARQL')
|
|
31
32
|
|
|
33
|
+
res.header('Accept-Patch', 'text/n3, application/sparql-update, application/sparql-update-single-match')
|
|
34
|
+
res.header('Accept-Post', '*/*')
|
|
35
|
+
if (!path.endsWith('/') && !glob.hasMagic(path)) res.header('Accept-Put', '*/*')
|
|
36
|
+
|
|
32
37
|
// Set live updates
|
|
33
38
|
if (ldp.live) {
|
|
34
39
|
res.header('Updates-Via', ldp.resourceMapper.resolveUrl(req.hostname).replace(/^http/, 'ws'))
|
|
@@ -49,6 +54,8 @@ async function handler (req, res, next) {
|
|
|
49
54
|
try {
|
|
50
55
|
ret = await ldp.get(options, req.accepts(['html', 'turtle', 'rdf+xml', 'n3', 'ld+json']) === 'html')
|
|
51
56
|
} catch (err) {
|
|
57
|
+
// set Accept-Put if container do not exist
|
|
58
|
+
if (err.status === 404 && path.endsWith('/')) res.header('Accept-Put', 'text/turtle')
|
|
52
59
|
// use globHandler if magic is detected
|
|
53
60
|
if (err.status === 404 && glob.hasMagic(path)) {
|
|
54
61
|
debug('forwarding to glob request')
|
|
@@ -116,10 +123,9 @@ async function handler (req, res, next) {
|
|
|
116
123
|
|
|
117
124
|
// If it is not in our RDFs we can't even translate,
|
|
118
125
|
// Sorry, we can't help
|
|
119
|
-
if (!possibleRDFType) {
|
|
126
|
+
if (!possibleRDFType || !RDFs.includes(contentType)) { // possibleRDFType defaults to text/turtle
|
|
120
127
|
return next(error(406, 'Cannot serve requested type: ' + contentType))
|
|
121
128
|
}
|
|
122
|
-
|
|
123
129
|
try {
|
|
124
130
|
// Translate from the contentType found to the possibleRDFType desired
|
|
125
131
|
const data = await translate(stream, baseUri, contentType, possibleRDFType)
|
|
@@ -128,8 +134,8 @@ async function handler (req, res, next) {
|
|
|
128
134
|
res.send(data)
|
|
129
135
|
return next()
|
|
130
136
|
} catch (err) {
|
|
131
|
-
debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' +
|
|
132
|
-
return next(error(500, '
|
|
137
|
+
debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 406 + ' ' + err.message)
|
|
138
|
+
return next(error(500, 'Cannot serve requested type: ' + requestedType))
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
package/lib/handlers/options.js
CHANGED
|
@@ -8,7 +8,7 @@ module.exports = handler
|
|
|
8
8
|
function handler (req, res, next) {
|
|
9
9
|
linkServiceEndpoint(req, res)
|
|
10
10
|
linkAuthProvider(req, res)
|
|
11
|
-
|
|
11
|
+
linkAcceptEndpoint(res)
|
|
12
12
|
|
|
13
13
|
res.status(204)
|
|
14
14
|
|
|
@@ -28,6 +28,8 @@ function linkServiceEndpoint (req, res) {
|
|
|
28
28
|
addLink(res, serviceEndpoint, 'service')
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function
|
|
32
|
-
res.header('Accept-Patch', 'application/sparql-update')
|
|
31
|
+
function linkAcceptEndpoint (res) {
|
|
32
|
+
res.header('Accept-Patch', 'text/n3, application/sparql-update, application/sparql-update-single-match')
|
|
33
|
+
res.header('Accept-Post', '*/*')
|
|
34
|
+
res.header('Accept-Put', '*/*')
|
|
33
35
|
}
|
package/lib/handlers/patch.js
CHANGED
|
@@ -39,7 +39,6 @@ function contentForNew (contentType) {
|
|
|
39
39
|
// Handles a PATCH request
|
|
40
40
|
async function patchHandler (req, res, next) {
|
|
41
41
|
debug(`PATCH -- ${req.originalUrl}`)
|
|
42
|
-
res.header('MS-Author-Via', 'SPARQL')
|
|
43
42
|
try {
|
|
44
43
|
// Obtain details of the target resource
|
|
45
44
|
const ldp = req.app.locals.ldp
|
|
@@ -69,12 +68,15 @@ async function patchHandler (req, res, next) {
|
|
|
69
68
|
patch.text = req.body ? req.body.toString() : ''
|
|
70
69
|
patch.uri = `${url}#patch-${hash(patch.text)}`
|
|
71
70
|
patch.contentType = getContentType(req.headers)
|
|
71
|
+
if (!patch.contentType) {
|
|
72
|
+
throw error(400, 'PATCH request requires a content-type via the Content-Type header')
|
|
73
|
+
}
|
|
72
74
|
debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType)
|
|
73
75
|
const parsePatch = PATCH_PARSERS[patch.contentType]
|
|
74
76
|
if (!parsePatch) {
|
|
75
77
|
throw error(415, `Unsupported patch content type: ${patch.contentType}`)
|
|
76
78
|
}
|
|
77
|
-
|
|
79
|
+
res.header('Accept-Patch', patch.contentType) // is this needed ?
|
|
78
80
|
// Parse the patch document and verify permissions
|
|
79
81
|
const patchObject = await parsePatch(url, patch.uri, patch.text)
|
|
80
82
|
await checkPermission(req, patchObject, resourceExists)
|
package/lib/handlers/put.js
CHANGED
|
@@ -8,10 +8,11 @@ const { stringToStream } = require('../utils')
|
|
|
8
8
|
|
|
9
9
|
async function handler (req, res, next) {
|
|
10
10
|
debug(req.originalUrl)
|
|
11
|
-
|
|
11
|
+
// deprecated kept for compatibility
|
|
12
|
+
res.header('MS-Author-Via', 'SPARQL') // is this needed ?
|
|
12
13
|
const contentType = req.get('content-type')
|
|
13
14
|
|
|
14
|
-
// check
|
|
15
|
+
// check whether a folder or resource with same name exists
|
|
15
16
|
try {
|
|
16
17
|
const ldp = req.app.locals.ldp
|
|
17
18
|
await ldp.checkItemName(req)
|
|
@@ -64,10 +65,16 @@ async function putStream (req, res, next, stream = req) {
|
|
|
64
65
|
try {
|
|
65
66
|
// First check if the file already exists
|
|
66
67
|
await ldp.resourceMapper.mapUrlToFile({ url: req })
|
|
68
|
+
// Fails on if-none-match asterisk precondition
|
|
69
|
+
if ((req.headers['if-none-match'] === '*') && !req.path.endsWith('/')) {
|
|
70
|
+
res.sendStatus(412)
|
|
71
|
+
return next()
|
|
72
|
+
}
|
|
67
73
|
} catch (err) {
|
|
68
74
|
resourceExists = false
|
|
69
75
|
}
|
|
70
76
|
try {
|
|
77
|
+
// Fails with Append on existing resource
|
|
71
78
|
if (!req.originalUrl.endsWith('.acl')) await checkPermission(req, resourceExists)
|
|
72
79
|
await ldp.put(req, stream, getContentType(req.headers))
|
|
73
80
|
res.sendStatus(201)
|
package/lib/ldp.js
CHANGED
|
@@ -145,20 +145,28 @@ class LDP {
|
|
|
145
145
|
|
|
146
146
|
const ldp = this
|
|
147
147
|
debug.handlers('POST -- On parent: ' + containerPath)
|
|
148
|
-
|
|
148
|
+
if (container) {
|
|
149
|
+
// Containers should not receive an extension
|
|
150
|
+
extension = ''
|
|
151
|
+
}
|
|
152
|
+
// pepare slug
|
|
149
153
|
if (slug) {
|
|
150
|
-
if (this.isAuxResource(slug, extension)) throw error(403, 'POST is not allowed for auxiliary resources')
|
|
151
154
|
slug = decodeURIComponent(slug)
|
|
155
|
+
|
|
156
|
+
if (container) {
|
|
157
|
+
// the name of a container cannot be a valid auxiliary resource document
|
|
158
|
+
while (this._containsInvalidSuffixes(slug + '/')) {
|
|
159
|
+
const idx = slug.lastIndexOf('.')
|
|
160
|
+
slug = slug.substr(0, idx)
|
|
161
|
+
}
|
|
162
|
+
} else if (this.isAuxResource(slug, extension)) throw error(403, 'POST to auxiliary resources is not allowed')
|
|
163
|
+
|
|
152
164
|
if (slug.match(/\/|\||:/)) {
|
|
153
|
-
throw error(400, 'The name of new file
|
|
165
|
+
throw error(400, 'The name of a POSTed new file may not contain ":" (colon), "|" (pipe), or "/" (slash)')
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
|
-
// Containers should not receive an extension
|
|
157
|
-
if (container) {
|
|
158
|
-
extension = ''
|
|
159
|
-
}
|
|
160
168
|
|
|
161
|
-
//
|
|
169
|
+
// always return a valid URL.
|
|
162
170
|
const resourceUrl = await ldp.getAvailableUrl(hostname, containerPath, { slug, extension, container })
|
|
163
171
|
debug.handlers('POST -- Will create at: ' + resourceUrl)
|
|
164
172
|
|
|
@@ -327,11 +335,25 @@ class LDP {
|
|
|
327
335
|
} catch (err) { }
|
|
328
336
|
}
|
|
329
337
|
|
|
330
|
-
|
|
338
|
+
/**
|
|
339
|
+
* This function is used to make sure a resource or container which contains
|
|
340
|
+
* reserved suffixes for auxiliary documents cannot be created.
|
|
341
|
+
* @param {string} path - the uri to check for invalid suffixes
|
|
342
|
+
* @returns {boolean} true is fail - if the path contains reserved suffixes
|
|
343
|
+
*/
|
|
344
|
+
_containsInvalidSuffixes (path) {
|
|
345
|
+
return AUXILIARY_RESOURCES.some(suffix => path.endsWith(suffix + '/'))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// check whether a document (or container) has the same name as another document (or container)
|
|
331
349
|
async checkItemName (url) {
|
|
332
350
|
let testName, testPath
|
|
333
351
|
const { hostname, pathname } = this.resourceMapper._parseUrl(url) // (url.url || url)
|
|
334
352
|
let itemUrl = this.resourceMapper.resolveUrl(hostname, pathname)
|
|
353
|
+
// make sure the resource being created does not attempt invalid resource creation
|
|
354
|
+
if (this._containsInvalidSuffixes(itemUrl)) {
|
|
355
|
+
throw error(400, `${itemUrl} contained reserved suffixes in path`)
|
|
356
|
+
}
|
|
335
357
|
const container = itemUrl.endsWith('/')
|
|
336
358
|
try {
|
|
337
359
|
const testUrl = container ? itemUrl.slice(0, -1) : itemUrl + '/'
|
|
@@ -350,7 +372,7 @@ class LDP {
|
|
|
350
372
|
}
|
|
351
373
|
}
|
|
352
374
|
if (testName) {
|
|
353
|
-
throw error(
|
|
375
|
+
throw error(409, `${testPath}: Container and resource cannot have the same name in URI`)
|
|
354
376
|
}
|
|
355
377
|
}
|
|
356
378
|
|
|
@@ -470,9 +492,9 @@ class LDP {
|
|
|
470
492
|
throw err
|
|
471
493
|
}
|
|
472
494
|
const stream = stringToStream(data)
|
|
473
|
-
// TODO 'text/turtle'
|
|
474
|
-
// This forces one
|
|
475
|
-
return { stream, contentType
|
|
495
|
+
// TODO contentType is defaultContainerContentType ('text/turtle'),
|
|
496
|
+
// This forces one translation turtle -> desired
|
|
497
|
+
return { stream, contentType, container: true }
|
|
476
498
|
} else {
|
|
477
499
|
let chunksize, contentRange, start, end
|
|
478
500
|
if (options.range) {
|
|
@@ -585,13 +607,13 @@ class LDP {
|
|
|
585
607
|
|
|
586
608
|
let itemName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
|
|
587
609
|
try {
|
|
588
|
-
// check resource exists
|
|
610
|
+
// check whether resource exists
|
|
589
611
|
const context = container ? '/' : ''
|
|
590
612
|
await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
|
|
591
613
|
itemName = `${uuid.v1()}-${itemName}`
|
|
592
614
|
} catch (e) {
|
|
593
615
|
try {
|
|
594
|
-
// check resource with same name exists
|
|
616
|
+
// check whether resource with same name exists
|
|
595
617
|
const context = !container ? '/' : ''
|
|
596
618
|
await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
|
|
597
619
|
itemName = `${uuid.v1()}-${itemName}`
|
|
@@ -140,7 +140,9 @@ class PasswordAuthenticator extends Authenticator {
|
|
|
140
140
|
})
|
|
141
141
|
.then(foundUser => {
|
|
142
142
|
if (!foundUser) {
|
|
143
|
-
|
|
143
|
+
// CWE - CWE-200: Exposure of Sensitive Information to an Unauthorized Actor (4.13)
|
|
144
|
+
// https://cwe.mitre.org/data/definitions/200.html
|
|
145
|
+
error = new Error('Invalid username/password combination.') // no detail for security 'No user found for that username')
|
|
144
146
|
error.statusCode = 400
|
|
145
147
|
throw error
|
|
146
148
|
}
|
|
@@ -151,7 +153,9 @@ class PasswordAuthenticator extends Authenticator {
|
|
|
151
153
|
})
|
|
152
154
|
.then(validUser => {
|
|
153
155
|
if (!validUser) {
|
|
154
|
-
|
|
156
|
+
// CWE - CWE-200: Exposure of Sensitive Information to an Unauthorized Actor (4.13)
|
|
157
|
+
// https://cwe.mitre.org/data/definitions/200.html
|
|
158
|
+
error = new Error('Invalid username/password combination.') // no detail for security 'User found but no password match')
|
|
155
159
|
error.statusCode = 400
|
|
156
160
|
throw error
|
|
157
161
|
}
|
|
@@ -178,7 +178,7 @@ class CreateAccountRequest extends AuthRequest {
|
|
|
178
178
|
.then(exists => {
|
|
179
179
|
if (exists) {
|
|
180
180
|
debug(`Canceling account creation, ${userAccount.webId} already exists`)
|
|
181
|
-
const error = new Error('Account
|
|
181
|
+
const error = new Error('Account creation failed')
|
|
182
182
|
error.status = 400
|
|
183
183
|
throw error
|
|
184
184
|
}
|
|
@@ -94,7 +94,7 @@ class PasswordResetEmailRequest extends AuthRequest {
|
|
|
94
94
|
.then(() => request.validate())
|
|
95
95
|
.then(() => request.loadUser())
|
|
96
96
|
.then(userAccount => request.sendResetLink(userAccount))
|
|
97
|
-
.then(() => request.
|
|
97
|
+
.then(() => request.resetLinkMessage())
|
|
98
98
|
.catch(error => request.error(error))
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -123,7 +123,10 @@ class PasswordResetEmailRequest extends AuthRequest {
|
|
|
123
123
|
return this.accountManager.accountExists(username)
|
|
124
124
|
.then(exists => {
|
|
125
125
|
if (!exists) {
|
|
126
|
-
|
|
126
|
+
// For security reasons, avoid leaking error information
|
|
127
|
+
// See: https://github.com/nodeSolidServer/node-solid-server/issues/1770
|
|
128
|
+
this.accountManager.verifyEmailDependencies()
|
|
129
|
+
return this.resetLinkMessage()
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
const userData = { username }
|
|
@@ -191,7 +194,7 @@ class PasswordResetEmailRequest extends AuthRequest {
|
|
|
191
194
|
/**
|
|
192
195
|
* Displays the 'your reset link has been sent' success message view
|
|
193
196
|
*/
|
|
194
|
-
|
|
197
|
+
resetLinkMessage () {
|
|
195
198
|
this.response.render('auth/reset-link-sent')
|
|
196
199
|
}
|
|
197
200
|
}
|
|
@@ -64,10 +64,11 @@ class SharingRequest extends AuthRequest {
|
|
|
64
64
|
* @param req {IncomingRequest}
|
|
65
65
|
* @param res {ServerResponse}
|
|
66
66
|
*/
|
|
67
|
-
static async get (req, res) {
|
|
67
|
+
static async get (req, res, next) {
|
|
68
68
|
const request = SharingRequest.fromParams(req, res)
|
|
69
69
|
|
|
70
70
|
const appUrl = request.getAppUrl()
|
|
71
|
+
if (!appUrl) return next()
|
|
71
72
|
const appOrigin = appUrl.origin
|
|
72
73
|
const serverUrl = new url.URL(req.app.locals.ldp.serverUri)
|
|
73
74
|
|
|
@@ -153,6 +154,7 @@ class SharingRequest extends AuthRequest {
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
getAppUrl () {
|
|
157
|
+
if (!this.authQueryParams.redirect_uri) return
|
|
156
158
|
return new url.URL(this.authQueryParams.redirect_uri)
|
|
157
159
|
}
|
|
158
160
|
|
package/lib/resource-mapper.js
CHANGED
|
@@ -25,6 +25,7 @@ class ResourceMapper {
|
|
|
25
25
|
rootPath,
|
|
26
26
|
includeHost = false,
|
|
27
27
|
defaultContentType = 'application/octet-stream',
|
|
28
|
+
defaultContainerContentType = 'text/turtle',
|
|
28
29
|
indexFilename = 'index.html',
|
|
29
30
|
overrideTypes = { acl: 'text/turtle', meta: 'text/turtle' }
|
|
30
31
|
}) {
|
|
@@ -33,6 +34,7 @@ class ResourceMapper {
|
|
|
33
34
|
this._includeHost = includeHost
|
|
34
35
|
this._readdir = readdir
|
|
35
36
|
this._defaultContentType = defaultContentType
|
|
37
|
+
this._defaultContainerContentType = defaultContainerContentType
|
|
36
38
|
this._types = { ...types, ...overrideTypes }
|
|
37
39
|
this._indexFilename = indexFilename
|
|
38
40
|
this._indexContentType = this._getContentTypeFromExtension(indexFilename)
|
|
@@ -187,10 +189,11 @@ class ResourceMapper {
|
|
|
187
189
|
return url
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
// Gets the expected content type based on the extension of the path
|
|
192
|
+
// Gets the expected content type based on resource type and the extension of the path
|
|
191
193
|
_getContentTypeFromExtension (path) {
|
|
194
|
+
const defaultContentType = (path === '' || path.endsWith('/')) ? this._defaultContainerContentType : this._defaultContentType
|
|
192
195
|
const extension = /\.([^/.]+)$/.exec(path)
|
|
193
|
-
return extension && this._types[extension[1].toLowerCase()] ||
|
|
196
|
+
return extension && this._types[extension[1].toLowerCase()] || defaultContentType
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
// Appends an extension for the specific content type, if needed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-server",
|
|
3
3
|
"description": "Solid server on top of the file-system",
|
|
4
|
-
"version": "5.7.
|
|
4
|
+
"version": "5.7.10",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Tim Berners-Lee",
|
|
7
7
|
"email": "timbl@w3.org"
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@solid/acl-check": "^0.4.5",
|
|
64
64
|
"@solid/oidc-auth-manager": "^0.24.3",
|
|
65
65
|
"@solid/oidc-op": "^0.11.6",
|
|
66
|
-
"async-lock": "^1.4.
|
|
66
|
+
"async-lock": "^1.4.1",
|
|
67
67
|
"body-parser": "^1.20.2",
|
|
68
68
|
"bootstrap": "^3.4.1",
|
|
69
69
|
"cached-path-relative": "^1.1.0",
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
"commander": "^8.3.0",
|
|
74
74
|
"cors": "^2.8.5",
|
|
75
75
|
"debug": "^4.3.4",
|
|
76
|
-
"express": "^4.18.
|
|
76
|
+
"express": "^4.18.3",
|
|
77
77
|
"express-handlebars": "^5.3.5",
|
|
78
|
-
"express-session": "^1.
|
|
78
|
+
"express-session": "^1.18.0",
|
|
79
79
|
"extend": "^3.0.2",
|
|
80
80
|
"from2": "^2.3.0",
|
|
81
81
|
"fs-extra": "^10.1.0",
|
|
@@ -89,13 +89,13 @@
|
|
|
89
89
|
"ip-range-check": "0.2.0",
|
|
90
90
|
"is-ip": "^3.1.0",
|
|
91
91
|
"li": "^1.3.0",
|
|
92
|
-
"mashlib": "^1.8.
|
|
92
|
+
"mashlib": "^1.8.10",
|
|
93
93
|
"mime-types": "^2.1.35",
|
|
94
94
|
"negotiator": "^0.6.3",
|
|
95
95
|
"node-fetch": "^2.7.0",
|
|
96
96
|
"node-forge": "^1.3.1",
|
|
97
97
|
"node-mailer": "^0.1.1",
|
|
98
|
-
"nodemailer": "^6.9.
|
|
98
|
+
"nodemailer": "^6.9.11",
|
|
99
99
|
"oidc-op-express": "^0.0.3",
|
|
100
100
|
"owasp-password-strength-test": "^1.3.0",
|
|
101
101
|
"recursive-readdir": "^2.2.3",
|
|
@@ -115,23 +115,23 @@
|
|
|
115
115
|
},
|
|
116
116
|
"devDependencies": {
|
|
117
117
|
"@solid/solid-auth-oidc": "0.3.0",
|
|
118
|
-
"chai": "^4.
|
|
118
|
+
"chai": "^4.4.1",
|
|
119
119
|
"chai-as-promised": "7.1.1",
|
|
120
120
|
"cross-env": "7.0.3",
|
|
121
121
|
"dirty-chai": "2.0.1",
|
|
122
122
|
"eslint": "^7.32.0",
|
|
123
123
|
"localstorage-memory": "1.0.3",
|
|
124
|
-
"mocha": "^10.
|
|
125
|
-
"nock": "^13.4
|
|
126
|
-
"node-mocks-http": "^1.14.
|
|
124
|
+
"mocha": "^10.3.0",
|
|
125
|
+
"nock": "^13.5.4",
|
|
126
|
+
"node-mocks-http": "^1.14.1",
|
|
127
127
|
"nyc": "15.1.0",
|
|
128
128
|
"pre-commit": "1.2.2",
|
|
129
129
|
"randombytes": "2.1.0",
|
|
130
130
|
"sinon": "12.0.1",
|
|
131
131
|
"sinon-chai": "3.7.0",
|
|
132
|
-
"snyk": "^1.
|
|
132
|
+
"snyk": "^1.1283.0",
|
|
133
133
|
"standard": "16.0.4",
|
|
134
|
-
"supertest": "^6.3.
|
|
134
|
+
"supertest": "^6.3.4",
|
|
135
135
|
"turtle-validator": "1.1.1",
|
|
136
136
|
"whatwg-url": "11.0.0"
|
|
137
137
|
},
|
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
"validate": "node ./test/validate-turtle.js",
|
|
147
147
|
"nyc": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 nyc --reporter=text-summary mocha --recursive test/integration/ test/unit/",
|
|
148
148
|
"mocha": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/ test/unit/",
|
|
149
|
+
"mocha-integration": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/http-test.js",
|
|
149
150
|
"prepublishOnly": "npm test",
|
|
150
151
|
"postpublish": "git push --follow-tags",
|
|
151
152
|
"test": "npm run standard && npm run validate && npm run nyc",
|