Use Problem object internally instead of Server.die() and Site.die()
authorNick Downing <nick@ndcode.org>
Tue, 25 Jan 2022 22:52:34 +0000 (09:52 +1100)
committerNick Downing <nick@ndcode.org>
Tue, 25 Jan 2022 23:51:58 +0000 (10:51 +1100)
Problem.mjs [new file with mode: 0644]
Server.mjs
Site.mjs
index.mjs

diff --git a/Problem.mjs b/Problem.mjs
new file mode 100644 (file)
index 0000000..9b2fd7e
--- /dev/null
@@ -0,0 +1,23 @@
+class Problem {
+  constructor(title, detail, status) {
+    this.title = title
+    this.detail = detail
+    this.status = status
+  }
+
+  // note: Javascript errors return status 400 (Bad request) in the client
+  // version of Problem, 500 (Internal server error) in the server version
+  static from(error) {
+    return (
+      error instanceof Problem ?
+        error :
+        new Problem(
+          'Internal server error',
+          (error.stack || error.message),
+          500
+        )
+    )
+  }
+}
+
+export default Problem
index baf64a3..2abad5a 100644 (file)
@@ -23,6 +23,7 @@
 
 import JSTCache from '@ndcode/jst_cache'
 import Listener from './Listener.mjs'
+import Problem from './Problem.mjs'
 import Resources from './Resources.mjs'
 import Site from './Site.mjs'
 import assert from 'assert'
@@ -197,28 +198,6 @@ class Server {
     response.end(data)
   }
 
-  die(response, pathname, message) {
-    console.log(message)
-    this.serve_internal(
-      response,
-      404,
-      'text/html; charset=utf-8',
-      Buffer.from(
-        `<html>
-  <head>
-    <meta http-equiv="content-type" content="text/html;charset=utf-8">
-    <title>404 Not Found</title>
-  </head>
-  <body style="font-family: sans-serif, serif, monospace; font-size: 16px;">
-    <h2>404 Not Found</h2>
-    <p>The document ${pathname} was not found.</p>
-  </body>
-</html>
-`
-      )
-    )
-  }
-
   redirect(response, location, message) {
     console.log(message)
     response.statusCode = 301
@@ -233,34 +212,35 @@ class Server {
     <meta http-equiv="content-type" content="text/html;charset=utf-8">
     <title>301 Moved Permanently</title>
   </head>
-  <body style="font-family: sans-serif, serif, monospace; font-size: 16px;">
+  <body style="font-family: sans-serif, serif, monospace; font-size: 20px;">
     <h2>301 Moved Permanently</h2>
-    <p>The document has moved <a href="${location}">here</a>.</p>
+    The document has moved <a href="${location}">here</a>.
   </body>
 </html>
-`
+`,
+        'utf-8'
       )
     )
   }
 
   async respond(request, response, protocol) {
+    let parsed_url = url.parse(
+      protocol + '//' + (request.headers.host || 'localhost') + request.url,
+      true
+    )
+    //console.log('parsed_url', parsed_url)
     try {
-      let parsed_url = url.parse(
-        protocol + '//' + (request.headers.host || 'localhost') + request.url,
-        true
-      )
-      //console.log('parsed_url', parsed_url)
-
       if (
-        !Object.prototype.hasOwnProperty.call(this.options.hosts, parsed_url.hostname)
-      ) {
-        this.die(
-          response,
-          parsed_url.pathname,
-          'nonexistent site: ' + parsed_url.hostname
+        !Object.prototype.hasOwnProperty.call(
+          this.options.hosts,
+          parsed_url.hostname
+        )
+      )
+        throw new Problem(
+          'Not found',
+          `Site "${parsed_url.hostname}" not found.`,
+          404
         )
-        return
-      }
 
       let host = this.options.hosts[parsed_url.hostname]
       switch (host.type) {
@@ -288,14 +268,28 @@ class Server {
         assert(false)
       }
     }
-    catch (err) {
-      let message = (err.stack || err.message).toString()
-      console.error(message)
+    catch (error) {
+      let problem = Problem.from(error)
+      console.log(`${parsed_url.host} ${problem.status} ${problem.title}`)
+      console.log(`  ${problem.detail}`)
       this.serve_internal(
         response,
-        500,
+        problem.status,
         'text/html; charset=utf-8',
-        Buffer.from(`<html><body><pre>${message}</pre></body></html>`)
+        Buffer.from(
+          `<html>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8">
+    <title>${problem.status} ${problem.title}</title>
+  </head>
+  <body style="font-family: sans-serif, serif, monospace; font-size: 20px;">
+    <h2>${problem.status} ${problem.title}</h2>
+    ${problem.detail.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('\n', '<br>')}
+  </body>
+</html>
+`,
+          'utf-8'
+        )
       )
     }
   }
index 8b267de..0820eaf 100644 (file)
--- a/Site.mjs
+++ b/Site.mjs
@@ -30,6 +30,7 @@ import MinCSSCache from '@ndcode/min_css_cache'
 import MinJSCache from '@ndcode/min_js_cache'
 import MinHTMLCache from '@ndcode/min_html_cache'
 import MinSVGCache from '@ndcode/min_svg_cache'
+import Problem from './Problem.mjs'
 import Resources from './Resources.mjs'
 import SassCSSCache from '@ndcode/sass_css_cache'
 import TextCache from '@ndcode/text_cache'
@@ -252,29 +253,6 @@ class Site {
     this.serve_internal(env.response, status, env.mime_type, env.caching, data)
   }
 
-  die(env, message) {
-    console.log(`${env.parsed_url.host} ${message}`)
-    this.serve_internal(
-      env.response,
-      404,
-      this.options.mime_types['.html'],
-      false,
-      Buffer.from(
-        `<html>
-  <head>
-    <meta http-equiv="content-type" content="text/html;charset=utf-8">
-    <title>404 Not Found</title>
-  </head>
-  <body style="font-family: sans-serif, serif, monospace; font-size: 16px;">
-    <h2>404 Not Found</h2>
-    <p>The document ${env.parsed_url.pathname} was not found.</p>
-  </body>
-</html>
-`
-      )
-    )
-  }
-
   redirect(env, pathname, message) {
     console.log(
       `${env.parsed_url.host} redirecting ${env.parsed_url.pathname} to ${pathname}`
@@ -293,12 +271,13 @@ class Site {
     <meta http-equiv="content-type" content="text/html;charset=utf-8">
     <title>301 Moved Permanently</title>
   </head>
-  <body style="font-family: sans-serif, serif, monospace; font-size: 16px;">
+  <body style="font-family: sans-serif, serif, monospace; font-size: 20px;">
     <h2>301 Moved Permanently</h2>
-    <p>The document has moved <a href="${location}">here</a>.</p>
+    The document has moved <a href="${location}">here</a>.
   </body>
 </html>
-`
+`,
+        'utf-8'
       )
     )
   }
@@ -609,26 +588,22 @@ class Site {
   }
 
   async serve_file(env, pathname) {
-    //console.log(`serve_file ${pathname}`)
-    if (await this.serve_jst(env, pathname + '.jst'))
-      return
-    if (await this.serve_less_css(env, pathname + '.less'))
-      return
-    if (await this.serve_min_css(env, pathname + '.min'))
-      return
-    if (await this.serve_min_html(env, pathname + '.min'))
-      return
-    if (await this.serve_min_js(env, pathname + '.min'))
-      return
-    if (await this.serve_min_svg(env, pathname + '.min'))
-      return
-    if (await this.serve_sass_css(env, pathname + '.sass'))
-      return
-    if (await this.serve_sass_css(env, pathname + '.scss'))
-      return
-    if (await this.serve_fs(env, pathname))
-      return
-    this.die(env, `file not found ${env.parsed_url.pathname}`)
+    if (
+      !await this.serve_jst(env, pathname + '.jst') &&
+        !await this.serve_less_css(env, pathname + '.less') &&
+        !await this.serve_min_css(env, pathname + '.min') &&
+        !await this.serve_min_html(env, pathname + '.min') &&
+        !await this.serve_min_js(env, pathname + '.min') &&
+        !await this.serve_min_svg(env, pathname + '.min') &&
+        !await this.serve_sass_css(env, pathname + '.sass') &&
+        !await this.serve_sass_css(env, pathname + '.scss') &&
+        !await this.serve_fs(env, pathname)
+    )
+      throw new Problem(
+        'Not found',
+        `File "${pathname}" not found.`,
+        404
+      )
   }
 
   async serve_dir(env, pathname, components) {
@@ -639,21 +614,21 @@ class Site {
     try {
       stats = await fsPromises.stat(pathname)
     }
-    catch (err) {
-      if (!(err instanceof Error) || err.code !== 'ENOENT')
-        throw err
-      this.die(env, `directory not found ${pathname}`)
-      return
-    }
-    if (!stats.isDirectory()) {
-      this.die(
-        env,
-        components.length > 1 ?
-          `not directory ${pathname}` :
-          `unknown file type in ${env.parsed_url.pathname}`
+    catch (error) {
+      if (!(error instanceof Error) || error.code !== 'ENOENT')
+        throw error
+      throw new Problem(
+        'Not found',
+        `Directory "${pathname}" not found.`,
+        404
       )
-      return
     }
+    if (!stats.isDirectory())
+      throw new Problem(
+        'Not found',
+        `Path "${pathname}" not directory.`,
+        404
+      )
     return /*await*/ this.serve_path(env, pathname, components)
   }
 
@@ -667,20 +642,25 @@ class Site {
 
     if (components[0].length === 0) {
       if (components.length > 1)
-        this.die(env, `empty directory name in ${env.parsed_url.pathname}`)
-      else
-        // directory with trailing slash
-        this.redirect(env, env.parsed_url.pathname + 'index.html')
+        throw new Problem(
+          'Not found',
+          `Path "${pathname}" followed by empty directory name.`,
+          404
+        )
+      // directory with trailing slash
+      this.redirect(env, env.parsed_url.pathname + 'index.html')
       return
     }
 
     if (
       components[0].charAt(0) === '.' ||
       components[0].charAt(0) === '_'
-    ) {
-      this.die(env, `bad component "${components[0]}" in ${env.parsed_url.pathname}`)
-      return
-    }
+    )
+      throw new Problem(
+        'Not found',
+        `Path "${pathname}" followed by bad component "${components[0]}".`,
+        404
+      )
 
     let i = components[0].lastIndexOf('.')
     if (i === -1)
@@ -692,10 +672,12 @@ class Site {
       extension.length !== 0 &&
       Object.prototype.hasOwnProperty.call(this.options.mime_types, extension)
     ) {
-      if (components.length > 1) {
-        this.die(env, `non-directory extension "${extension}" in ${env.parsed_url.pathname}`)
-        return
-      }
+      if (components.length > 1)
+        throw new Problem(
+          'Not found',
+          `Directory "${pathname}" has non-directory extension "${extension}".`,
+          404
+        )
       return /*await*/ this.serve_file(env, pathname)
     }
     return /*await*/ this.serve_dir(env, pathname, components.slice(1))
@@ -732,10 +714,12 @@ class Site {
       // build path, ensuring that remaining components are safe
       /*let*/ pathname = `${this.options.certbot_webroot}/.well-known`
       for (let i = 1; i < components.length; ++i) {
-        if (components[i].charAt(0) == '.') {
-          this.die(env, `bad component "${components[i]}" in ${env.parsed_url.pathname}`)
-          return
-        }
+        if (components[i].charAt(0) == '.')
+          throw new Problem(
+            'Not found',
+            `Path "${pathname}" followed by bad component "${components[i]}".`,
+            404
+          )
         pathname = `${pathname}/${components[i]}`
       }
 
index 6ddd31a..c3b6b01 100644 (file)
--- a/index.mjs
+++ b/index.mjs
@@ -1,5 +1,6 @@
+import Problem from './Problem.mjs'
 import Resources from './Resources.mjs'
 import Server from './Server.mjs'
 import Site from './Site.mjs'
 
-export default {Resources, Server, Site}
+export default {Problem, Resources, Server, Site}