solid-server 5.7.9-alpha → 5.7.9
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/README.md +9 -0
- 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 +8 -8
- package/lib/models/authenticator.js +6 -2
- package/lib/resource-mapper.js +5 -2
- package/package.json +11 -11
package/.github/workflows/ci.yml
CHANGED
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
|
@@ -158,7 +158,7 @@ class LDP {
|
|
|
158
158
|
extension = ''
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
//
|
|
161
|
+
// always return a valid URL.
|
|
162
162
|
const resourceUrl = await ldp.getAvailableUrl(hostname, containerPath, { slug, extension, container })
|
|
163
163
|
debug.handlers('POST -- Will create at: ' + resourceUrl)
|
|
164
164
|
|
|
@@ -327,7 +327,7 @@ class LDP {
|
|
|
327
327
|
} catch (err) { }
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
// check
|
|
330
|
+
// check whether a document (or container) has the same name as another document (or container)
|
|
331
331
|
async checkItemName (url) {
|
|
332
332
|
let testName, testPath
|
|
333
333
|
const { hostname, pathname } = this.resourceMapper._parseUrl(url) // (url.url || url)
|
|
@@ -350,7 +350,7 @@ class LDP {
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
if (testName) {
|
|
353
|
-
throw error(
|
|
353
|
+
throw error(409, `${testPath}: Container and resource cannot have the same name in URI`)
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
|
|
@@ -470,9 +470,9 @@ class LDP {
|
|
|
470
470
|
throw err
|
|
471
471
|
}
|
|
472
472
|
const stream = stringToStream(data)
|
|
473
|
-
// TODO 'text/turtle'
|
|
474
|
-
// This forces one
|
|
475
|
-
return { stream, contentType
|
|
473
|
+
// TODO contentType is defaultContainerContentType ('text/turtle'),
|
|
474
|
+
// This forces one translation turtle -> desired
|
|
475
|
+
return { stream, contentType, container: true }
|
|
476
476
|
} else {
|
|
477
477
|
let chunksize, contentRange, start, end
|
|
478
478
|
if (options.range) {
|
|
@@ -585,13 +585,13 @@ class LDP {
|
|
|
585
585
|
|
|
586
586
|
let itemName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
|
|
587
587
|
try {
|
|
588
|
-
// check resource exists
|
|
588
|
+
// check whether resource exists
|
|
589
589
|
const context = container ? '/' : ''
|
|
590
590
|
await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
|
|
591
591
|
itemName = `${uuid.v1()}-${itemName}`
|
|
592
592
|
} catch (e) {
|
|
593
593
|
try {
|
|
594
|
-
// check resource with same name exists
|
|
594
|
+
// check whether resource with same name exists
|
|
595
595
|
const context = !container ? '/' : ''
|
|
596
596
|
await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
|
|
597
597
|
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
|
}
|
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.9
|
|
4
|
+
"version": "5.7.9",
|
|
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",
|
|
@@ -95,7 +95,7 @@
|
|
|
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
|
},
|