Change configuration scheme to use _config/server.jst and each _config/site.jst
authorNick Downing <nick@ndcode.org>
Tue, 30 Oct 2018 02:04:35 +0000 (13:04 +1100)
committerNick Downing <nick@ndcode.org>
Tue, 30 Oct 2018 02:04:35 +0000 (13:04 +1100)
Server.js
Site.js
SiteConfig.js [new file with mode: 0644]
SiteRedirect.js [new file with mode: 0644]
SiteRoot.js [new file with mode: 0644]
SiteRootConfig.js [new file with mode: 0644]
_config/server.jst [new file with mode: 0644]
_config/sites.json [deleted file]
index.js
jst_server.js

index 1bbd1e9..7dfe9b8 100644 (file)
--- a/Server.js
+++ b/Server.js
@@ -16,13 +16,16 @@ let fs_mkdir = util.promisify(fs.mkdir)
 let fs_readFile = util.promisify(fs.readFile)
 let yauzl_open = util.promisify(yauzl.open)
 
-let Server = function(socket_io, caching) {
+let Server = function(sites, enable_socket_io, enable_caching) {
   if (!this instanceof Server)
     throw Error('Server is a constructor')
 
-  this.socket_io = socket_io
-  this.caching = caching || false
-  this.site_cache = {}
+  this.sites = sites
+  this.socket_io = enable_socket_io ? new (require('socket.io'))() : undefined
+  this.enable_caching = enable_caching || false
+
+  this.http_servers = []
+  this.https_servers = []
 
   this.build_cache_email = new BuildCache()
   this.build_cache_json = new BuildCache()
@@ -32,13 +35,36 @@ let Server = function(socket_io, caching) {
   this.build_cache_zip = new BuildCache()
   this.json_cache = new JSONCache(true)
 
-  this.sites = undefined
   this.mime_types = undefined
   this.mime_type_html = undefined
   this.mime_type_default = 'application/octet-stream'
 }
 
-Server.prototype.attach = function(server, protocol) {
+Server.prototype.listen = async function(port, protocol, ssl_cert, ssl_key) {
+  console.log(`listen on port ${port} protocol ${protocol}`)
+  let server
+  switch (protocol) {
+  case 'http:':
+    server = require('http').createServer()
+    this.http_servers.push(server) // in future use this for shutdown/reload
+    break;
+  case 'https:':
+    server = require('https').createServer(
+      {
+        'cert': await fs_readFile(
+          ssl_cert || '_ssl/localhost_cert_bundle.pem'
+        ),
+        'key': await fs_readFile(
+          ssl_key || '_ssl/localhost_key.pem'
+        )
+      }
+    )
+    this.https_servers.push(server) // in future use this for shutdown/reload
+    break;
+  default:
+    assert(false);
+  }
+  server.listen(port)
   server.on(
     'request',
     (request, response) =>
@@ -227,7 +253,6 @@ Server.prototype.modify_json =
   }
 
 Server.prototype.refresh_config = async function() {
-  this.sites = await this.get_json('_config/sites.json')
   this.mime_types = await this.get_json('_config/mime_types.json')
   this.mime_type_html =
     Object.prototype.hasOwnProperty.call(this.mime_types, '.html') ?
@@ -305,7 +330,9 @@ Server.prototype.respond = async function(request, response, protocol) {
     )
     //console.log('parsed_url', parsed_url)
 
-    if (!Object.prototype.hasOwnProperty.call(this.sites, parsed_url.hostname)) {
+    if (
+      !Object.prototype.hasOwnProperty.call(this.sites, parsed_url.hostname)
+    ) {
       this.die(
         response,
         parsed_url.pathname,
@@ -313,64 +340,20 @@ Server.prototype.respond = async function(request, response, protocol) {
       )
       return
     }
-    let temp = this.sites[parsed_url.hostname]
-    switch (temp.type) {
-    case 'redirect':
-      let hostname = temp.domain
-      if (parsed_url.port !== undefined)
-        hostname += ':' + parsed_url.port
-      this.redirect(
-        response,
-        `${parsed_url.protocol}//${hostname}${request.url}`,
-        `redirecting ${parsed_url.host} to ${hostname}`
-      )
-      break
-    case 'site':
-      let site_factory
-      try {
-        site_factory = await this.get_jst(temp.root, 'site_factory.jst')
-      }
-      catch (err) {
-        if (err.code !== 'ENOENT') // note: err.code might be undefined
-          throw err
-        site_factory = site_factory_default
-      }
-      let site = undefined
-      if (
-        !Object.prototype.hasOwnProperty.call(this.site_cache, temp.root) ||
-        (site = this.site_cache[temp.root]).factory !== site_factory
-      ) {
-        if (site !== undefined)
-          for (let i of site.object.socket_io_connect_listeners) {
-            assert(this.socket_io !== undefined)
-            this.socket_io.removeListener('connect', i)
-          }
-        site = {
-          factory: site_factory,
-          object: await site_factory(this, temp.root)
-        }
-        for (let i of site.object.socket_io_connect_listeners) {
-          assert(this.socket_io !== undefined)
-          this.socket_io.on('connect', i)
-        }
-        this.site_cache[temp.root] = site
+
+    let site = this.sites[parsed_url.hostname]
+    site.respond(
+      {
+        mime_type: this.mime_type_default,
+        parsed_url: parsed_url,
+        pathname: parsed_url.pathname,
+        pathname_pos: 0,
+        response: response,
+        request: request,
+        status: 200,
+        site: site
       }
-      await site.object.respond(
-        {
-          mime_type: this.mime_type_default,
-          parsed_url: parsed_url,
-          pathname: parsed_url.pathname,
-          pathname_pos: 0,
-          response: response,
-          request: request,
-          status: 200,
-          site: site.object
-        }
-      )
-      break
-    default:
-      assert(false)
-    }
+    )
   }
   catch (err) {
     let message = (err.stack || err.message).toString()
diff --git a/Site.js b/Site.js
index 0067955..be61e9f 100644 (file)
--- a/Site.js
+++ b/Site.js
@@ -1,17 +1,8 @@
-let assert = require('assert')
-let fs = require('fs')
-let js_template = require('js_template')
-let util = require('util')
-
-let fs_readFile = util.promisify(fs.readFile)
-let fs_stat = util.promisify(fs.stat)
-
-let Site = function(server, root) {
+let Site = function(server) {
   if (!this instanceof Site)
     throw Error('Site is a constructor')
   this.server = server
-  this.root = root
-  this.socket_io_connect_listeners = []
+  this.socket_io_connect_listeners = [] // later will use this for destruction
 }
 
 Site.prototype.serve = function(env, data, message_from) {
@@ -40,203 +31,8 @@ Site.prototype.redirect = function(env, pathname) {
   )
 }
 
-Site.prototype.get_email = function(pathname) {
-  return /*await*/ this.server.get_email(this.root + pathname)
-}
-Site.prototype.get_json = function(pathname) {
-  return /*await*/ this.server.get_json(this.root + pathname)
-}
-Site.prototype.get_jst = function(pathname) {
-  return /*await*/ this.server.get_jst(this.root, this.root + pathname)
-}
-Site.prototype.get_less = function(pathname) {
-  return /*await*/ this.server.get_less(this.root, this.root + pathname)
-}
-Site.prototype.get_text = function(pathname) {
-  return /*await*/ this.server.get_text(this.root + pathname)
-}
-Site.prototype.get_zet = function(pathname) {
-  return /*await*/ this.server.get_zet(this.root + pathname)
-}
-Site.prototype.get_zip = function(pathname) {
-  return /*await*/ this.server.get_zip(this.root + pathname)
-}
-
-Site.prototype.ensure_dir = async function(pathname) {
-  return /*await*/ this.server.ensure_dir(this.root + pathname)
-}
-
-// this is for read/write JSON files
-// they will not be reloaded from disk if modified
-Site.prototype.read_json = async function(pathname, default_value) {
-  return /*await*/ this.server.read_json(
-    this.root + pathname,
-    default_value
-  )
-}
-Site.prototype.write_json = async function(pathname, value, timeout) {
-  return /*await*/ this.server.write_json(
-    this.root + pathname,
-    value,
-    timeout
-  )
-}
-Site.prototype.modify_json =
-  async function(pathname, default_value, modify_func, timeout) {
-    return /*await*/ this.server.modify_json(
-      this.root + pathname,
-      default_value,
-      modify_func,
-      timeout
-    )
-  }
-
-Site.prototype.serve_jst = async function(env, pathname) {
-  let jst
-  try {
-    jst = await this.get_jst(pathname)
-  }
-  catch (err) {
-    if (err.code !== 'ENOENT')
-      throw err
-    return false
-  }
-  await jst(env)
-  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(pathname)
-  }
-  catch (err) {
-    if (err.code !== 'ENOENT')
-      throw err
-    return false
-  }
-  this.serve(env, data, 'less')
-  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
-  }
-  this.serve(env, data, 'fs')
-  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
-  this.serve(env, zip[env.pathname], 'zip')
-  return true
-}
-
 Site.prototype.respond = async function(env) {
-  while (true) {
-    if (env.pathname_pos >= env.pathname.length) {
-      // directory without trailing slash
-      this.redirect(env, env.pathname + '/index.html')
-      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)
-        // directory with trailing slash
-        this.redirect(env, env.pathname + 'index.html')
-      else
-        this.die(env, `empty directory name in ${env.pathname}`)
-      return
-    }
-
-    if (
-      filename.charAt(0) === '.' ||
-      filename.charAt(0) === '_'
-    ) {
-      this.die(env, `bad component "${filename}" in ${env.pathname}`)
-      return
-    }
-
-    let k = filename.lastIndexOf('.')
-    if (k === -1)
-      k = filename.length
-    let filetype = filename.slice(k)
-
-    if (
-      filetype.length !== 0 &&
-      Object.prototype.hasOwnProperty.call(this.server.mime_types, filetype)
-    ) {
-      if (j < env.pathname.length) {
-        this.die(env, `non-directory filetype "${filetype}" in ${env.pathname}`)
-        return
-      }
-      env.mime_type = this.server.mime_types[filetype]
-      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
-      this.die(env, `directory not found ${pathname}`)
-      return
-    }
-    if (!stats.isDirectory()) {
-      this.die(
-        env,
-        j < env.pathname.length ?
-          `not directory ${pathname}` :
-          `unknown filetype "${filetype}" in ${pathname}`
-      )
-      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, '/_favicon/favicons.zip')
-  ) {
-    this.die(env, `file not found ${env.pathname}`)
-  }
+  throw new Error('not implemented')
 }
 
 module.exports = Site
diff --git a/SiteConfig.js b/SiteConfig.js
new file mode 100644 (file)
index 0000000..eb272e6
--- /dev/null
@@ -0,0 +1,20 @@
+let AbstractSite = require('AbstractSite')
+
+let Site = function(server, root, config_file) {
+  AbstractSite.call(this, server)
+  this.root = 
+  this.new_host = new_host
+}
+
+Site.prototype = Object.create(AbstractSite.prototype)
+
+SiteRedirect.respond = async function(env) {
+  let new_host = this.new_host
+  if (parsed_url.port !== undefined)
+    new_host += ':' + parsed_url.port
+  this.redirect(
+    response,
+    `${parsed_url.protocol}//${new_host}${request.url}`,
+    `redirecting ${parsed_url.host} to ${new_host}`
+  )
+}
diff --git a/SiteRedirect.js b/SiteRedirect.js
new file mode 100644 (file)
index 0000000..eeb7ce5
--- /dev/null
@@ -0,0 +1,23 @@
+let Site = require('./Site')
+
+let SiteRedirect = function(server, redirect) {
+  if (!this instanceof SiteRedirect)
+    throw Error('SiteRedirect is a constructor')
+  Site.call(this, server)
+  this.redirect = redirect
+}
+
+SiteRedirect.prototype = Object.create(Site.prototype)
+
+SiteRedirect.respond = async function(env) {
+  let new_host = this.redirect
+  if (parsed_url.port !== undefined)
+    new_host += ':' + parsed_url.port
+  this.redirect(
+    response,
+    `${parsed_url.protocol}//${redirect}${request.url}`,
+    `redirecting ${parsed_url.host} to ${new_host}`
+  )
+}
+
+module.exports = SiteRedirect
diff --git a/SiteRoot.js b/SiteRoot.js
new file mode 100644 (file)
index 0000000..b7a8f0b
--- /dev/null
@@ -0,0 +1,218 @@
+let Site = require('./Site')
+let assert = require('assert')
+let fs = require('fs')
+let js_template = require('js_template')
+let util = require('util')
+
+let fs_readFile = util.promisify(fs.readFile)
+let fs_stat = util.promisify(fs.stat)
+
+let SiteRoot = function(server, root) {
+  if (!this instanceof SiteRoot)
+    throw Error('SiteRoot is a constructor')
+  Site.call(this, server)
+  this.root = root
+}
+
+SiteRoot.prototype = Object.create(Site.prototype)
+
+SiteRoot.prototype.get_email = function(pathname) {
+  return /*await*/ this.server.get_email(this.root + pathname)
+}
+SiteRoot.prototype.get_json = function(pathname) {
+  return /*await*/ this.server.get_json(this.root + pathname)
+}
+SiteRoot.prototype.get_jst = function(pathname) {
+  return /*await*/ this.server.get_jst(this.root, /*this.root +*/ pathname)
+}
+SiteRoot.prototype.get_less = function(pathname) {
+  return /*await*/ this.server.get_less(this.root, this.root + pathname)
+}
+SiteRoot.prototype.get_text = function(pathname) {
+  return /*await*/ this.server.get_text(this.root + pathname)
+}
+SiteRoot.prototype.get_zet = function(pathname) {
+  return /*await*/ this.server.get_zet(this.root + pathname)
+}
+SiteRoot.prototype.get_zip = function(pathname) {
+  return /*await*/ this.server.get_zip(this.root + pathname)
+}
+
+SiteRoot.prototype.ensure_dir = async function(pathname) {
+  return /*await*/ this.server.ensure_dir(this.root + pathname)
+}
+
+// this is for read/write JSON files
+// they will not be reloaded from disk if modified
+SiteRoot.prototype.read_json = async function(pathname, default_value) {
+  return /*await*/ this.server.read_json(
+    this.root + pathname,
+    default_value
+  )
+}
+SiteRoot.prototype.write_json = async function(pathname, value, timeout) {
+  return /*await*/ this.server.write_json(
+    this.root + pathname,
+    value,
+    timeout
+  )
+}
+SiteRoot.prototype.modify_json =
+  async function(pathname, default_value, modify_func, timeout) {
+    return /*await*/ this.server.modify_json(
+      this.root + pathname,
+      default_value,
+      modify_func,
+      timeout
+    )
+  }
+
+SiteRoot.prototype.serve_jst = async function(env, pathname) {
+  let jst
+  try {
+    jst = await this.get_jst(pathname)
+  }
+  catch (err) {
+    if (err.code !== 'ENOENT')
+      throw err
+    return false
+  }
+  await jst(env)
+  return true
+}
+
+SiteRoot.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(pathname)
+  }
+  catch (err) {
+    if (err.code !== 'ENOENT')
+      throw err
+    return false
+  }
+  this.serve(env, data, 'less')
+  return true
+}
+
+SiteRoot.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
+  }
+  this.serve(env, data, 'fs')
+  return true
+}
+
+
+SiteRoot.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
+  this.serve(env, zip[env.pathname], 'zip')
+  return true
+}
+
+SiteRoot.prototype.respond = async function(env) {
+  while (true) {
+    if (env.pathname_pos >= env.pathname.length) {
+      // directory without trailing slash
+      this.redirect(env, env.pathname + '/index.html')
+      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)
+        // directory with trailing slash
+        this.redirect(env, env.pathname + 'index.html')
+      else
+        this.die(env, `empty directory name in ${env.pathname}`)
+      return
+    }
+
+    if (
+      filename.charAt(0) === '.' ||
+      filename.charAt(0) === '_'
+    ) {
+      this.die(env, `bad component "${filename}" in ${env.pathname}`)
+      return
+    }
+
+    let k = filename.lastIndexOf('.')
+    if (k === -1)
+      k = filename.length
+    let filetype = filename.slice(k)
+
+    if (
+      filetype.length !== 0 &&
+      Object.prototype.hasOwnProperty.call(this.server.mime_types, filetype)
+    ) {
+      if (j < env.pathname.length) {
+        this.die(env, `non-directory filetype "${filetype}" in ${env.pathname}`)
+        return
+      }
+      env.mime_type = this.server.mime_types[filetype]
+      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
+      this.die(env, `directory not found ${pathname}`)
+      return
+    }
+    if (!stats.isDirectory()) {
+      this.die(
+        env,
+        j < env.pathname.length ?
+          `not directory ${pathname}` :
+          `unknown filetype "${filetype}" in ${pathname}`
+      )
+      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, '/_favicon/favicons.zip')
+  ) {
+    this.die(env, `file not found ${env.pathname}`)
+  }
+}
+
+module.exports = SiteRoot
diff --git a/SiteRootConfig.js b/SiteRootConfig.js
new file mode 100644 (file)
index 0000000..43122c2
--- /dev/null
@@ -0,0 +1,26 @@
+let SiteRoot = require('./SiteRoot')
+
+let SiteRootConfig = function(server, root, config) {
+  if (!this instanceof SiteRootConfig)
+    throw Error('SiteRootConfig is a constructor')
+  SiteRoot.call(this, server, root)
+  this.config = config
+}
+
+SiteRootConfig.prototype = Object.create(SiteRoot.prototype)
+
+SiteRootConfig.prototype.respond = async function(env) {
+  let site
+  try {
+    site = await this.get_jst(this.config)
+  }
+  catch (err) {
+    if (err.code !== 'ENOENT') // error type??
+      throw err
+    // no custom site object, handle the request ourselves
+    return SiteRoot.prototype.respond.call(this, env)
+  }
+  return site.respond(env)
+}
+
+module.exports = SiteRootConfig
diff --git a/_config/server.jst b/_config/server.jst
new file mode 100644 (file)
index 0000000..34202a5
--- /dev/null
@@ -0,0 +1,15 @@
+let Server = require('../Server')
+let SiteRedirect = require('../SiteRedirect')
+let SiteRootConfig = require('../SiteRootConfig')
+
+let server = new Server(
+  true, // enable_socket_io
+  false // enable_caching
+)
+server.sites = {
+  'localhost': new SiteRootConfig(server, 'site', '/_config/site.jst'),
+  'localhost.localdomain': new SiteRedirect(server, 'localhost')
+}
+await server.listen(8080, 'http:')
+await server.listen(8443, 'https:')
+return server // later will use to destroy when _config/server.jst changes
diff --git a/_config/sites.json b/_config/sites.json
deleted file mode 100644 (file)
index 2868a40..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "localhost": {"type": "site", "root": "site"},
-  "localhost.localdomain": {"type": "redirect", "domain": "localhost"}
-}
index 8842391..017e2d2 100644 (file)
--- a/index.js
+++ b/index.js
@@ -1,2 +1,5 @@
 exports.Server = require('./Server')
 exports.Site = require('./Site')
+exports.SiteRedirect = require('./SiteRedirect')
+exports.SiteRoot = require('./SiteRoot')
+exports.SiteRootConfig = require('./SiteRootConfig')
index f6b08cb..d5164df 100755 (executable)
@@ -1,48 +1,14 @@
 #!/usr/bin/env node
 
-let Server = require('./Server')
-let SocketIO = require('socket.io')
-let commander = require('commander')
-let fs = require('fs')
-let http = require('http')
-let https = require('https')
+let js_template = require('js_template')
 
-commander.version('1.0.0').option(
-  '-c, --enable-caching',
-  'Enable caching'
-).option(
-  '-j, --ssl-cert [path]',
-  'Set SSL certificate [_ssl/localhost_cert_bundle.pem]',
-  '_ssl/localhost_cert_bundle.pem'
-).option(
-  '-k, --ssl-key [path]',
-  'Set SSL private key [_ssl/localhost_key.pem]',
-  '_ssl/localhost_key.pem'
-).option(
-  '-p, --http-port [port]',
-  'Set HTTP listen port, -1 disable [8080]',
-  8080
-).option(
-  '-q, --https-port [port]',
-  'Set HTTPS listen port, -1 disable [8443]',
-  8443
-).parse(process.argv)
-
-let server = new Server(new SocketIO(), commander.enableCaching)
-if (commander.httpPort !== -1) {
-  let http_server = http.createServer()
-  server.attach(http_server, 'http:')
-  http_server.listen(commander.httpPort)
-  console.log('HTTP server listening on port', commander.httpPort)
-}
-if (commander.httpsPort !== -1) {
-  let https_server = https.createServer(
-    {
-      'cert': fs.readFileSync(commander.sslCert),
-      'key': fs.readFileSync(commander.sslKey)
+;(
+  async () => {
+    try {
+      await js_template('.', '.', '/_config/server.jst')
+    }
+    catch (err) {
+      console.error(err.stack || err.message)
     }
-  )
-  server.attach(https_server, 'https:')
-  https_server.listen(commander.httpsPort)
-  console.log('HTTPS server listening on port', commander.httpsPort)
-}
+  }
+)() // ignore returned promise