1- import debug from '../debug.mjs'
2- const debugServer = debug . server
3- import fs from 'fs'
4- import util from '../utils.mjs'
5- import * as Auth from '../api/authn/index.mjs'
6- import { createRequire } from 'module'
7- const require = createRequire ( import . meta. url )
8-
9- /**
10- * Serves as a last-stop error handler for all other middleware.
11- *
12- * @param err {Error}
13- * @param req {IncomingRequest}
14- * @param res {ServerResponse}
15- * @param next {Function}
16- */
17- export function handler ( err , req , res , next ) {
18- debugServer ( 'Error page because of:' , err )
19-
20- const locals = req . app . locals
21- const authMethod = locals . authMethod
22- const ldp = locals . ldp
23-
24- // If the user specifies this function,
25- // they can customize the error programmatically
26- if ( ldp . errorHandler ) {
27- debugServer ( 'Using custom error handler' )
28- return ldp . errorHandler ( err , req , res , next )
29- }
30-
31- const statusCode = statusCodeFor ( err , req , authMethod )
32- switch ( statusCode ) {
33- case 401 :
34- setAuthenticateHeader ( req , res , err )
35- renderLoginRequired ( req , res , err )
36- break
37- case 403 :
38- renderNoPermission ( req , res , err )
39- break
40- default :
41- if ( ldp . noErrorPages ) {
42- sendErrorResponse ( statusCode , res , err )
43- } else {
44- sendErrorPage ( statusCode , res , err , ldp )
45- }
46- }
47- }
48-
49- /**
50- * Returns the HTTP status code for a given request error.
51- *
52- * @param err {Error}
53- * @param req {IncomingRequest}
54- * @param authMethod {string}
55- *
56- * @returns {number }
57- */
58- function statusCodeFor ( err , req , authMethod ) {
59- let statusCode = err . status || err . statusCode || 500
60-
61- if ( authMethod === 'oidc' ) {
62- statusCode = Auth . oidc . statusCodeOverride ( statusCode , req )
63- }
64-
65- return statusCode
66- }
67-
68- /**
69- * Dispatches the writing of the `WWW-Authenticate` response header (used for
70- * 401 Unauthorized responses).
71- *
72- * @param req {IncomingRequest}
73- * @param res {ServerResponse}
74- * @param err {Error}
75- */
76- export function setAuthenticateHeader ( req , res , err ) {
77- const locals = req . app . locals
78- const authMethod = locals . authMethod
79-
80- switch ( authMethod ) {
81- case 'oidc' :
82- Auth . oidc . setAuthenticateHeader ( req , res , err )
83- break
84- case 'tls' :
85- Auth . tls . setAuthenticateHeader ( req , res )
86- break
87- default :
88- break
89- }
90- }
91-
92- /**
93- * Sends the HTTP status code and error message in the response.
94- *
95- * @param statusCode {number}
96- * @param res {ServerResponse}
97- * @param err {Error}
98- */
99- export function sendErrorResponse ( statusCode , res , err ) {
100- res . status ( statusCode )
101- res . header ( 'Content-Type' , 'text/plain;charset=utf-8' )
102- res . send ( err . message + '\n' )
103- }
104-
105- /**
106- * Sends the HTTP status code and error message as a custom error page.
107- *
108- * @param statusCode {number}
109- * @param res {ServerResponse}
110- * @param err {Error}
111- * @param ldp {LDP}
112- */
113- export function sendErrorPage ( statusCode , res , err , ldp ) {
114- const errorPage = ldp . errorPages + statusCode . toString ( ) + '.html'
115-
116- return new Promise ( ( resolve ) => {
117- fs . readFile ( errorPage , 'utf8' , ( readErr , text ) => {
118- if ( readErr ) {
119- // Fall back on plain error response
120- return resolve ( sendErrorResponse ( statusCode , res , err ) )
121- }
122-
123- res . status ( statusCode )
124- res . header ( 'Content-Type' , 'text/html' )
125- res . send ( text )
126- resolve ( )
127- } )
128- } )
129- }
130-
131- /**
132- * Renders the databrowser
133- *
134- * @param req {IncomingRequest}
135- * @param res {ServerResponse}
136- */
137- function renderDataBrowser ( req , res ) {
138- res . set ( 'Content-Type' , 'text/html' )
139- const ldp = req . app . locals . ldp
140-
141- const defaultDataBrowser = require . resolve ( 'mashlib/dist/databrowser.html' )
142-
143- const dataBrowserPath = ldp . dataBrowserPath === 'default' ? defaultDataBrowser : ldp . dataBrowserPath
144- debugServer ( ' sending data browser file: ' + dataBrowserPath )
145- const dataBrowserHtml = fs . readFileSync ( dataBrowserPath , 'utf8' )
146- // Note: This must be done instead of sendFile because the test suite doesn't accept 412 responses
147- res . set ( 'content-type' , 'text/html' )
148- res . send ( dataBrowserHtml )
149- }
150-
151- /**
152- * Renders a 401 response explaining that a login is required.
153- *
154- * @param req {IncomingRequest}
155- * @param res {ServerResponse}
156- */
157- function renderLoginRequired ( req , res , err ) {
158- const currentUrl = util . fullUrlForReq ( req )
159- debugServer ( `Display login-required for ${ currentUrl } ` )
160- res . statusMessage = err . message
161- res . status ( 401 )
162- if ( req . accepts ( 'html' ) ) {
163- renderDataBrowser ( req , res )
164- } else {
165- res . send ( 'Not Authenticated' )
166- }
167- }
168-
169- /**
170- * Renders a 403 response explaining that the user has no permission.
171- *
172- * @param req {IncomingRequest}
173- * @param res {ServerResponse}
174- */
175- function renderNoPermission ( req , res , err ) {
176- const currentUrl = util . fullUrlForReq ( req )
177- debugServer ( `Display no-permission for ${ currentUrl } ` )
178- res . statusMessage = err . message
179- res . status ( 403 )
180- if ( req . accepts ( 'html' ) ) {
181- renderDataBrowser ( req , res )
182- } else {
183- res . send ( 'Not Authorized' )
184- }
185- }
186-
187- /**
188- * Returns a response body for redirecting browsers to a Select Provider /
189- * login workflow page. Uses either a JS location.href redirect or an
190- * http-equiv type html redirect for no-script conditions.
191- *
192- * @param url {string}
193- *
194- * @returns {string } Response body
195- */
196- export function redirectBody ( url ) {
197- return `<!DOCTYPE HTML>
198- <meta charset="UTF-8">
199- <script>
200- window.location.href = "${ url } " + encodeURIComponent(window.location.hash)
201- </script>
202- <noscript>
203- <meta http-equiv="refresh" content="0; url=${ url } ">
204- </noscript>
205- <title>Redirecting...</title>
206- If you are not redirected automatically,
207- follow the <a href='${ url } '>link to login</a>
208- `
209- }
210-
211- export default {
212- handler,
213- redirectBody,
214- sendErrorPage,
215- sendErrorResponse,
216- setAuthenticateHeader
217- }
1+ import { server as debug } from '../debug.mjs'
2+ import fs from 'fs'
3+ import { fileURLToPath } from 'url'
4+ import * as util from '../utils.mjs'
5+ import Auth from '../api/authn/index.mjs'
6+
7+ function statusCodeFor ( err , req , authMethod ) {
8+ let statusCode = err . status || err . statusCode || 500
9+
10+ if ( authMethod === 'oidc' ) {
11+ statusCode = Auth . oidc . statusCodeOverride ( statusCode , req )
12+ }
13+
14+ return statusCode
15+ }
16+
17+ export function setAuthenticateHeader ( req , res , err ) {
18+ const locals = req . app . locals
19+ const authMethod = locals . authMethod
20+
21+ switch ( authMethod ) {
22+ case 'oidc' :
23+ Auth . oidc . setAuthenticateHeader ( req , res , err )
24+ break
25+ case 'tls' :
26+ Auth . tls . setAuthenticateHeader ( req , res )
27+ break
28+ default :
29+ break
30+ }
31+ }
32+
33+ export function sendErrorResponse ( statusCode , res , err ) {
34+ res . status ( statusCode )
35+ res . header ( 'Content-Type' , 'text/plain;charset=utf-8' )
36+ res . send ( err . message + '\n' )
37+ }
38+
39+ export function sendErrorPage ( statusCode , res , err , ldp ) {
40+ const errorPage = ldp . errorPages + statusCode . toString ( ) + '.html'
41+
42+ return new Promise ( ( resolve ) => {
43+ fs . readFile ( errorPage , 'utf8' , ( readErr , text ) => {
44+ if ( readErr ) {
45+ return resolve ( sendErrorResponse ( statusCode , res , err ) )
46+ }
47+
48+ res . status ( statusCode )
49+ res . header ( 'Content-Type' , 'text/html' )
50+ res . send ( text )
51+ resolve ( )
52+ } )
53+ } )
54+ }
55+
56+ function renderDataBrowser ( req , res ) {
57+ res . set ( 'Content-Type' , 'text/html' )
58+ const ldp = req . app . locals . ldp
59+ const defaultDataBrowser = import . meta. resolve ( 'mashlib/dist/databrowser.html' )
60+ let dataBrowserPath = ldp . dataBrowserPath === 'default' ? defaultDataBrowser : ldp . dataBrowserPath
61+ debug ( ' sending data browser file: ' + dataBrowserPath )
62+ // `import.meta.resolve` returns a file:// URL string; convert it to a
63+ // filesystem path for `fs.readFileSync` when necessary.
64+ if ( typeof dataBrowserPath === 'string' && dataBrowserPath . startsWith ( 'file://' ) ) {
65+ dataBrowserPath = fileURLToPath ( dataBrowserPath )
66+ }
67+ const dataBrowserHtml = fs . readFileSync ( dataBrowserPath , 'utf8' )
68+ res . set ( 'content-type' , 'text/html' )
69+ res . send ( dataBrowserHtml )
70+ }
71+
72+ export function handler ( err , req , res , next ) {
73+ debug ( 'Error page because of:' , err )
74+
75+ const locals = req . app . locals
76+ const authMethod = locals . authMethod
77+ const ldp = locals . ldp
78+
79+ if ( ldp . errorHandler ) {
80+ debug ( 'Using custom error handler' )
81+ return ldp . errorHandler ( err , req , res , next )
82+ }
83+
84+ const statusCode = statusCodeFor ( err , req , authMethod )
85+ switch ( statusCode ) {
86+ case 401 :
87+ setAuthenticateHeader ( req , res , err )
88+ renderLoginRequired ( req , res , err )
89+ break
90+ case 403 :
91+ renderNoPermission ( req , res , err )
92+ break
93+ default :
94+ if ( ldp . noErrorPages ) {
95+ sendErrorResponse ( statusCode , res , err )
96+ } else {
97+ sendErrorPage ( statusCode , res , err , ldp )
98+ }
99+ }
100+ }
101+
102+ function renderLoginRequired ( req , res , err ) {
103+ const currentUrl = util . fullUrlForReq ( req )
104+ debug ( `Display login-required for ${ currentUrl } ` )
105+ res . statusMessage = err . message
106+ res . status ( 401 )
107+ if ( req . accepts ( 'html' ) ) {
108+ renderDataBrowser ( req , res )
109+ } else {
110+ res . send ( 'Not Authenticated' )
111+ }
112+ }
113+
114+ function renderNoPermission ( req , res , err ) {
115+ const currentUrl = util . fullUrlForReq ( req )
116+ debug ( `Display no-permission for ${ currentUrl } ` )
117+ res . statusMessage = err . message
118+ res . status ( 403 )
119+ if ( req . accepts ( 'html' ) ) {
120+ renderDataBrowser ( req , res )
121+ } else {
122+ res . send ( 'Not Authorized' )
123+ }
124+ }
125+
126+ export function redirectBody ( url ) {
127+ return `<!DOCTYPE HTML>
128+ <meta charset="UTF-8">
129+ <script>
130+ window.location.href = "${ url } " + encodeURIComponent(window.location.hash)
131+ </script>
132+ <noscript>
133+ <meta http-equiv="refresh" content="0; url=${ url } ">
134+ </noscript>
135+ <title>Redirecting...</title>
136+ If you are not redirected automatically,
137+ follow the <a href='${ url } '>link to login</a>
138+ `
139+ }
140+
141+ export default {
142+ handler,
143+ redirectBody,
144+ sendErrorPage,
145+ sendErrorResponse,
146+ setAuthenticateHeader
147+ }
0 commit comments