Completely rewrite the URL parsing and file serving code, removes the need for the...
authorNick Downing <downing.nick@gmail.com>
Sat, 20 Oct 2018 13:33:50 +0000 (00:33 +1100)
committerNick Downing <downing.nick@gmail.com>
Sat, 20 Oct 2018 13:43:29 +0000 (00:43 +1100)
.gitignore
Site.js
config.js
config/mime_types.json
server.js

index d5cdf20..5add944 100644 (file)
@@ -1,2 +1,2 @@
 /node_modules
-/yarn-lock.json
+/yarn.lock
diff --git a/Site.js b/Site.js
index dbff5c3..a1709a2 100644 (file)
--- a/Site.js
+++ b/Site.js
@@ -26,200 +26,13 @@ let Site = function(root) {
   this.root = root
 }
 
-Site.prototype.respond = async function(request, response, parsed_url) {
-  let path = parsed_url.pathname.split('/')
-
-  // path must begin with /
-  if (path.length === 0 || path[0].length) {
-    server.die(response)
-    return
-  }
-
-  // path elements must be findable in the file system (thus can't be empty)
-  let dir_name = ''
-  let dir_name_is_pub = false
-  for (let i = 1; i < path.length - 1; ++i) {
-    dir_name += '/' + path[i]
-    if (path[i].length === 0 || path[i].charAt(0) === '.') {
-      console.log(parsed_url.host, 'bad path component', dir_name)
-      server.die(response)
-      return
-    }
-    let stats
-    try {
-      stats = await fs_stat(this.root + dir_name)
-    }
-    catch (err) {
-      if (err.code !== 'ENOENT')
-        throw err
-      if (!dir_name_is_pub) {
-        let temp = dir_name + '.pub'
-        try {
-          stats = await fs_stat(this.root + temp)
-          dir_name = temp
-          dir_name_is_pub = true
-        }
-        catch (err2) {
-          if (err2.code !== 'ENOENT')
-            throw err2
-          console.log(parsed_url.host, 'directory not found', dir_name)
-          server.die(response)
-          return
-        }
-      }
-      if (!stats.isDirectory()) {
-        console.log(parsed_url.host, 'not directory', dir_name)
-        server.die(response)
-        return
-      }
-    }
-  }
-
-  file_name = path[path.length - 1]
-  if (file_name === '') {
-    path[path.length - 1] = 'index.html'
-    path = path.join('/')
-    console.log(parsed_url.host, 'server.redirecting', parsed_url.pathname, 'to', path)
-    server.redirect(response, path + (parsed_url.search || ''))
-    return
-  }
-  let page = path.join('/')
-
-  let temp = file_name.lastIndexOf('.')
-  let file_type = temp === -1 ? '' : file_name.substring(temp + 1)
-  let mime_type =
-    Object.prototype.hasOwnProperty.call(config.mime_types, file_type) ?
-    config.mime_types[file_type] :
-    config.mime_type_default
-
-  if (dir_name_is_pub) {
-    temp = this.root + dir_name + '/' + file_name
-    try {
-      let data = await fs_readFile(temp)
-      console.log(
-        parsed_url.host,
-        'serving',
-        temp,
-        'length',
-        data.length,
-        'from pub'
-      )
-      server.serve(response, 200, mime_type, data)
-      return
-    }
-    catch (err) {
-      if (err.code !== 'ENOENT')
-        throw err
-    }
-  }
-  else {
-    // at this point dir_name is guaranteed to be a prefix of page
-    // (has no .pub component), though constructed in a roundabout way
-    temp = page + '.pub'
-    try {
-      let data = await fs_readFile(this.root + temp)
-      console.log(
-        parsed_url.host,
-        'serving',
-        temp,
-        'length',
-        data.length,
-        'from pub'
-      )
-      server.serve(response, 200, mime_type, data)
-      return
-    }
-    catch (err) {
-      if (err.code !== 'ENOENT')
-        throw err
-    }
-
-    switch (file_type) {
-    case 'html':
-      temp = page + '.jst'
-      let template
-      try {
-        template = await js_template(this.root, this.root, temp)
-      }
-      catch (err) {
-        if (err.code !== 'ENOENT') // note: err.code might be undefined
-          throw err
-        template = undefined
-      }
-      if (template !== undefined) {
-        let _out = []
-        await template(
-          {
-            parsed_url: parsed_url,
-            response: response,
-            request: request,
-            site: this
-          },
-          _out
-        )
-        let data = Buffer.from(_out.join(''))
-        console.log(
-          parsed_url.host,
-          'serving',
-          temp,
-          'length',
-          data.length,
-          'from js'
-        )
-        server.serve(response, 200, mime_type, data)
-        return
-      }
-      break
-
-    case 'css':
-      temp = page + '.less'
-      try {
-        let data = await this.get_less(temp, dir_name)
-        console.log(
-          parsed_url.host,
-          'serving',
-          temp,
-          'length',
-          data.length,
-          'from less'
-        )
-        server.serve(response, 200, mime_type, data)
-        return
-      }
-      catch (err) {
-        if (err.code !== 'ENOENT') // note: err.code might be undefined
-          throw err
-      }
-      break
-    }
-  }
-
-  let favicons = await resources.get_zip(this.root + '/favicons.zip')
-  if (Object.prototype.hasOwnProperty.call(favicons, page)) {
-    let data = favicons[page]
-    console.log(
-      parsed_url.host,
-      'serving',
-      page,
-      'length',
-      data.length,
-      'from favicons'
-    )
-    server.serve(response, 200, mime_type, data)
-    return
-  }
-
-  console.log(parsed_url.host, 'file not found', page)
-  server.die(response)
-}
-
 Site.prototype.get_email = function(path) {
   path = this.root + path
   return resources.build_cache_email.get(
     path,
-    async responseult => {
+    async result => {
       console.log('getting', path, 'as email')
-      responseult.value = emailjs.server.connect(
+      result.value = emailjs.server.connect(
         JSON.parse(await fs_readFile(path))
       )
     }
@@ -232,24 +45,24 @@ Site.prototype.get_json = function(path) {
   path = this.root + path
   return resources.build_cache_json.get(
     path,
-    async responseult => {
+    async result => {
       console.log('getting', path, 'as json')
-      responseult.value = JSON.parse(await fs_readFile(path))
+      result.value = JSON.parse(await fs_readFile(path))
     }
   )
 }
 
-Site.prototype.get_less = function(path, dir_name) {
+Site.prototype.get_less = function(dirname, path) {
   path = this.root + path
   return resources.build_cache_less.get(
     path,
-    async responseult => {
+    async result => {
       console.log('getting', path, 'as less')
       let render = await less.render(
         await fs_readFile(path, {encoding: 'utf-8'}),
         {
           //color: true,
-          //compresponses: false,
+          //compress: false,
           //depends: false,
           filename: path,
           //globalVars: null,
@@ -259,7 +72,7 @@ Site.prototype.get_less = function(path, dir_name) {
           //lint: false,
           //math: 0,
           //modifyVars: null,
-          paths: [this.root + dir_name],
+          paths: [this.root + dirname],
           //plugins: [],
           //reUsePluginManager: true,
           //rewriteUrls: false,
@@ -269,8 +82,8 @@ Site.prototype.get_less = function(path, dir_name) {
           //urlArgs: ''
         }
       )
-      responseult.deps.concat(render.imports)
-      responseult.value = Buffer.from(render.css)
+      result.deps.concat(render.imports)
+      result.value = Buffer.from(render.css)
     }
   )
 }
@@ -279,9 +92,9 @@ Site.prototype.get_text = function(path) {
   path = this.root + path
   return resources.build_cache_text.get(
     path,
-    async responseult => {
+    async result => {
       console.log('getting', path, 'as text')
-      responseult.value = await fs_readFile(path, {encoding: 'utf-8'})
+      result.value = await fs_readFile(path, {encoding: 'utf-8'})
     }
   )
 }
@@ -290,15 +103,15 @@ Site.prototype.get_zet = function(path) {
   path = this.root + path
   return resources.build_cache_zet.get(
     path,
-    async responseult => {
+    async result => {
       console.log('getting', path, 'as zet')
-      responseult.deps = [
+      result.deps = [
         path + '.map.0',
         path + '.param.0',
         path + '.v.0',
         path + '.vocab.0'
       ]
-      responseult.value = new zetjs.Index(path)
+      result.value = new zetjs.Index(path)
     }
   )
 }
@@ -307,36 +120,36 @@ Site.prototype.get_zip = function(path) {
   path = this.root + path
   return resources.build_cache_zip.get(
     path,
-    async responseult => {
+    async result => {
       console.log('getting', path, 'as zip')
-      responseult.value = {}
+      result.value = {}
       let zipfile = await yauzl_open(path, {autoClose: false})
       let entries = []
       await new Promise(
-        (responseolve, reject) => {
+        (resolve, reject) => {
           zipfile.
           on('entry', entry => {entries.push(entry)}).
-          on('end', () => responseolve())
+          on('end', () => resolve())
         }
       )
       for (let i = 0; i < entries.length; ++i) {
         let read_stream = await new Promise(
-          (responseolve, reject) => {
+          (resolve, reject) => {
             zipfile.openReadStream(
               entries[i],
               (err, stream) => {
                 if (err)
                   reject(err)
-                responseolve(stream)
+                resolve(stream)
               }
             )
           }
         )
         let write_stream = new stream_buffers.WritableStreamBuffer()
         let data = new Promise(
-          (responseolve, reject) => {
+          (resolve, reject) => {
             write_stream.
-            on('finish', () => {responseolve(write_stream.getContents())}).
+            on('finish', () => {resolve(write_stream.getContents())}).
             on('error', () => {reject()})
           }
         )
@@ -344,7 +157,7 @@ Site.prototype.get_zip = function(path) {
         let path = '/' + entries[i].fileName
         data = await data
         console.log('entry path', path, 'size', data.length)
-        responseult.value[path] = data
+        result.value[path] = data
       }
       await zipfile.close()
     }
@@ -370,4 +183,206 @@ Site.prototype.flush_json = async function(path) {
   return /*await*/ resources.json_cache.flush(this.root + path)
 }
 
+Site.prototype.serve_jst = async function(env, pathname) {
+  let jst
+  try {
+    jst = await js_template(this.root, this.root, this.root + pathname)
+  }
+  catch (err) {
+    if (err.code !== 'ENOENT')
+      throw err
+    return false
+  }
+  let out = []
+  let mime_type = await jst(env, out)
+  if (mime_type === undefined) {
+    // for directories the mime type must be returned, for files we
+    // can look it up from the pathname starting at current position
+    // (for files we're guaranteed to be on last pathname component)
+    let filetype = env.pathname.slice(env.pathname_pos) 
+    assert(
+      Object.prototype.hasOwnProperty.call(config.mime_types, filetype)
+    )
+    mime_type = config.mime_types[filetype]
+  }
+  let data = Buffer.from(out.join(''))
+  console.log(
+    `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from jst`
+  )
+  server.serve(env.response, 200, mime_type, data)
+  return true
+}
+
+Site.prototype.serve_less = async function(env, pathname) {
+  if (env.pathname.slice(env.pathname_pos) !== '.css')
+    return false
+  let data 
+  try {
+    data = await this.get_less(this.root, pathname)
+  }
+  catch (err) {
+    if (err.code !== 'ENOENT')
+      throw err
+    return false
+  }
+  console.log(
+    `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from less`
+  )
+  server.serve(env.response, 200, config.mime_types['.css'], data)
+  return true
+}
+
+Site.prototype.serve_fs = async function(env, pathname) {
+  let data 
+  try {
+    data = await fs_readFile(this.root + pathname)
+  }
+  catch (err) {
+    if (err.code !== 'ENOENT')
+      throw err
+    return false
+  }
+  console.log(
+    `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from fs`
+  )
+  let filetype = env.pathname.slice(env.pathname_pos)
+  server.serve(env.response, 200, config.mime_types[filetype], data)
+  return true
+}
+
+
+Site.prototype.serve_zip = async function(env, pathname) {
+  let zip 
+  try {
+    zip = await this.get_zip(pathname)
+  }
+  catch (err) {
+    if (err.code !== 'ENOENT')
+      throw err
+    return false
+  }
+  if (!Object.prototype.hasOwnProperty.call(zip, env.pathname))
+    return false
+  let data = zip[env.pathname]
+  console.log(
+    `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from zip`
+  )
+  let filetype = env.pathname.slice(env.pathname_pos)
+  server.serve(env.response, 200, config.mime_types[filetype], data)
+  return true
+}
+
+Site.prototype.respond = async function(env) {
+  while (true) {
+    if (env.pathname_pos >= env.pathname.length) {
+      let pathname = env.pathname + '/index.html'
+      console.log(
+        `${env.parsed_url.host} redirecting ${env.pathname} to ${pathname}`
+      )
+      server.redirect(
+        env.response,
+        pathname + (env.parsed_url.search || '')
+      )
+      return
+    }
+
+    assert(env.pathname.charAt(env.pathname_pos) === '/')
+    let i = env.pathname_pos + 1
+    let j = env.pathname.indexOf('/', i)
+    if (j === -1)
+      j = env.pathname.length
+    let filename = env.pathname.slice(i, j)
+
+    if (filename.length === 0) {
+      if (j >= env.pathname.length) {
+        let pathname = env.pathname + 'index.html'
+        console.log(
+          `${env.parsed_url.host} redirecting ${env.pathname} to ${pathname}`
+        )
+        server.redirect(
+          env.response,
+          pathname + (env.parsed_url.search || '')
+        )
+      }
+      else {
+        console.log(
+          `${env.parsed_url.host} empty directory name in ${env.pathname}`
+        )
+        server.die(env.response)
+      }
+      return
+    }
+
+    if (
+      filename.charAt(0) === '.' ||
+      filename.charAt(0) === '_'
+    ) {
+      console.log(
+        `${env.parsed_url.host} bad component "${filename}" in ${env.pathname}`
+      )
+      server.die(env.response)
+      return
+    }
+
+    let k = filename.lastIndexOf('.')
+    if (k === -1)
+      k = filename.length
+    let filetype = filename.slice(k)
+
+    if (
+      filetype.length !== 0 &&
+      Object.prototype.hasOwnProperty.call(config.mime_types, filetype)
+    ) {
+      if (j < env.pathname.length) {
+        console.log(
+          `${env.parsed_url.host} non-directory filetype "${filetype}" in ${env.pathname}`
+        )
+        server.die(env.response)
+        return
+      }
+      env.pathname_pos = i + k // advance to "." at start of filetype
+      break
+    }
+
+    env.pathname_pos = j
+    let pathname = env.pathname.slice(0, env.pathname_pos)
+    if (await this.serve_jst(env, pathname + '.dir.jst'))
+      return
+
+    let stats
+    try {
+      stats = await fs_stat(this.root + pathname)
+    }
+    catch (err) {
+      if (err.code !== 'ENOENT')
+        throw err
+      console.log(
+        `${env.parsed_url.host} directory not found: ${pathname}`
+      )
+      server.die(env.response)
+      return
+    }
+    if (!stats.isDirectory()) {
+      console.log(
+        `${env.parsed_url.host} not directory: ${pathname}`
+      )
+      server.die(env.response)
+      return
+    }
+  }    
+
+  if (
+    !await this.serve_jst(env, env.pathname + '.jst') &&
+    !await this.serve_less(env, env.pathname + '.less') &&
+    !await this.serve_fs(env, env.pathname) &&
+    !await this.serve_zip(env, '/favicons.zip')
+  ) {
+    console.log(
+      `${env.parsed_url.host} file not found ${env.pathname}`
+    )
+    server.die(env.response)
+  }
+}
+
 module.exports = Site
index 00eee41..80300fe 100644 (file)
--- a/config.js
+++ b/config.js
@@ -23,8 +23,8 @@ let refresh = async () => {
     }
   )
   mime_type_html =
-    Object.prototype.hasOwnProperty.call(mime_types, 'html') ?
-    mime_types.html :
+    Object.prototype.hasOwnProperty.call(mime_types, '.html') ?
+    mime_types['.html'] :
     mime_type_default
 
   // a bit awkward... changing the exports on the fly
index 7a70b6b..6a6a4e3 100644 (file)
@@ -1,11 +1,11 @@
 {
-  "css": "text/css; charset=utf-8",
-  "html": "text/html; charset=utf-8",
-  "ico": "image/x-icon",
-  "jpg": "image/jpg",
-  "jpeg": "image/jpeg",
-  "js": "application/javascript; charset=utf-8",
-  "json": "application/json; charset=utf-8",
-  "png": "image/png",
-  "xml": "text/xml; charset=utf-8"
+  ".css": "text/css; charset=utf-8",
+  ".html": "text/html; charset=utf-8",
+  ".ico": "image/x-icon",
+  ".jpg": "image/jpeg",
+  ".jpeg": "image/jpeg",
+  ".js": "application/javascript; charset=utf-8",
+  ".json": "application/json; charset=utf-8",
+  ".png": "image/png",
+  ".xml": "text/xml; charset=utf-8"
 }
index e43b006..73864c5 100644 (file)
--- a/server.js
+++ b/server.js
@@ -83,7 +83,16 @@ let respond = async (request, response, protocol) => {
         }
         site_cache[temp.root] = site
       }
-      await site.object.respond(request, response, parsed_url)
+      await site.object.respond(
+        {
+          parsed_url: parsed_url,
+          pathname: parsed_url.pathname,
+          pathname_pos: 0,
+          response: response,
+          request: request,
+          site: site.object
+        }
+      )
       break
     default:
       assert(false)