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.
@@ -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/.nvmrc CHANGED
@@ -1 +1 @@
1
- 16.14.0
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.
@@ -14,7 +14,7 @@
14
14
  </div>
15
15
 
16
16
  <div class="alert alert-success">
17
- <p>A Reset Password link has been sent to your email.</p>
17
+ <p>A Reset Password link has been sent to the associated email account.</p>
18
18
  </div>
19
19
  </div>
20
20
  </body>
@@ -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
@@ -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)
@@ -8,10 +8,11 @@ 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')
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 if a folder or resource with same name exists
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
- // prepare slug
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 POSTed may not contain : | or /')
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
- // allways return a valid URL.
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
- // check if a document (or container) has the same name than a document (or container)
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(404, `${testPath}: Container and resource cannot have the same name in URI`)
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' is fixed, should be contentType instead
474
- // This forces one more translation turtle -> desired
475
- return { stream, contentType: 'text/turtle', container: true }
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}`
@@ -537,7 +537,7 @@ class AccountManager {
537
537
  throw new Error('Email service is not set up')
538
538
  }
539
539
 
540
- if (!userAccount.email) {
540
+ if (userAccount && !userAccount.email) {
541
541
  throw new Error('Account recovery email has not been provided')
542
542
  }
543
543
  }
@@ -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
  }
@@ -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 already exists')
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.renderSuccess())
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
- throw new Error('Account not found for that username')
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
- renderSuccess () {
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
 
@@ -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.9-alpha",
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.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",
@@ -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.9",
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.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": "^10.2.0",
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
  },
@@ -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",