solid-server 5.7.7 → 5.7.9-alpha

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: [12.x, 14.x, 16.x]
20
+ node-version: [16.x, 18.x]
21
21
  os: [ubuntu-latest]
22
22
 
23
23
  steps:
@@ -97,4 +97,4 @@ jobs:
97
97
  build-args: SOLID_SERVER_VERSION=${{ steps.tagName.outputs.version }}
98
98
  push: true
99
99
  tags: ${{ steps.meta.outputs.tags }}
100
- labels: ${{ steps.meta.outputs.labels }}
100
+ labels: ${{ steps.meta.outputs.labels }}
package/README.md CHANGED
@@ -9,6 +9,8 @@
9
9
 
10
10
  `solid-server` lets you run a Solid server on top of the file-system. You can use it as a [command-line tool](https://github.com/solid/node-solid-server/blob/master/README.md#command-line-usage) (easy) or as a [library](https://github.com/solid/node-solid-server/blob/master/README.md#library-usage) (advanced).
11
11
 
12
+ The [solid test suite](https://github.com/nodeSolidServer/node-solid-server/blob/main/test/surface/run-solid-test-suite.sh) runs as part of GitHub Actions on this repository, ensuring that this server is always (to the best of our knowledge) fully spec compliant.
13
+
12
14
  ## Solid Features supported
13
15
  - [x] [Linked Data Platform](http://www.w3.org/TR/ldp/)
14
16
  - [x] [Web Access Control](http://www.w3.org/wiki/WebAccessControl)
@@ -28,7 +28,14 @@ class ACLChecker {
28
28
  constructor (resource, options = {}) {
29
29
  this.resource = resource
30
30
  this.resourceUrl = new URL(resource)
31
- this.agentOrigin = options.strictOrigin && options.agentOrigin ? rdf.sym(options.agentOrigin) : null
31
+ this.agentOrigin = null
32
+ try {
33
+ if (options.strictOrigin && options.agentOrigin) {
34
+ this.agentOrigin = rdf.sym(options.agentOrigin)
35
+ }
36
+ } catch (e) {
37
+ // noop
38
+ }
32
39
  this.fetch = options.fetch
33
40
  this.fetchGraph = options.fetchGraph
34
41
  this.trustedOrigins = options.strictOrigin && options.trustedOrigins ? options.trustedOrigins.map(trustedOrigin => rdf.sym(trustedOrigin)) : null
@@ -72,9 +72,10 @@ function allow (mode) {
72
72
  }
73
73
  }
74
74
 
75
- // check user is owner. Find owner from /.meta
76
- if (resourceUrl.endsWith('.acl') && userId === await ldp.getOwner(req.hostname)) return next()
77
-
75
+ // check if user is owner. Check isOwner from /.meta
76
+ try {
77
+ if (resourceUrl.endsWith('.acl') && (await ldp.isOwner(userId, req.hostname))) return next()
78
+ } catch (err) {}
78
79
  const error = req.authError || await req.acl.getError(userId, mode)
79
80
  debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`)
80
81
  next(error)
@@ -53,7 +53,11 @@ async function patchHandler (req, res, next) {
53
53
  ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile(
54
54
  { url: req, createIfNotExists: true, contentType: contentTypeForNew(req) }))
55
55
  // check if a folder with same name exists
56
- await ldp.checkItemName(req)
56
+ try {
57
+ await ldp.checkItemName(req)
58
+ } catch (err) {
59
+ return next(err)
60
+ }
57
61
  resourceExists = false
58
62
  }
59
63
  const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname })
@@ -143,8 +147,6 @@ async function checkPermission (request, patchObject, resourceExists) {
143
147
  // Now that we know the details of the patch,
144
148
  // we might need to perform additional checks.
145
149
  let modes = []
146
- // acl:default Write is required for create
147
- if (!resourceExists) modes = ['Write']
148
150
  const { acl, session: { userId } } = request
149
151
  // Read access is required for DELETE and WHERE.
150
152
  // If we would allows users without read access,
@@ -164,7 +166,7 @@ async function checkPermission (request, patchObject, resourceExists) {
164
166
  if (!allAllowed) {
165
167
  // check owner with Control
166
168
  const ldp = request.app.locals.ldp
167
- if (request.path.endsWith('.acl') && userId === await ldp.getOwner(request.hostname)) return Promise.resolve(patchObject)
169
+ if (request.path.endsWith('.acl') && await ldp.isOwner(userId, request.hostname)) return Promise.resolve(patchObject)
168
170
 
169
171
  const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode)))
170
172
  const error = errors.filter(error => !!error)
@@ -9,10 +9,17 @@ const { stringToStream } = require('../utils')
9
9
  async function handler (req, res, next) {
10
10
  debug(req.originalUrl)
11
11
  res.header('MS-Author-Via', 'SPARQL')
12
-
13
12
  const contentType = req.get('content-type')
13
+
14
+ // check if a folder or resource with same name exists
15
+ try {
16
+ const ldp = req.app.locals.ldp
17
+ await ldp.checkItemName(req)
18
+ } catch (e) {
19
+ return next(e)
20
+ }
14
21
  // 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
22
+ // TODO check that /profile/card is a minimal valid WebID card
16
23
  if (isAuxiliary(req) || req.originalUrl === '/profile/card') {
17
24
  if (contentType === 'text/turtle') {
18
25
  return bodyParser.text({ type: () => true })(req, res, () => putValidRdf(req, res, next))
@@ -21,17 +28,51 @@ async function handler (req, res, next) {
21
28
  return putStream(req, res, next)
22
29
  }
23
30
 
31
+ // Verifies whether the user is allowed to perform Append PUT on the target
32
+ async function checkPermission (request, resourceExists) {
33
+ // If no ACL object was passed down, assume permissions are okay.
34
+ if (!request.acl) return Promise.resolve()
35
+ // At this point, we already assume append access,
36
+ // we might need to perform additional checks.
37
+ let modes = []
38
+ // acl:default Write is required for PUT when Resource Exists
39
+ if (resourceExists) modes = ['Write']
40
+ // if (resourceExists && request.originalUrl.endsWith('.acl')) modes = ['Control']
41
+ const { acl, session: { userId } } = request
42
+
43
+ const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode, request.method, resourceExists)))
44
+ const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true)
45
+ if (!allAllowed) {
46
+ // check owner with Control
47
+ // const ldp = request.app.locals.ldp
48
+ // if (request.path.endsWith('.acl') && userId === await ldp.getOwner(request.hostname)) return Promise.resolve()
49
+
50
+ const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode)))
51
+ const error = errors.filter(error => !!error)
52
+ .reduce((prevErr, err) => prevErr.status > err.status ? prevErr : err, { status: 0 })
53
+ return Promise.reject(error)
54
+ }
55
+ return Promise.resolve()
56
+ }
57
+
24
58
  // TODO could be renamed as putResource (it now covers container and non-container)
25
59
  async function putStream (req, res, next, stream = req) {
26
60
  const ldp = req.app.locals.ldp
61
+ // try {
62
+ // Obtain details of the target resource
63
+ let resourceExists = true
64
+ try {
65
+ // First check if the file already exists
66
+ await ldp.resourceMapper.mapUrlToFile({ url: req })
67
+ } catch (err) {
68
+ resourceExists = false
69
+ }
27
70
  try {
28
- debug('test ' + req.get('content-type') + getContentType(req.headers))
71
+ if (!req.originalUrl.endsWith('.acl')) await checkPermission(req, resourceExists)
29
72
  await ldp.put(req, stream, getContentType(req.headers))
30
- debug('succeded putting the file/folder')
31
73
  res.sendStatus(201)
32
74
  return next()
33
75
  } catch (err) {
34
- debug('error putting the file/folder:' + err.message)
35
76
  err.message = 'Can\'t write file/folder: ' + err.message
36
77
  return next(err)
37
78
  }
package/lib/header.js CHANGED
@@ -128,7 +128,7 @@ async function addPermissions (req, res, next) {
128
128
  getPermissionsFor(acl, null, req),
129
129
  getPermissionsFor(acl, session.userId, req)
130
130
  ])
131
- if (resource.endsWith('.acl') && userPerms === '' && session.userId === await ldp.getOwner(req.hostname)) userPerms = 'control'
131
+ if (resource.endsWith('.acl') && userPerms === '' && await ldp.isOwner(session.userId, req.hostname)) userPerms = 'control'
132
132
  debug.ACL(`Permissions on ${resource} for ${session.userId || '(none)'}: ${userPerms}`)
133
133
  debug.ACL(`Permissions on ${resource} for public: ${publicPerms}`)
134
134
  res.set('WAC-Allow', `user="${userPerms}",public="${publicPerms}"`)
@@ -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
+ // allways 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) {
@@ -243,8 +219,11 @@ class LDP {
243
219
  'PUT request requires a content-type via the Content-Type header')
244
220
  }
245
221
 
246
- // check if a folder or file with same name exists
247
- await this.checkItemName(url)
222
+ // reject resource with percent-encoded $ extension
223
+ const dollarExtensionRegex = /%(?:24)\.[^%(?:24)]*$/ // /\$\.[^$]*$/
224
+ if ((url.url || url).match(dollarExtensionRegex)) {
225
+ throw error(400, 'Resource with a $.ext is not allowed by the server')
226
+ }
248
227
 
249
228
  // First check if we are above quota
250
229
  let isOverQuota
@@ -348,19 +327,30 @@ class LDP {
348
327
  } catch (err) { }
349
328
  }
350
329
 
330
+ // check if a document (or container) has the same name than a document (or container)
351
331
  async checkItemName (url) {
352
- let testName
353
- const itemUrl = (url.url || url)
332
+ let testName, testPath
333
+ const { hostname, pathname } = this.resourceMapper._parseUrl(url) // (url.url || url)
334
+ let itemUrl = this.resourceMapper.resolveUrl(hostname, pathname)
354
335
  const container = itemUrl.endsWith('/')
355
336
  try {
356
337
  const testUrl = container ? itemUrl.slice(0, -1) : itemUrl + '/'
357
338
  const { path: testPath } = await this.resourceMapper.mapUrlToFile({ url: testUrl })
358
- // testName = fs.lstatSync(testPath).isDirectory()
359
339
  testName = container ? fs.lstatSync(testPath).isFile() : fs.lstatSync(testPath).isDirectory()
360
- } catch (err) { testName = false }
361
-
340
+ } catch (err) {
341
+ testName = false
342
+
343
+ // item does not exist, check one level up the tree
344
+ if (itemUrl.endsWith('/')) itemUrl = itemUrl.substring(0, itemUrl.length - 1)
345
+ itemUrl = itemUrl.substring(0, itemUrl.lastIndexOf('/') + 1)
346
+ const { pathname } = this.resourceMapper._parseUrl(itemUrl) // (url.url || url)
347
+ // check not at root
348
+ if (pathname !== '/') {
349
+ return await this.checkItemName(itemUrl)
350
+ }
351
+ }
362
352
  if (testName) {
363
- throw error(200, 'Container and resource cannot have the same name in URI')
353
+ throw error(404, `${testPath}: Container and resource cannot have the same name in URI`)
364
354
  }
365
355
  }
366
356
 
@@ -437,7 +427,7 @@ class LDP {
437
427
  // this is a hack to replace solid:owner, using solid:account in /.meta to avoid NSS migration
438
428
  // this /.meta has no functionality in actual NSS
439
429
  // comment https://github.com/solid/node-solid-server/pull/1604#discussion_r652903546
440
- async getOwner (hostname) {
430
+ async isOwner (webId, hostname) {
441
431
  // const ldp = req.app.locals.ldp
442
432
  const rootUrl = this.resourceMapper.resolveUrl(hostname)
443
433
  let graph
@@ -445,8 +435,8 @@ class LDP {
445
435
  // TODO check for permission ?? Owner is a MUST
446
436
  graph = await this.getGraph(rootUrl + '/.meta')
447
437
  const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#')
448
- const owner = await graph.any(null, SOLID('account'), $rdf.sym(rootUrl + '/'))
449
- return owner.uri
438
+ const owner = await graph.statementsMatching($rdf.sym(webId), SOLID('account'), $rdf.sym(rootUrl + '/'))
439
+ return owner.length
450
440
  } catch (error) {
451
441
  throw new Error(`Failed to get owner from ${rootUrl}/.meta, got ` + error)
452
442
  }
@@ -589,17 +579,26 @@ class LDP {
589
579
  }
590
580
  }
591
581
 
592
- async getAvailableUrl (hostname, containerURI, { slug = uuid.v1(), extension }) {
582
+ async getAvailableUrl (hostname, containerURI, { slug = uuid.v1(), extension, container }) {
593
583
  let requestUrl = this.resourceMapper.resolveUrl(hostname, containerURI)
594
- requestUrl = requestUrl.replace(/\/*$/, '/')
584
+ requestUrl = requestUrl.replace(/\/*$/, '/') // ??? what for
595
585
 
596
- const { path: containerFilePath } = await this.resourceMapper.mapUrlToFile({ url: requestUrl })
597
- let fileName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
598
- if (await promisify(fs.exists)(utilPath.join(containerFilePath, fileName))) {
599
- fileName = `${uuid.v1()}-${fileName}`
600
- }
601
-
602
- return requestUrl + fileName
586
+ let itemName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
587
+ try {
588
+ // check 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 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
603
602
  }
604
603
 
605
604
  getTrustedOrigins (req) {
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.7",
4
+ "version": "5.7.9-alpha",
5
5
  "author": {
6
6
  "name": "Tim Berners-Lee",
7
7
  "email": "timbl@w3.org"
@@ -69,7 +69,7 @@
69
69
  "cached-path-relative": "^1.1.0",
70
70
  "camelize": "^1.0.1",
71
71
  "cheerio": "^1.0.0-rc.12",
72
- "colorette": "^2.0.19",
72
+ "colorette": "^2.0.20",
73
73
  "commander": "^8.3.0",
74
74
  "cors": "^2.8.5",
75
75
  "debug": "^4.3.4",
@@ -82,27 +82,27 @@
82
82
  "get-folder-size": "^2.0.1",
83
83
  "glob": "^7.2.3",
84
84
  "global-tunnel-ng": "^2.7.1",
85
- "handlebars": "^4.7.7",
85
+ "handlebars": "^4.7.8",
86
86
  "http-proxy-middleware": "^2.0.6",
87
- "inquirer": "^8.2.5",
87
+ "inquirer": "^8.2.6",
88
88
  "into-stream": "^6.0.0",
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.8",
92
+ "mashlib": "^1.8.9",
93
93
  "mime-types": "^2.1.35",
94
94
  "negotiator": "^0.6.3",
95
- "node-fetch": "^2.6.9",
95
+ "node-fetch": "^2.7.0",
96
96
  "node-forge": "^1.3.1",
97
97
  "node-mailer": "^0.1.1",
98
- "nodemailer": "^6.9.1",
98
+ "nodemailer": "^6.9.7",
99
99
  "oidc-op-express": "^0.0.3",
100
100
  "owasp-password-strength-test": "^1.3.0",
101
101
  "recursive-readdir": "^2.2.3",
102
102
  "request": "^2.88.2",
103
103
  "rimraf": "^3.0.2",
104
104
  "solid-auth-client": "^2.5.6",
105
- "solid-namespace": "^0.5.2",
105
+ "solid-namespace": "^0.5.3",
106
106
  "solid-ws": "^0.4.3",
107
107
  "text-encoder-lite": "^2.0.0",
108
108
  "the-big-username-blacklist": "^1.5.2",
@@ -110,26 +110,26 @@
110
110
  "urijs": "^1.19.11",
111
111
  "uuid": "^8.3.2",
112
112
  "valid-url": "^1.0.9",
113
- "validator": "^13.9.0",
113
+ "validator": "^13.11.0",
114
114
  "vhost": "^3.0.2"
115
115
  },
116
116
  "devDependencies": {
117
- "@solid/solid-auth-oidc": "^0.3.0",
118
- "chai": "^4.3.7",
117
+ "@solid/solid-auth-oidc": "0.3.0",
118
+ "chai": "^4.3.10",
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.3.0",
126
- "node-mocks-http": "^1.12.2",
124
+ "mocha": "^10.2.0",
125
+ "nock": "^13.4.0",
126
+ "node-mocks-http": "^1.14.0",
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.1119.0",
132
+ "snyk": "^1.1264.0",
133
133
  "standard": "16.0.4",
134
134
  "supertest": "^6.3.3",
135
135
  "turtle-validator": "1.1.1",