solid-server 5.6.9-beta

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.
Files changed (170) hide show
  1. package/.acl +10 -0
  2. package/.github/workflows/ci.yml +47 -0
  3. package/.nvmrc +1 -0
  4. package/.snyk +35 -0
  5. package/.well-known/.acl +15 -0
  6. package/CHANGELOG.md +198 -0
  7. package/CONTRIBUTING.md +139 -0
  8. package/CONTRIBUTORS.md +36 -0
  9. package/Dockerfile +22 -0
  10. package/LICENSE.md +23 -0
  11. package/README.md +453 -0
  12. package/bin/lib/cli-utils.js +85 -0
  13. package/bin/lib/cli.js +39 -0
  14. package/bin/lib/init.js +94 -0
  15. package/bin/lib/invalidUsernames.js +148 -0
  16. package/bin/lib/migrateLegacyResources.js +69 -0
  17. package/bin/lib/options.js +399 -0
  18. package/bin/lib/start.js +148 -0
  19. package/bin/lib/updateIndex.js +56 -0
  20. package/bin/solid +3 -0
  21. package/bin/solid-test +12 -0
  22. package/bin/solid.js +3 -0
  23. package/common/css/solid.css +58 -0
  24. package/common/fonts/glyphicons-halflings-regular.eot +0 -0
  25. package/common/fonts/glyphicons-halflings-regular.svg +288 -0
  26. package/common/fonts/glyphicons-halflings-regular.ttf +0 -0
  27. package/common/fonts/glyphicons-halflings-regular.woff +0 -0
  28. package/common/fonts/glyphicons-halflings-regular.woff2 +0 -0
  29. package/common/img/.gitkeep +0 -0
  30. package/common/js/auth-buttons.js +65 -0
  31. package/common/js/solid.js +454 -0
  32. package/common/well-known/security.txt +2 -0
  33. package/config/defaults.js +25 -0
  34. package/config/usernames-blacklist.json +4 -0
  35. package/config.json-default +22 -0
  36. package/default-templates/emails/delete-account.js +49 -0
  37. package/default-templates/emails/invalid-username.js +30 -0
  38. package/default-templates/emails/reset-password.js +49 -0
  39. package/default-templates/emails/welcome.js +39 -0
  40. package/default-templates/new-account/.acl +26 -0
  41. package/default-templates/new-account/.meta +5 -0
  42. package/default-templates/new-account/.meta.acl +25 -0
  43. package/default-templates/new-account/.well-known/.acl +19 -0
  44. package/default-templates/new-account/favicon.ico +0 -0
  45. package/default-templates/new-account/favicon.ico.acl +26 -0
  46. package/default-templates/new-account/inbox/.acl +26 -0
  47. package/default-templates/new-account/private/.acl +10 -0
  48. package/default-templates/new-account/profile/.acl +19 -0
  49. package/default-templates/new-account/profile/card$.ttl +25 -0
  50. package/default-templates/new-account/public/.acl +19 -0
  51. package/default-templates/new-account/robots.txt +3 -0
  52. package/default-templates/new-account/robots.txt.acl +26 -0
  53. package/default-templates/new-account/settings/.acl +20 -0
  54. package/default-templates/new-account/settings/prefs.ttl +15 -0
  55. package/default-templates/new-account/settings/privateTypeIndex.ttl +4 -0
  56. package/default-templates/new-account/settings/publicTypeIndex.ttl +4 -0
  57. package/default-templates/new-account/settings/publicTypeIndex.ttl.acl +25 -0
  58. package/default-templates/new-account/settings/serverSide.ttl.acl +13 -0
  59. package/default-templates/new-account/settings/serverSide.ttl.inactive +12 -0
  60. package/default-templates/server/.acl +10 -0
  61. package/default-templates/server/.well-known/.acl +15 -0
  62. package/default-templates/server/favicon.ico +0 -0
  63. package/default-templates/server/favicon.ico.acl +15 -0
  64. package/default-templates/server/index.html +55 -0
  65. package/default-templates/server/robots.txt +3 -0
  66. package/default-templates/server/robots.txt.acl +15 -0
  67. package/default-views/account/account-deleted.hbs +17 -0
  68. package/default-views/account/delete-confirm.hbs +51 -0
  69. package/default-views/account/delete-link-sent.hbs +17 -0
  70. package/default-views/account/delete.hbs +51 -0
  71. package/default-views/account/invalid-username.hbs +22 -0
  72. package/default-views/account/register-disabled.hbs +6 -0
  73. package/default-views/account/register-form.hbs +132 -0
  74. package/default-views/account/register.hbs +24 -0
  75. package/default-views/auth/auth-hidden-fields.hbs +8 -0
  76. package/default-views/auth/change-password.hbs +58 -0
  77. package/default-views/auth/goodbye.hbs +23 -0
  78. package/default-views/auth/login-required.hbs +34 -0
  79. package/default-views/auth/login-tls.hbs +11 -0
  80. package/default-views/auth/login-username-password.hbs +28 -0
  81. package/default-views/auth/login.hbs +55 -0
  82. package/default-views/auth/no-permission.hbs +29 -0
  83. package/default-views/auth/password-changed.hbs +27 -0
  84. package/default-views/auth/reset-link-sent.hbs +21 -0
  85. package/default-views/auth/reset-password.hbs +52 -0
  86. package/default-views/auth/sharing.hbs +49 -0
  87. package/default-views/shared/create-account.hbs +8 -0
  88. package/default-views/shared/error.hbs +5 -0
  89. package/docs/how-to-delete-your-account.md +56 -0
  90. package/docs/login-and-grant-access-to-application.md +32 -0
  91. package/examples/custom-error-handling.js +31 -0
  92. package/examples/ldp-with-webid.js +12 -0
  93. package/examples/simple-express-app.js +20 -0
  94. package/examples/simple-ldp-server.js +8 -0
  95. package/favicon.ico +0 -0
  96. package/favicon.ico.acl +15 -0
  97. package/index.html +48 -0
  98. package/index.js +3 -0
  99. package/lib/acl-checker.js +274 -0
  100. package/lib/api/accounts/user-accounts.js +88 -0
  101. package/lib/api/authn/force-user.js +21 -0
  102. package/lib/api/authn/index.js +5 -0
  103. package/lib/api/authn/webid-oidc.js +202 -0
  104. package/lib/api/authn/webid-tls.js +69 -0
  105. package/lib/api/index.js +6 -0
  106. package/lib/capability-discovery.js +54 -0
  107. package/lib/common/fs-utils.js +43 -0
  108. package/lib/common/template-utils.js +50 -0
  109. package/lib/common/user-utils.js +28 -0
  110. package/lib/create-app.js +322 -0
  111. package/lib/create-server.js +107 -0
  112. package/lib/debug.js +17 -0
  113. package/lib/handlers/allow.js +82 -0
  114. package/lib/handlers/auth-proxy.js +63 -0
  115. package/lib/handlers/copy.js +39 -0
  116. package/lib/handlers/cors-proxy.js +95 -0
  117. package/lib/handlers/delete.js +23 -0
  118. package/lib/handlers/error-pages.js +212 -0
  119. package/lib/handlers/get.js +219 -0
  120. package/lib/handlers/index.js +42 -0
  121. package/lib/handlers/options.js +33 -0
  122. package/lib/handlers/patch/n3-patch-parser.js +49 -0
  123. package/lib/handlers/patch/sparql-update-parser.js +16 -0
  124. package/lib/handlers/patch.js +203 -0
  125. package/lib/handlers/post.js +99 -0
  126. package/lib/handlers/put.js +56 -0
  127. package/lib/handlers/restrict-to-top-domain.js +13 -0
  128. package/lib/header.js +136 -0
  129. package/lib/http-error.js +34 -0
  130. package/lib/ldp-container.js +161 -0
  131. package/lib/ldp-copy.js +73 -0
  132. package/lib/ldp-middleware.js +32 -0
  133. package/lib/ldp.js +620 -0
  134. package/lib/lock.js +10 -0
  135. package/lib/metadata.js +10 -0
  136. package/lib/models/account-manager.js +603 -0
  137. package/lib/models/account-template.js +152 -0
  138. package/lib/models/authenticator.js +333 -0
  139. package/lib/models/oidc-manager.js +53 -0
  140. package/lib/models/solid-host.js +131 -0
  141. package/lib/models/user-account.js +112 -0
  142. package/lib/models/webid-tls-certificate.js +184 -0
  143. package/lib/payment-pointer-discovery.js +83 -0
  144. package/lib/requests/add-cert-request.js +138 -0
  145. package/lib/requests/auth-request.js +234 -0
  146. package/lib/requests/create-account-request.js +468 -0
  147. package/lib/requests/delete-account-confirm-request.js +170 -0
  148. package/lib/requests/delete-account-request.js +144 -0
  149. package/lib/requests/login-request.js +205 -0
  150. package/lib/requests/password-change-request.js +201 -0
  151. package/lib/requests/password-reset-email-request.js +199 -0
  152. package/lib/requests/sharing-request.js +259 -0
  153. package/lib/resource-mapper.js +198 -0
  154. package/lib/server-config.js +167 -0
  155. package/lib/services/blacklist-service.js +33 -0
  156. package/lib/services/email-service.js +162 -0
  157. package/lib/services/token-service.js +47 -0
  158. package/lib/utils.js +254 -0
  159. package/lib/webid/index.js +13 -0
  160. package/lib/webid/lib/get.js +27 -0
  161. package/lib/webid/lib/parse.js +12 -0
  162. package/lib/webid/tls/index.js +185 -0
  163. package/package.json +172 -0
  164. package/renovate.json +5 -0
  165. package/robots.txt +3 -0
  166. package/robots.txt.acl +15 -0
  167. package/static/account-recovery.html +78 -0
  168. package/static/popup-redirect.html +1 -0
  169. package/static/signup.html +108 -0
  170. package/static/signup.html.acl +14 -0
@@ -0,0 +1,95 @@
1
+ /* eslint-disable node/no-deprecated-api */
2
+
3
+ module.exports = addCorsProxyHandler
4
+
5
+ const { createProxyMiddleware } = require('http-proxy-middleware')
6
+ const cors = require('cors')
7
+ const debug = require('../debug')
8
+ const url = require('url')
9
+ const dns = require('dns')
10
+ const isIp = require('is-ip')
11
+ const ipRange = require('ip-range-check')
12
+ const validUrl = require('valid-url')
13
+
14
+ const CORS_SETTINGS = {
15
+ methods: 'GET',
16
+ exposedHeaders: 'Authorization, User, Location, Link, Vary, Last-Modified, Content-Length, Content-Location, MS-Author-Via, X-Powered-By',
17
+ maxAge: 1728000,
18
+ origin: true
19
+ }
20
+ const PROXY_SETTINGS = {
21
+ target: 'dynamic',
22
+ logLevel: 'silent',
23
+ changeOrigin: true,
24
+ followRedirects: true,
25
+ proxyTimeout: 10000,
26
+ router: req => req.destination.target,
27
+ pathRewrite: (path, req) => req.destination.path
28
+ }
29
+ // https://en.wikipedia.org/wiki/Reserved_IP_addresses
30
+ const RESERVED_IP_RANGES = [
31
+ '127.0.0.0/8', // loopback
32
+ '::1/128', // loopback
33
+ '0.0.0.0/8', // current network (only valid as source address)
34
+ '169.254.0.0/16', // link-local
35
+ '10.0.0.0/8', // private network
36
+ '100.64.0.0/10', // Shared Address Space
37
+ '172.16.0.0/12', // private network
38
+ '192.0.0.0/24', // IETF Protocol Assignments
39
+ '192.0.2.0/24', // TEST-NET-1, documentation and examples
40
+ '192.88.99.0/24', // IPv6 to IPv4 relay (includes 2002::/16)
41
+ '192.168.0.0/16', // private network
42
+ '198.18.0.0/15', // network benchmark tests
43
+ '198.51.100.0/24', // TEST-NET-2, documentation and examples
44
+ '203.0.113.0/24', // TEST-NET-3, documentation and examples
45
+ '224.0.0.0/4', // IP multicast (former Class D network)
46
+ '240.0.0.0/4', // reserved (former Class E network)
47
+ '255.255.255.255', // broadcast
48
+ '64:ff9b::/96', // IPv4/IPv6 translation (RFC 6052)
49
+ '100::/64', // discard prefix (RFC 6666)
50
+ '2001::/32', // Teredo tunneling
51
+ '2001:10::/28', // deprecated (previously ORCHID
52
+ '2001:20::/28', // ORCHIDv2
53
+ '2001:db8::/32', // documentation and example source code
54
+ '2002::/16', // 6to4
55
+ 'fc00::/7', // unique local address
56
+ 'fe80::/10', // link-local address
57
+ 'ff00::/8' // multicast
58
+ ]
59
+
60
+ // Adds a CORS proxy handler to the application on the given path
61
+ function addCorsProxyHandler (app, path) {
62
+ const corsHandler = cors(CORS_SETTINGS)
63
+ const proxyHandler = createProxyMiddleware(PROXY_SETTINGS)
64
+
65
+ debug.settings(`CORS proxy listening at ${path}?uri={uri}`)
66
+ app.get(path, extractProxyConfig, corsHandler, proxyHandler)
67
+ }
68
+
69
+ // Extracts proxy configuration parameters from the request
70
+ function extractProxyConfig (req, res, next) {
71
+ // Retrieve and validate the destination URL
72
+ const uri = req.query.uri
73
+ debug.settings(`Proxy request for ${uri}`)
74
+ if (!validUrl.isUri(uri)) {
75
+ return res.status(400).send(`Invalid URL passed: ${uri || '(none)'}`)
76
+ }
77
+
78
+ // Parse the URL and retrieve its host's IP address
79
+ const { protocol, host, hostname, path } = url.parse(uri)
80
+ if (isIp(hostname)) {
81
+ addProxyConfig(null, hostname)
82
+ } else {
83
+ dns.lookup(hostname, addProxyConfig)
84
+ }
85
+
86
+ // Verifies and adds the proxy configuration to the request
87
+ function addProxyConfig (error, hostAddress) {
88
+ // Ensure the host is not a local IP
89
+ if (error || RESERVED_IP_RANGES.some(r => ipRange(hostAddress, r))) {
90
+ return res.status(400).send(`Cannot proxy ${uri}`)
91
+ }
92
+ req.destination = { path, target: `${protocol}//${host}` }
93
+ next()
94
+ }
95
+ }
@@ -0,0 +1,23 @@
1
+ module.exports = handler
2
+
3
+ const debug = require('../debug').handlers
4
+
5
+ async function handler (req, res, next) {
6
+ debug('DELETE -- Request on' + req.originalUrl)
7
+
8
+ const ldp = req.app.locals.ldp
9
+ try {
10
+ await ldp.delete(req)
11
+ debug('DELETE -- Ok.')
12
+ res.sendStatus(200)
13
+ next()
14
+ } catch (err) {
15
+ debug('DELETE -- Failed to delete: ' + err)
16
+
17
+ // method DELETE not allowed
18
+ if (err.status === 405) {
19
+ res.set('allow', 'OPTIONS, HEAD, GET, PATCH, POST, PUT')
20
+ }
21
+ next(err)
22
+ }
23
+ }
@@ -0,0 +1,212 @@
1
+ const debug = require('../debug').server
2
+ const fs = require('fs')
3
+ const util = require('../utils')
4
+ const Auth = require('../api/authn')
5
+
6
+ /**
7
+ * Serves as a last-stop error handler for all other middleware.
8
+ *
9
+ * @param err {Error}
10
+ * @param req {IncomingRequest}
11
+ * @param res {ServerResponse}
12
+ * @param next {Function}
13
+ */
14
+ function handler (err, req, res, next) {
15
+ debug('Error page because of:', err)
16
+
17
+ const locals = req.app.locals
18
+ const authMethod = locals.authMethod
19
+ const ldp = locals.ldp
20
+
21
+ // If the user specifies this function,
22
+ // they can customize the error programmatically
23
+ if (ldp.errorHandler) {
24
+ debug('Using custom error handler')
25
+ return ldp.errorHandler(err, req, res, next)
26
+ }
27
+
28
+ const statusCode = statusCodeFor(err, req, authMethod)
29
+ switch (statusCode) {
30
+ case 401:
31
+ setAuthenticateHeader(req, res, err)
32
+ renderLoginRequired(req, res, err)
33
+ break
34
+ case 403:
35
+ renderNoPermission(req, res, err)
36
+ break
37
+ default:
38
+ if (ldp.noErrorPages) {
39
+ sendErrorResponse(statusCode, res, err)
40
+ } else {
41
+ sendErrorPage(statusCode, res, err, ldp)
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Returns the HTTP status code for a given request error.
48
+ *
49
+ * @param err {Error}
50
+ * @param req {IncomingRequest}
51
+ * @param authMethod {string}
52
+ *
53
+ * @returns {number}
54
+ */
55
+ function statusCodeFor (err, req, authMethod) {
56
+ let statusCode = err.status || err.statusCode || 500
57
+
58
+ if (authMethod === 'oidc') {
59
+ statusCode = Auth.oidc.statusCodeOverride(statusCode, req)
60
+ }
61
+
62
+ return statusCode
63
+ }
64
+
65
+ /**
66
+ * Dispatches the writing of the `WWW-Authenticate` response header (used for
67
+ * 401 Unauthorized responses).
68
+ *
69
+ * @param req {IncomingRequest}
70
+ * @param res {ServerResponse}
71
+ * @param err {Error}
72
+ */
73
+ function setAuthenticateHeader (req, res, err) {
74
+ const locals = req.app.locals
75
+ const authMethod = locals.authMethod
76
+
77
+ switch (authMethod) {
78
+ case 'oidc':
79
+ Auth.oidc.setAuthenticateHeader(req, res, err)
80
+ break
81
+ case 'tls':
82
+ Auth.tls.setAuthenticateHeader(req, res)
83
+ break
84
+ default:
85
+ break
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Sends the HTTP status code and error message in the response.
91
+ *
92
+ * @param statusCode {number}
93
+ * @param res {ServerResponse}
94
+ * @param err {Error}
95
+ */
96
+ function sendErrorResponse (statusCode, res, err) {
97
+ res.status(statusCode)
98
+ res.header('Content-Type', 'text/plain;charset=utf-8')
99
+ res.send(err.message + '\n')
100
+ }
101
+
102
+ /**
103
+ * Sends the HTTP status code and error message as a custom error page.
104
+ *
105
+ * @param statusCode {number}
106
+ * @param res {ServerResponse}
107
+ * @param err {Error}
108
+ * @param ldp {LDP}
109
+ */
110
+ function sendErrorPage (statusCode, res, err, ldp) {
111
+ const errorPage = ldp.errorPages + statusCode.toString() + '.html'
112
+
113
+ return new Promise((resolve) => {
114
+ fs.readFile(errorPage, 'utf8', (readErr, text) => {
115
+ if (readErr) {
116
+ // Fall back on plain error response
117
+ return resolve(sendErrorResponse(statusCode, res, err))
118
+ }
119
+
120
+ res.status(statusCode)
121
+ res.header('Content-Type', 'text/html')
122
+ res.send(text)
123
+ resolve()
124
+ })
125
+ })
126
+ }
127
+
128
+ /**
129
+ * Renders the databrowser
130
+ *
131
+ * @param req {IncomingRequest}
132
+ * @param res {ServerResponse}
133
+ */
134
+ function renderDataBrowser (req, res) {
135
+ res.set('Content-Type', 'text/html')
136
+ const ldp = req.app.locals.ldp
137
+ const defaultDataBrowser = require.resolve('mashlib/dist/databrowser.html')
138
+ const dataBrowserPath = ldp.dataBrowserPath === 'default' ? defaultDataBrowser : ldp.dataBrowserPath
139
+ debug(' sending data browser file: ' + dataBrowserPath)
140
+ const dataBrowserHtml = fs.readFileSync(dataBrowserPath, 'utf8')
141
+ // Note: This must be done instead of sendFile because the test suite doesn't accept 412 responses
142
+ res.set('content-type', 'text/html')
143
+ res.send(dataBrowserHtml)
144
+ }
145
+
146
+ /**
147
+ * Renders a 401 response explaining that a login is required.
148
+ *
149
+ * @param req {IncomingRequest}
150
+ * @param res {ServerResponse}
151
+ */
152
+ function renderLoginRequired (req, res, err) {
153
+ const currentUrl = util.fullUrlForReq(req)
154
+ debug(`Display login-required for ${currentUrl}`)
155
+ res.statusMessage = err.message
156
+ res.status(401)
157
+ if (req.accepts('html')) {
158
+ renderDataBrowser(req, res)
159
+ } else {
160
+ res.send('Not Authenticated')
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Renders a 403 response explaining that the user has no permission.
166
+ *
167
+ * @param req {IncomingRequest}
168
+ * @param res {ServerResponse}
169
+ */
170
+ function renderNoPermission (req, res, err) {
171
+ const currentUrl = util.fullUrlForReq(req)
172
+ debug(`Display no-permission for ${currentUrl}`)
173
+ res.statusMessage = err.message
174
+ res.status(403)
175
+ if (req.accepts('html')) {
176
+ renderDataBrowser(req, res)
177
+ } else {
178
+ res.send('Not Authorized')
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Returns a response body for redirecting browsers to a Select Provider /
184
+ * login workflow page. Uses either a JS location.href redirect or an
185
+ * http-equiv type html redirect for no-script conditions.
186
+ *
187
+ * @param url {string}
188
+ *
189
+ * @returns {string} Response body
190
+ */
191
+ function redirectBody (url) {
192
+ return `<!DOCTYPE HTML>
193
+ <meta charset="UTF-8">
194
+ <script>
195
+ window.location.href = "${url}" + encodeURIComponent(window.location.hash)
196
+ </script>
197
+ <noscript>
198
+ <meta http-equiv="refresh" content="0; url=${url}">
199
+ </noscript>
200
+ <title>Redirecting...</title>
201
+ If you are not redirected automatically,
202
+ follow the <a href='${url}'>link to login</a>
203
+ `
204
+ }
205
+
206
+ module.exports = {
207
+ handler,
208
+ redirectBody,
209
+ sendErrorPage,
210
+ sendErrorResponse,
211
+ setAuthenticateHeader
212
+ }
@@ -0,0 +1,219 @@
1
+ /* eslint-disable no-mixed-operators, no-async-promise-executor */
2
+
3
+ module.exports = handler
4
+
5
+ const fs = require('fs')
6
+ const glob = require('glob')
7
+ const _path = require('path')
8
+ const $rdf = require('rdflib')
9
+ const Negotiator = require('negotiator')
10
+ const mime = require('mime-types')
11
+
12
+ const debug = require('debug')('solid:get')
13
+ const debugGlob = require('debug')('solid:glob')
14
+ const allow = require('./allow')
15
+
16
+ const translate = require('../utils.js').translate
17
+ const error = require('../http-error')
18
+
19
+ const RDFs = require('../ldp').mimeTypesAsArray()
20
+
21
+ async function handler (req, res, next) {
22
+ const ldp = req.app.locals.ldp
23
+ const includeBody = req.method === 'GET'
24
+ const negotiator = new Negotiator(req)
25
+ const baseUri = ldp.resourceMapper.resolveUrl(req.hostname, req.path)
26
+ const path = res.locals.path || req.path
27
+ const requestedType = negotiator.mediaType()
28
+ const possibleRDFType = negotiator.mediaType(RDFs)
29
+
30
+ res.header('MS-Author-Via', 'SPARQL')
31
+
32
+ // Set live updates
33
+ if (ldp.live) {
34
+ res.header('Updates-Via', ldp.resourceMapper.resolveUrl(req.hostname).replace(/^http/, 'ws'))
35
+ }
36
+
37
+ debug(req.originalUrl + ' on ' + req.hostname)
38
+
39
+ const options = {
40
+ hostname: req.hostname,
41
+ path: path,
42
+ includeBody: includeBody,
43
+ possibleRDFType: possibleRDFType,
44
+ range: req.headers.range,
45
+ contentType: req.headers.accept
46
+ }
47
+
48
+ let ret
49
+ try {
50
+ ret = await ldp.get(options, req.accepts(['html', 'turtle', 'rdf+xml', 'n3', 'ld+json']) === 'html')
51
+ } catch (err) {
52
+ // use globHandler if magic is detected
53
+ if (err.status === 404 && glob.hasMagic(path)) {
54
+ debug('forwarding to glob request')
55
+ return globHandler(req, res, next)
56
+ } else {
57
+ debug(req.method + ' -- Error: ' + err.status + ' ' + err.message)
58
+ return next(err)
59
+ }
60
+ }
61
+
62
+ let stream
63
+ let contentType
64
+ let container
65
+ let contentRange
66
+ let chunksize
67
+
68
+ if (ret) {
69
+ stream = ret.stream
70
+ contentType = ret.contentType
71
+ container = ret.container
72
+ contentRange = ret.contentRange
73
+ chunksize = ret.chunksize
74
+ }
75
+
76
+ // Till here it must exist
77
+ if (!includeBody) {
78
+ debug('HEAD only')
79
+ res.setHeader('Content-Type', ret.contentType)
80
+ return res.status(200).send('OK')
81
+ }
82
+
83
+ // Handle dataBrowser
84
+ if (requestedType && requestedType.includes('text/html')) {
85
+ const { path: filename } = await ldp.resourceMapper.mapUrlToFile({ url: options })
86
+ const mimeTypeByExt = mime.lookup(_path.basename(filename))
87
+ const isHtmlResource = mimeTypeByExt && mimeTypeByExt.includes('html')
88
+ const useDataBrowser = ldp.dataBrowserPath && (
89
+ container ||
90
+ RDFs.includes(contentType) && !isHtmlResource && !ldp.suppressDataBrowser)
91
+
92
+ if (useDataBrowser) {
93
+ res.set('Content-Type', 'text/html')
94
+ const defaultDataBrowser = require.resolve('mashlib/dist/databrowser.html')
95
+ const dataBrowserPath = ldp.dataBrowserPath === 'default' ? defaultDataBrowser : ldp.dataBrowserPath
96
+ debug(' sending data browser file: ' + dataBrowserPath)
97
+ res.sendFile(dataBrowserPath)
98
+ return
99
+ } else if (stream) {
100
+ res.setHeader('Content-Type', contentType)
101
+ return stream.pipe(res)
102
+ }
103
+ }
104
+
105
+ // If request accepts the content-type we found
106
+ if (stream && negotiator.mediaType([contentType])) {
107
+ res.setHeader('Content-Type', contentType)
108
+ if (contentRange) {
109
+ const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize }
110
+ res.writeHead(206, headers)
111
+ return stream.pipe(res)
112
+ } else {
113
+ return stream.pipe(res)
114
+ }
115
+ }
116
+
117
+ // If it is not in our RDFs we can't even translate,
118
+ // Sorry, we can't help
119
+ if (!possibleRDFType) {
120
+ return next(error(406, 'Cannot serve requested type: ' + contentType))
121
+ }
122
+
123
+ try {
124
+ // Translate from the contentType found to the possibleRDFType desired
125
+ const data = await translate(stream, baseUri, contentType, possibleRDFType)
126
+ debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType)
127
+ res.setHeader('Content-Type', possibleRDFType)
128
+ res.send(data)
129
+ return next()
130
+ } catch (err) {
131
+ debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 500 + ' ' + err.message)
132
+ return next(error(500, 'Error translating between RDF formats'))
133
+ }
134
+ }
135
+
136
+ async function globHandler (req, res, next) {
137
+ const { ldp } = req.app.locals
138
+
139
+ // Ensure this is a glob for all files in a single folder
140
+ // https://github.com/solid/solid-spec/pull/148
141
+ const requestUrl = await ldp.resourceMapper.getRequestUrl(req)
142
+ if (!/^[^*]+\/\*$/.test(requestUrl)) {
143
+ return next(error(404, 'Unsupported glob pattern'))
144
+ }
145
+
146
+ // Extract the folder on the file system from the URL glob
147
+ const folderUrl = requestUrl.substr(0, requestUrl.length - 1)
148
+ const folderPath = (await ldp.resourceMapper.mapUrlToFile({ url: folderUrl, searchIndex: false })).path
149
+
150
+ const globOptions = {
151
+ noext: true,
152
+ nobrace: true,
153
+ nodir: true
154
+ }
155
+
156
+ glob(`${folderPath}*`, globOptions, async (err, matches) => {
157
+ if (err || matches.length === 0) {
158
+ debugGlob('No files matching the pattern')
159
+ return next(error(404, 'No files matching glob pattern'))
160
+ }
161
+
162
+ // Matches found
163
+ const globGraph = $rdf.graph()
164
+
165
+ debugGlob('found matches ' + matches)
166
+ await Promise.all(matches.map(match => new Promise(async (resolve, reject) => {
167
+ const urlData = await ldp.resourceMapper.mapFileToUrl({ path: match, hostname: req.hostname })
168
+ fs.readFile(match, { encoding: 'utf8' }, function (err, fileData) {
169
+ if (err) {
170
+ debugGlob('error ' + err)
171
+ return resolve()
172
+ }
173
+ // Files should be Turtle
174
+ if (urlData.contentType !== 'text/turtle') {
175
+ return resolve()
176
+ }
177
+ // The agent should have Read access to the file
178
+ hasReadPermissions(match, req, res, function (allowed) {
179
+ if (allowed) {
180
+ try {
181
+ $rdf.parse(fileData, globGraph, urlData.url, 'text/turtle')
182
+ } catch (parseErr) {
183
+ debugGlob(`error parsing ${match}: ${parseErr}`)
184
+ }
185
+ }
186
+ return resolve()
187
+ })
188
+ })
189
+ })))
190
+
191
+ const data = $rdf.serialize(undefined, globGraph, requestUrl, 'text/turtle')
192
+ // TODO this should be added as a middleware in the routes
193
+ res.setHeader('Content-Type', 'text/turtle')
194
+ debugGlob('returning turtle')
195
+
196
+ res.send(data)
197
+ next()
198
+ })
199
+ }
200
+
201
+ // TODO: get rid of this ugly hack that uses the Allow handler to check read permissions
202
+ function hasReadPermissions (file, req, res, callback) {
203
+ const ldp = req.app.locals.ldp
204
+
205
+ if (!ldp.webid) {
206
+ // FIXME: what is the rule that causes
207
+ // "Unexpected literal in error position of callback" in `npm run standard`?
208
+ // eslint-disable-next-line
209
+ return callback(true)
210
+ }
211
+
212
+ const root = ldp.resourceMapper.resolveFilePath(req.hostname)
213
+ const relativePath = '/' + _path.relative(root, file)
214
+ res.locals.path = relativePath
215
+ // FIXME: what is the rule that causes
216
+ // "Unexpected literal in error position of callback" in `npm run standard`?
217
+ // eslint-disable-next-line
218
+ allow('Read')(req, res, err => callback(!err))
219
+ }
@@ -0,0 +1,42 @@
1
+ /* eslint-disable node/no-deprecated-api */
2
+
3
+ module.exports = handler
4
+
5
+ const path = require('path')
6
+ const debug = require('debug')('solid:index')
7
+ const Negotiator = require('negotiator')
8
+ const url = require('url')
9
+ const URI = require('urijs')
10
+
11
+ async function handler (req, res, next) {
12
+ const indexFile = 'index.html'
13
+ const ldp = req.app.locals.ldp
14
+ const negotiator = new Negotiator(req)
15
+ const requestedType = negotiator.mediaType()
16
+
17
+ try {
18
+ const { path: filename } = await ldp.resourceMapper.mapUrlToFile({ url: req })
19
+
20
+ const stats = await ldp.stat(filename)
21
+ if (!stats.isDirectory()) {
22
+ return next()
23
+ }
24
+ // redirect to the right container if missing trailing /
25
+ if (req.path.lastIndexOf('/') !== req.path.length - 1) {
26
+ return res.redirect(301, URI.joinPaths(req.path, '/').toString())
27
+ }
28
+
29
+ if (requestedType && requestedType.indexOf('text/html') !== 0) {
30
+ return next()
31
+ }
32
+ debug('Looking for index in ' + req.path)
33
+
34
+ // Check if file exists in first place
35
+ await ldp.exists(req.hostname, path.join(req.path, indexFile))
36
+ res.locals.path = url.resolve(req.path, indexFile)
37
+ debug('Found an index for current path')
38
+ } catch (e) {
39
+ // Ignore errors
40
+ }
41
+ next()
42
+ }
@@ -0,0 +1,33 @@
1
+ /* eslint-disable node/no-deprecated-api */
2
+
3
+ const addLink = require('../header').addLink
4
+ const url = require('url')
5
+
6
+ module.exports = handler
7
+
8
+ function handler (req, res, next) {
9
+ linkServiceEndpoint(req, res)
10
+ linkAuthProvider(req, res)
11
+ linkSparqlEndpoint(res)
12
+
13
+ res.status(204)
14
+
15
+ next()
16
+ }
17
+
18
+ function linkAuthProvider (req, res) {
19
+ const locals = req.app.locals
20
+ if (locals.authMethod === 'oidc') {
21
+ const oidcProviderUri = locals.host.serverUri
22
+ addLink(res, oidcProviderUri, 'http://openid.net/specs/connect/1.0/issuer')
23
+ }
24
+ }
25
+
26
+ function linkServiceEndpoint (req, res) {
27
+ const serviceEndpoint = url.resolve(req.app.locals.ldp.resourceMapper.resolveUrl(req.hostname, req.path), '.well-known/solid')
28
+ addLink(res, serviceEndpoint, 'service')
29
+ }
30
+
31
+ function linkSparqlEndpoint (res) {
32
+ res.header('Accept-Patch', 'application/sparql-update')
33
+ }
@@ -0,0 +1,49 @@
1
+ // Parses a text/n3 patch
2
+
3
+ module.exports = parsePatchDocument
4
+
5
+ const $rdf = require('rdflib')
6
+ const error = require('../../http-error')
7
+
8
+ const PATCH_NS = 'http://www.w3.org/ns/solid/terms#'
9
+ const PREFIXES = `PREFIX solid: <${PATCH_NS}>\n`
10
+
11
+ // Parses the given N3 patch document
12
+ async function parsePatchDocument (targetURI, patchURI, patchText) {
13
+ // Parse the N3 document into triples
14
+ const patchGraph = $rdf.graph()
15
+ try {
16
+ $rdf.parse(patchText, patchGraph, patchURI, 'text/n3')
17
+ } catch (err) {
18
+ throw error(400, `Patch document syntax error: ${err}`)
19
+ }
20
+
21
+ // Query the N3 document for insertions and deletions
22
+ let firstResult
23
+ try {
24
+ firstResult = await queryForFirstResult(patchGraph, `${PREFIXES}
25
+ SELECT ?insert ?delete ?where WHERE {
26
+ ?patch solid:patches <${targetURI}>.
27
+ OPTIONAL { ?patch solid:inserts ?insert. }
28
+ OPTIONAL { ?patch solid:deletes ?delete. }
29
+ OPTIONAL { ?patch solid:where ?where. }
30
+ }`)
31
+ } catch (err) {
32
+ throw error(400, `No patch for ${targetURI} found.`, err)
33
+ }
34
+
35
+ // Return the insertions and deletions as an rdflib patch document
36
+ const { '?insert': insert, '?delete': deleted, '?where': where } = firstResult
37
+ if (!insert && !deleted) {
38
+ throw error(400, 'Patch should at least contain inserts or deletes.')
39
+ }
40
+ return { insert, delete: deleted, where }
41
+ }
42
+
43
+ // Queries the store with the given SPARQL query and returns the first result
44
+ function queryForFirstResult (store, sparql) {
45
+ return new Promise((resolve, reject) => {
46
+ const query = $rdf.SPARQLToQuery(sparql, false, store)
47
+ store.query(query, resolve, null, () => reject(new Error('No results.')))
48
+ })
49
+ }
@@ -0,0 +1,16 @@
1
+ // Parses an application/sparql-update patch
2
+
3
+ module.exports = parsePatchDocument
4
+
5
+ const $rdf = require('rdflib')
6
+ const error = require('../../http-error')
7
+
8
+ // Parses the given SPARQL UPDATE document
9
+ async function parsePatchDocument (targetURI, patchURI, patchText) {
10
+ const baseURI = patchURI.replace(/#.*/, '')
11
+ try {
12
+ return $rdf.sparqlUpdateParser(patchText, $rdf.graph(), baseURI)
13
+ } catch (err) {
14
+ throw error(400, `Patch document syntax error: ${err}`)
15
+ }
16
+ }