solid-server 5.7.8 → 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.
@@ -17,7 +17,7 @@ jobs:
17
17
 
18
18
  strategy:
19
19
  matrix:
20
- node-version: [16.x, 18.x]
20
+ node-version: [18.x]
21
21
  os: [ubuntu-latest]
22
22
 
23
23
  steps:
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.
@@ -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
- const acl = await this.getNearestACL().catch(err => {
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
- resource = rdf.sym(this.resource.substring(0, this.resource.length - this.suffix.length))
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
- // If the slug is an acl, reject
75
- /* if (this.isAcl(this.slug)) {
76
- this.aclCached[cacheKey] = Promise.resolve(false)
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(acl.graph, resource, directory, aclFile, agentOrigin, (uriNode) => {
89
- return this.fetch(uriNode.doc().value, acl.graph)
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 accessDeniedForAccessTo (mode) {
98
- const accessDeniedAccessTo = aclCheck.accessDenied(acl.graph, directory, null, aclFile, agent, [ACL(mode)], agentOrigin, trustedOrigins, originTrustedModes)
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
- accessDenied = accessResult ? false : accessDenied || accessDeniedAccessTo
101
- // debugCache('accessDenied result ' + accessDenied)
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') && directory) {
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') && directory) {
114
- // if resource and acl have same parent container,
115
- // then accessTo Write from parent is required
116
- if (directory.value === dirname(aclFile.value) + '/') {
117
- accessDeniedForAccessTo('Write')
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 the ACL that applies to the resource
144
- async getNearestACL () {
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
- while (possibleACLs.length > 0 && !returnAcl) {
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 = true
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
- returnAcl = { acl, graph, isContainer }
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
- const groupNodes = returnAcl.graph.statementsMatching(null, ACL('agentGroup'), null)
172
- const groupUrls = groupNodes.map(node => node.object.value.split('#')[0])
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 graph = await this.fetch(groupUrl, returnAcl.graph)
176
- this.requests[groupUrl] = this.requests[groupUrl] || graph
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
 
@@ -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
  }
@@ -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 + ' -- ' + 500 + ' ' + err.message)
132
- return next(error(500, 'Error translating between RDF formats'))
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
 
@@ -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
- linkSparqlEndpoint(res)
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 linkSparqlEndpoint (res) {
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
  }
@@ -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
@@ -53,7 +52,11 @@ async function patchHandler (req, res, next) {
53
52
  ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile(
54
53
  { url: req, createIfNotExists: true, contentType: contentTypeForNew(req) }))
55
54
  // check if a folder with same name exists
56
- await ldp.checkItemName(req)
55
+ try {
56
+ await ldp.checkItemName(req)
57
+ } catch (err) {
58
+ return next(err)
59
+ }
57
60
  resourceExists = false
58
61
  }
59
62
  const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname })
@@ -65,12 +68,15 @@ async function patchHandler (req, res, next) {
65
68
  patch.text = req.body ? req.body.toString() : ''
66
69
  patch.uri = `${url}#patch-${hash(patch.text)}`
67
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
+ }
68
74
  debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType)
69
75
  const parsePatch = PATCH_PARSERS[patch.contentType]
70
76
  if (!parsePatch) {
71
77
  throw error(415, `Unsupported patch content type: ${patch.contentType}`)
72
78
  }
73
-
79
+ res.header('Accept-Patch', patch.contentType) // is this needed ?
74
80
  // Parse the patch document and verify permissions
75
81
  const patchObject = await parsePatch(url, patch.uri, patch.text)
76
82
  await checkPermission(req, patchObject, resourceExists)
@@ -8,11 +8,19 @@ const { stringToStream } = require('../utils')
8
8
 
9
9
  async function handler (req, res, next) {
10
10
  debug(req.originalUrl)
11
- res.header('MS-Author-Via', 'SPARQL')
12
-
11
+ // deprecated kept for compatibility
12
+ res.header('MS-Author-Via', 'SPARQL') // is this needed ?
13
13
  const contentType = req.get('content-type')
14
+
15
+ // check whether a folder or resource with same name exists
16
+ try {
17
+ const ldp = req.app.locals.ldp
18
+ await ldp.checkItemName(req)
19
+ } catch (e) {
20
+ return next(e)
21
+ }
14
22
  // check for valid rdf content for auxiliary resource and /profile/card
15
- // in future we may check that /profile/card is a minimal valid WebID card
23
+ // TODO check that /profile/card is a minimal valid WebID card
16
24
  if (isAuxiliary(req) || req.originalUrl === '/profile/card') {
17
25
  if (contentType === 'text/turtle') {
18
26
  return bodyParser.text({ type: () => true })(req, res, () => putValidRdf(req, res, next))
@@ -21,17 +29,57 @@ async function handler (req, res, next) {
21
29
  return putStream(req, res, next)
22
30
  }
23
31
 
32
+ // Verifies whether the user is allowed to perform Append PUT on the target
33
+ async function checkPermission (request, resourceExists) {
34
+ // If no ACL object was passed down, assume permissions are okay.
35
+ if (!request.acl) return Promise.resolve()
36
+ // At this point, we already assume append access,
37
+ // we might need to perform additional checks.
38
+ let modes = []
39
+ // acl:default Write is required for PUT when Resource Exists
40
+ if (resourceExists) modes = ['Write']
41
+ // if (resourceExists && request.originalUrl.endsWith('.acl')) modes = ['Control']
42
+ const { acl, session: { userId } } = request
43
+
44
+ const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode, request.method, resourceExists)))
45
+ const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true)
46
+ if (!allAllowed) {
47
+ // check owner with Control
48
+ // const ldp = request.app.locals.ldp
49
+ // if (request.path.endsWith('.acl') && userId === await ldp.getOwner(request.hostname)) return Promise.resolve()
50
+
51
+ const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode)))
52
+ const error = errors.filter(error => !!error)
53
+ .reduce((prevErr, err) => prevErr.status > err.status ? prevErr : err, { status: 0 })
54
+ return Promise.reject(error)
55
+ }
56
+ return Promise.resolve()
57
+ }
58
+
24
59
  // TODO could be renamed as putResource (it now covers container and non-container)
25
60
  async function putStream (req, res, next, stream = req) {
26
61
  const ldp = req.app.locals.ldp
62
+ // try {
63
+ // Obtain details of the target resource
64
+ let resourceExists = true
65
+ try {
66
+ // First check if the file already exists
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
+ }
73
+ } catch (err) {
74
+ resourceExists = false
75
+ }
27
76
  try {
28
- debug('test ' + req.get('content-type') + getContentType(req.headers))
77
+ // Fails with Append on existing resource
78
+ if (!req.originalUrl.endsWith('.acl')) await checkPermission(req, resourceExists)
29
79
  await ldp.put(req, stream, getContentType(req.headers))
30
- debug('succeded putting the file/folder')
31
80
  res.sendStatus(201)
32
81
  return next()
33
82
  } catch (err) {
34
- debug('error putting the file/folder:' + err.message)
35
83
  err.message = 'Can\'t write file/folder: ' + err.message
36
84
  return next(err)
37
85
  }
@@ -25,7 +25,7 @@ function LdpMiddleware (corsSettings) {
25
25
  router.get('/*', index, allow('Read'), header.addPermissions, get)
26
26
  router.post('/*', allow('Append'), post)
27
27
  router.patch('/*', allow('Append'), patch)
28
- router.put('/*', allow('Write'), put)
28
+ router.put('/*', allow('Append'), put)
29
29
  router.delete('/*', allow('Write'), del)
30
30
 
31
31
  return router
package/lib/ldp.js CHANGED
@@ -157,37 +157,13 @@ class LDP {
157
157
  if (container) {
158
158
  extension = ''
159
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
160
 
175
- // TODO: possibly package this in ldp.post
176
- let resourceUrl = await ldp.getAvailableUrl(hostname, containerPath, { slug, extension })
161
+ // always return a valid URL.
162
+ const resourceUrl = await ldp.getAvailableUrl(hostname, containerPath, { slug, extension, container })
177
163
  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
164
 
189
165
  await ldp.put(resourceUrl, stream, contentType)
190
- return URL.parse(originalUrl).path
166
+ return URL.parse(resourceUrl).path
191
167
  }
192
168
 
193
169
  isAuxResource (slug, extension) {
@@ -249,10 +225,6 @@ class LDP {
249
225
  throw error(400, 'Resource with a $.ext is not allowed by the server')
250
226
  }
251
227
 
252
- // check if a folder or file with same name exists
253
- // const urlItem = url.url || url
254
- await this.checkItemName(url)
255
-
256
228
  // First check if we are above quota
257
229
  let isOverQuota
258
230
  // Someone had a reason to make url actually a req sometimes but not
@@ -355,8 +327,9 @@ class LDP {
355
327
  } catch (err) { }
356
328
  }
357
329
 
330
+ // check whether a document (or container) has the same name as another document (or container)
358
331
  async checkItemName (url) {
359
- let testName
332
+ let testName, testPath
360
333
  const { hostname, pathname } = this.resourceMapper._parseUrl(url) // (url.url || url)
361
334
  let itemUrl = this.resourceMapper.resolveUrl(hostname, pathname)
362
335
  const container = itemUrl.endsWith('/')
@@ -373,11 +346,11 @@ class LDP {
373
346
  const { pathname } = this.resourceMapper._parseUrl(itemUrl) // (url.url || url)
374
347
  // check not at root
375
348
  if (pathname !== '/') {
376
- await this.checkItemName(itemUrl)
349
+ return await this.checkItemName(itemUrl)
377
350
  }
378
351
  }
379
352
  if (testName) {
380
- throw error(404, 'Container and resource cannot have the same name in URI')
353
+ throw error(409, `${testPath}: Container and resource cannot have the same name in URI`)
381
354
  }
382
355
  }
383
356
 
@@ -497,9 +470,9 @@ class LDP {
497
470
  throw err
498
471
  }
499
472
  const stream = stringToStream(data)
500
- // TODO 'text/turtle' is fixed, should be contentType instead
501
- // This forces one more translation turtle -> desired
502
- return { stream, contentType: 'text/turtle', container: true }
473
+ // TODO contentType is defaultContainerContentType ('text/turtle'),
474
+ // This forces one translation turtle -> desired
475
+ return { stream, contentType, container: true }
503
476
  } else {
504
477
  let chunksize, contentRange, start, end
505
478
  if (options.range) {
@@ -606,17 +579,26 @@ class LDP {
606
579
  }
607
580
  }
608
581
 
609
- async getAvailableUrl (hostname, containerURI, { slug = uuid.v1(), extension }) {
582
+ async getAvailableUrl (hostname, containerURI, { slug = uuid.v1(), extension, container }) {
610
583
  let requestUrl = this.resourceMapper.resolveUrl(hostname, containerURI)
611
- requestUrl = requestUrl.replace(/\/*$/, '/')
584
+ requestUrl = requestUrl.replace(/\/*$/, '/') // ??? what for
612
585
 
613
- const { path: containerFilePath } = await this.resourceMapper.mapUrlToFile({ url: requestUrl })
614
- let fileName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
615
- if (await promisify(fs.exists)(utilPath.join(containerFilePath, fileName))) {
616
- fileName = `${uuid.v1()}-${fileName}`
617
- }
618
-
619
- return requestUrl + fileName
586
+ let itemName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
587
+ try {
588
+ // check whether resource exists
589
+ const context = container ? '/' : ''
590
+ await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
591
+ itemName = `${uuid.v1()}-${itemName}`
592
+ } catch (e) {
593
+ try {
594
+ // check whether resource with same name exists
595
+ const context = !container ? '/' : ''
596
+ await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
597
+ itemName = `${uuid.v1()}-${itemName}`
598
+ } catch (e) {}
599
+ }
600
+ if (container) itemName += '/'
601
+ return requestUrl + itemName
620
602
  }
621
603
 
622
604
  getTrustedOrigins (req) {
@@ -140,7 +140,9 @@ class PasswordAuthenticator extends Authenticator {
140
140
  })
141
141
  .then(foundUser => {
142
142
  if (!foundUser) {
143
- error = new Error('No user found for that username')
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
- error = new Error('User found but no password match')
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
  }
@@ -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()] || this._defaultContentType
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.8",
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.0",
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.2",
76
+ "express": "^4.18.3",
77
77
  "express-handlebars": "^5.3.5",
78
- "express-session": "^1.17.3",
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.7",
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.3.10",
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": "^9.2.2",
125
- "nock": "^13.4.0",
126
- "node-mocks-http": "^1.14.0",
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.1264.0",
132
+ "snyk": "^1.1283.0",
133
133
  "standard": "16.0.4",
134
- "supertest": "^6.3.3",
134
+ "supertest": "^6.3.4",
135
135
  "turtle-validator": "1.1.1",
136
136
  "whatwg-url": "11.0.0"
137
137
  },