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.
- package/.acl +10 -0
- package/.github/workflows/ci.yml +47 -0
- package/.nvmrc +1 -0
- package/.snyk +35 -0
- package/.well-known/.acl +15 -0
- package/CHANGELOG.md +198 -0
- package/CONTRIBUTING.md +139 -0
- package/CONTRIBUTORS.md +36 -0
- package/Dockerfile +22 -0
- package/LICENSE.md +23 -0
- package/README.md +453 -0
- package/bin/lib/cli-utils.js +85 -0
- package/bin/lib/cli.js +39 -0
- package/bin/lib/init.js +94 -0
- package/bin/lib/invalidUsernames.js +148 -0
- package/bin/lib/migrateLegacyResources.js +69 -0
- package/bin/lib/options.js +399 -0
- package/bin/lib/start.js +148 -0
- package/bin/lib/updateIndex.js +56 -0
- package/bin/solid +3 -0
- package/bin/solid-test +12 -0
- package/bin/solid.js +3 -0
- package/common/css/solid.css +58 -0
- package/common/fonts/glyphicons-halflings-regular.eot +0 -0
- package/common/fonts/glyphicons-halflings-regular.svg +288 -0
- package/common/fonts/glyphicons-halflings-regular.ttf +0 -0
- package/common/fonts/glyphicons-halflings-regular.woff +0 -0
- package/common/fonts/glyphicons-halflings-regular.woff2 +0 -0
- package/common/img/.gitkeep +0 -0
- package/common/js/auth-buttons.js +65 -0
- package/common/js/solid.js +454 -0
- package/common/well-known/security.txt +2 -0
- package/config/defaults.js +25 -0
- package/config/usernames-blacklist.json +4 -0
- package/config.json-default +22 -0
- package/default-templates/emails/delete-account.js +49 -0
- package/default-templates/emails/invalid-username.js +30 -0
- package/default-templates/emails/reset-password.js +49 -0
- package/default-templates/emails/welcome.js +39 -0
- package/default-templates/new-account/.acl +26 -0
- package/default-templates/new-account/.meta +5 -0
- package/default-templates/new-account/.meta.acl +25 -0
- package/default-templates/new-account/.well-known/.acl +19 -0
- package/default-templates/new-account/favicon.ico +0 -0
- package/default-templates/new-account/favicon.ico.acl +26 -0
- package/default-templates/new-account/inbox/.acl +26 -0
- package/default-templates/new-account/private/.acl +10 -0
- package/default-templates/new-account/profile/.acl +19 -0
- package/default-templates/new-account/profile/card$.ttl +25 -0
- package/default-templates/new-account/public/.acl +19 -0
- package/default-templates/new-account/robots.txt +3 -0
- package/default-templates/new-account/robots.txt.acl +26 -0
- package/default-templates/new-account/settings/.acl +20 -0
- package/default-templates/new-account/settings/prefs.ttl +15 -0
- package/default-templates/new-account/settings/privateTypeIndex.ttl +4 -0
- package/default-templates/new-account/settings/publicTypeIndex.ttl +4 -0
- package/default-templates/new-account/settings/publicTypeIndex.ttl.acl +25 -0
- package/default-templates/new-account/settings/serverSide.ttl.acl +13 -0
- package/default-templates/new-account/settings/serverSide.ttl.inactive +12 -0
- package/default-templates/server/.acl +10 -0
- package/default-templates/server/.well-known/.acl +15 -0
- package/default-templates/server/favicon.ico +0 -0
- package/default-templates/server/favicon.ico.acl +15 -0
- package/default-templates/server/index.html +55 -0
- package/default-templates/server/robots.txt +3 -0
- package/default-templates/server/robots.txt.acl +15 -0
- package/default-views/account/account-deleted.hbs +17 -0
- package/default-views/account/delete-confirm.hbs +51 -0
- package/default-views/account/delete-link-sent.hbs +17 -0
- package/default-views/account/delete.hbs +51 -0
- package/default-views/account/invalid-username.hbs +22 -0
- package/default-views/account/register-disabled.hbs +6 -0
- package/default-views/account/register-form.hbs +132 -0
- package/default-views/account/register.hbs +24 -0
- package/default-views/auth/auth-hidden-fields.hbs +8 -0
- package/default-views/auth/change-password.hbs +58 -0
- package/default-views/auth/goodbye.hbs +23 -0
- package/default-views/auth/login-required.hbs +34 -0
- package/default-views/auth/login-tls.hbs +11 -0
- package/default-views/auth/login-username-password.hbs +28 -0
- package/default-views/auth/login.hbs +55 -0
- package/default-views/auth/no-permission.hbs +29 -0
- package/default-views/auth/password-changed.hbs +27 -0
- package/default-views/auth/reset-link-sent.hbs +21 -0
- package/default-views/auth/reset-password.hbs +52 -0
- package/default-views/auth/sharing.hbs +49 -0
- package/default-views/shared/create-account.hbs +8 -0
- package/default-views/shared/error.hbs +5 -0
- package/docs/how-to-delete-your-account.md +56 -0
- package/docs/login-and-grant-access-to-application.md +32 -0
- package/examples/custom-error-handling.js +31 -0
- package/examples/ldp-with-webid.js +12 -0
- package/examples/simple-express-app.js +20 -0
- package/examples/simple-ldp-server.js +8 -0
- package/favicon.ico +0 -0
- package/favicon.ico.acl +15 -0
- package/index.html +48 -0
- package/index.js +3 -0
- package/lib/acl-checker.js +274 -0
- package/lib/api/accounts/user-accounts.js +88 -0
- package/lib/api/authn/force-user.js +21 -0
- package/lib/api/authn/index.js +5 -0
- package/lib/api/authn/webid-oidc.js +202 -0
- package/lib/api/authn/webid-tls.js +69 -0
- package/lib/api/index.js +6 -0
- package/lib/capability-discovery.js +54 -0
- package/lib/common/fs-utils.js +43 -0
- package/lib/common/template-utils.js +50 -0
- package/lib/common/user-utils.js +28 -0
- package/lib/create-app.js +322 -0
- package/lib/create-server.js +107 -0
- package/lib/debug.js +17 -0
- package/lib/handlers/allow.js +82 -0
- package/lib/handlers/auth-proxy.js +63 -0
- package/lib/handlers/copy.js +39 -0
- package/lib/handlers/cors-proxy.js +95 -0
- package/lib/handlers/delete.js +23 -0
- package/lib/handlers/error-pages.js +212 -0
- package/lib/handlers/get.js +219 -0
- package/lib/handlers/index.js +42 -0
- package/lib/handlers/options.js +33 -0
- package/lib/handlers/patch/n3-patch-parser.js +49 -0
- package/lib/handlers/patch/sparql-update-parser.js +16 -0
- package/lib/handlers/patch.js +203 -0
- package/lib/handlers/post.js +99 -0
- package/lib/handlers/put.js +56 -0
- package/lib/handlers/restrict-to-top-domain.js +13 -0
- package/lib/header.js +136 -0
- package/lib/http-error.js +34 -0
- package/lib/ldp-container.js +161 -0
- package/lib/ldp-copy.js +73 -0
- package/lib/ldp-middleware.js +32 -0
- package/lib/ldp.js +620 -0
- package/lib/lock.js +10 -0
- package/lib/metadata.js +10 -0
- package/lib/models/account-manager.js +603 -0
- package/lib/models/account-template.js +152 -0
- package/lib/models/authenticator.js +333 -0
- package/lib/models/oidc-manager.js +53 -0
- package/lib/models/solid-host.js +131 -0
- package/lib/models/user-account.js +112 -0
- package/lib/models/webid-tls-certificate.js +184 -0
- package/lib/payment-pointer-discovery.js +83 -0
- package/lib/requests/add-cert-request.js +138 -0
- package/lib/requests/auth-request.js +234 -0
- package/lib/requests/create-account-request.js +468 -0
- package/lib/requests/delete-account-confirm-request.js +170 -0
- package/lib/requests/delete-account-request.js +144 -0
- package/lib/requests/login-request.js +205 -0
- package/lib/requests/password-change-request.js +201 -0
- package/lib/requests/password-reset-email-request.js +199 -0
- package/lib/requests/sharing-request.js +259 -0
- package/lib/resource-mapper.js +198 -0
- package/lib/server-config.js +167 -0
- package/lib/services/blacklist-service.js +33 -0
- package/lib/services/email-service.js +162 -0
- package/lib/services/token-service.js +47 -0
- package/lib/utils.js +254 -0
- package/lib/webid/index.js +13 -0
- package/lib/webid/lib/get.js +27 -0
- package/lib/webid/lib/parse.js +12 -0
- package/lib/webid/tls/index.js +185 -0
- package/package.json +172 -0
- package/renovate.json +5 -0
- package/robots.txt +3 -0
- package/robots.txt.acl +15 -0
- package/static/account-recovery.html +78 -0
- package/static/popup-redirect.html +1 -0
- package/static/signup.html +108 -0
- 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
|
+
}
|