initial
authorken <ken@kengrimes.com>
Tue, 1 Aug 2017 23:18:52 +0000 (23:18 +0000)
committerken <ken@kengrimes.com>
Tue, 1 Aug 2017 23:18:52 +0000 (23:18 +0000)
strappServer.js [new file with mode: 0644]

diff --git a/strappServer.js b/strappServer.js
new file mode 100644 (file)
index 0000000..2f8d383
--- /dev/null
@@ -0,0 +1,365 @@
+/**
+* @file      Node entry and main driver
+* @author    Jordan Lavatai, Ken Grimes
+* @version   0.0.3
+* @license   AGPL-3.0
+* @copyright Strapp.io 2017
+* @summary   HTTP(S) Router that uses the first directory in the requested URL
+*            as the route name
+*/
+'use strict'
+const opts = require('./opts.js')
+
+const dlog = (msg) => console.log(msg)
+
+class Server {
+  constructor(...props) {
+    Object.assign(this, new.target.defaults, ...props)
+    this.init()
+  }
+  init() {
+    if (this.cacheDir)
+      this.loadCache(this.cacheDir, '')
+    require('fs').readFile('./strapp.js', (err, data) => {
+      if (err)
+        throw new Error (err)
+      this.cache['strapp.js'] = ['application/javascript',data]
+    })
+    this.startHttpd()
+  }
+  loadCache(dirPath, prefix) {
+    require('fs').readdir(this.cacheDir, (err, files) => {
+      if (err)
+        console.log(err)
+      else
+        files.forEach((entName) => {
+          let filePath = `${dirPath}${require('path').sep}${entName}`
+          require('fs').stat(filePath, (err, stat) => {
+            if (err)
+              console.log(err)
+            else if (stat && stat.isDirectory())
+              this.loadCache(filePath, `${entName}/`)
+            else
+              require('fs').readFile(filePath, (err, data) => {
+                if (err)
+                  console.log(err)
+                else
+                  this.cache[`${prefix}${entName}`] =
+                  [require('mime').lookup(filePath), data]
+              })
+          })
+        })
+    })
+  }
+  startHttpd() {
+    if (this.tls)
+      this.httpd = require('https')
+      .createServer(this.tls, (rq, rs) => this.httpRequest(rq, rs))
+    else
+      this.httpd = require('http')
+      .createServer((rq, rs) => this.httpRequest(rq, rs))
+    this.httpd.listen(this, () => this.port = this.httpd.address().port)
+  }
+  httpRequest(request, response) {
+    const htArgv = request.url.slice(1).split('?')
+    dlog(`request for ${request.url} received`)
+    if (request.method === 'GET' && htArgv[0] in this.cache) {
+      dlog(`found in cache`)
+      response.writeHead(200, { 'Content-Type': this.cache[htArgv[0]][0] })
+      response.write(this.cache[htArgv[0]][1])
+      response.end()
+      return
+    }
+    let pubKey = ''
+    const authHeader = request.headers['Authorization']
+    if (authHeader) {
+      const authInfo = authHeader.split(' ')
+      dlog(`authInfo: ${authInfo}`)
+      if (authInfo[0] === 'STRAPP') {
+        let answer = ''
+        switch (authInfo.length) {
+        case 3: answer = authInfo[2]
+        case 2: pubKey = authInfo[1]
+          break
+        default:
+          response.writeHead(400)
+          response.end()
+          return
+        }
+        this.authenticate(pubKey, answer).then((question, authenticated) => {
+          response.setHeader('WWW-Authenticate',
+                             `STRAPP ${this.question(pubKey)} ${this.tls.key}`)
+          if (authenticated) {
+            this.processRequestData(request, response, htArgv, pubKey)
+          }
+          else {
+            response.writeHead(401)
+            response.end()
+          }
+        }).catch((err) => console.log(err))
+      }
+    }
+    else
+      this.processRequestData(request, response, htArgv, pubKey)
+  }
+  processRequestData(request, response, args, pubKey) {
+    if (request.method === 'PUT' || request.method === 'POST') {
+      let data = ''
+      request.on('data', (chunk) => {
+        data += chunk
+        if (data.length > 5e5)
+          request.connection.destroy()
+      })
+      request.on('end', this.sendData(request, response, args, pubKey, data))
+    }
+    else
+      this.sendData(request, response, args, pubKey)
+  }
+  authenticate(pubKey, answer) {
+    return new Promise((resolve, reject) => {
+      require('crypto').randomBytes(256, (err, buf) => {
+        if (err)
+          reject(err)
+        else {
+          let authenticated = false
+          if (pubKey in this.answers) {
+            const answerBuf = Buffer.from(answer)
+            require('crypto').privateDecrypt({ key: this.tls.cert }, answerBuf)
+            authenticated = this.answers[pubKey].equals(answerBuf)
+          }
+          this.answers[pubKey] = buf
+          let question = Buffer.from(buf)
+          require('crypto').publicEncrypt({ key: pubKey }, question)
+          resolve(question.toString(), authenticated)
+        }
+      })
+    })
+  }
+  sendData(request, response, args, pubKey, data) {
+    if (this.app) {
+      let msgID = this.msgID++
+      if (this.msgID >= this.pendingResponses.length)
+        this.msgID = 0
+      this.pendingResponses[msgID] = response
+      if (!data)
+        data = ''
+      this.app.send(`${args[0]} ${request.method} ${pubKey} ${msgID} ${data}`)
+    }
+    else {
+      response.writeHead(200, { 'Content-Type': 'text/html' })
+      response.write(Server.bootstrapp)
+      response.end()
+    }
+  }
+}
+Server.defaults = {
+  port: 0,
+  tls: { key: '', cert: '' },
+  host: undefined,
+  cache: {},
+  cacheDir: './www',
+  pendingResponses: new Array(20),
+  msgID: 0,
+  answers: {},
+  remoteAdminKey: undefined,
+  controller: undefined,
+  app: undefined,
+  httpd: undefined,
+  wsd: undefined
+}
+Server.bootstrapp = `
+<!DOCTYPE html>
+<html>
+  <title>bootstrapp</title>
+  <head>
+    <script src='/strapp.js'></script>
+  </head>
+  <body>
+  </body>
+</html>
+`
+
+//TODO: module.exports = Server
+
+const server = new Server({
+  port: 2443,
+  tls: { key: require('fs').readFileSync('../certs/key.pem'),
+         cert: require('fs').readFileSync('../certs/cert.pem')
+       }
+})
+
+
+// //TTL implementation
+// setInterval(30000, () => {
+//   applications.forEach((app) => {
+//     app.sockets.forEach((socket, idx) => {
+//       if (!socket.connected) {
+//         console.log(`Socket ${socket} timed out`)
+//         socket.terminate()
+//         app.sockets.splice(idx, 1)
+//       }
+//       else {
+//         socket.connected = false
+//         socket.ping('', false, true)
+//       }
+//     })
+//   })
+// })
+
+// const startApplication = (conf) => {
+//   let httpd
+//   if (conf.tls)
+//     httpd = require('https')
+//     .createServer(conf.tls, listen)
+//     .listen(conf, conf.callback)
+//   else
+//     httpd = require('http')
+//     .createServer(listen)
+//     .listen(conf, conf.callback)
+  
+//   let app = {
+//     httpd: httpd,
+//     sockets: [],
+//     openSocket: function(pubKey) {
+//       const wsd = require('ws').Server()({
+//         noServer: true,
+//         verifyClient: (info) => pubKey === parseAuth(info.req).pubKey
+//           && authenticate(info.req)
+//       })
+//       let timeout = setTimeout(30000, wsd.close())
+//       wsd.on('connection', (socket) => {
+//         clearTimeout(timeout)
+//         socket.on('pong', socket.connected = true)
+//         socket.emit('pong')
+//         this.sockets.push(socket)
+//       })
+//     }
+//   }
+//   return app
+// }
+
+// startApplication(opts.port, opts.tls)
+
+// if (conf.tls)
+//   httpd = require('https')
+//   .createServer(conf.tls)
+//   .listen(conf, resolve)
+// else
+//   httpd = require('http')
+//   .createServer()
+//   .listen(resolve)
+
+
+// const listener = function(port, conf) Promise((resolve, reject) => {
+//   resolve(Object.create(null, {
+//     httpd: { value: null },
+//     port: { value: 0 },
+//     transceiver: { value: null, writable: true, configurable: true }
+//   }))
+// })
+
+// const a = {
+//   httpd: null,
+//   port: 0,
+//   transceiver: null
+// }
+
+
+// /** @func
+//  * @summary Authenticate a request
+//  * @arg {http.ClientRequest} request
+//  * @return {bool} true if the request is authentic
+//  */
+// const authenticate = (request) => {
+//   return true
+// }
+
+// const startPortForward = function(port, httpOpts, transceiver) {
+//   const httpd, port, channel
+// }
+
+// /** @func
+//  * Assumes Authorization header of form STRAPP {pubKey} {answer}
+//  * @summary parse the components of a STRAPP authorization header
+//  * @arg {http.ClientRequest} request
+//  * @return {string} the empty string '' on error
+//  */
+// const parseAuth = (request) => {
+//   if (request.headers['Authorization']) {
+//     request.auth = request.headers['Authorization'].split(' ')
+//     if (request.auth[0] === 'STRAPP')
+//       switch(request.auth.length) {
+//       case 3: request.answer = request.auth[2]
+//       case 2: request.pubKey = request.auth[1]
+//       default:
+//       }
+//   }
+//   return request
+// }
+
+// /** @func
+//  * @summary Creates a promise that resolves a websocket to an application host
+//  * @arg {string} pubKey - the public key authorized to connect.
+//  * @arg {Object} [conf] - configuration for the websocket. 
+//  * @arg {Object} [conf.tls] - TLS configuration for HTTPS layer security
+//  * @arg {string} [conf.tls.cert] - domain certificate for HTTPS
+//  * @arg {string} [conf.tls.key] - domain public key for HTTPS
+//  * @arg {string} [conf.host] - host to attach the port listener to (all IPs)
+//  * @arg {number} [conf.backlog] - how many connections to buffer (511)
+//  * @arg {boolean} [conf.exclusive] - don't share the port (false)
+//  */
+// const pWebSocket = function(pubKey, conf) Promise((resolve, reject) => {
+//     let timeout
+//     conf = conf || {}
+//     if (!pubKey)
+//       reject(new Error('no pubKey'))
+//     require('get-port')().then((port) => {
+//       conf.port = port
+//       new Promise((resolve, reject) => {
+//         if (conf.tls)
+//           httpd = require('https')
+//           .createServer(conf.tls)
+//           .listen(conf, resolve)
+//         else
+//           httpd = require('http')
+//           .createServer()
+//           .listen(resolve)
+//       }).then(() => {
+//         const wsd = new Promise((resolve, reject) => {
+//           (require('ws').Server())({
+//             server: httpd,
+//             verifyClient: (info) => pubKey === parseAuth(info.req).pubKey
+//               && authenticate(info.req)
+//           }).on('connection', resolve)
+//           timeout = setTimeout(30000, reject('timeout'))
+//         }).then((socket) => {
+//           clearTimeout(timeout)
+//           const webSocket = Object.create(null, {
+//             socket: { value: socket },
+//             httpd: { value: httpd },
+//             port: { value: port },
+//             transmit: { value: socket.send },
+//             receive: { value: this.receive }
+//           })
+//           socket.on('message', webSocket.receive)
+//           resolve(webSocket)
+//         }).catch(reject)
+//       }).catch(reject)
+//     }).catch(reject)
+//   })
+
+// Object.keys(opts['bindings']).forEach((key) => {
+//   router.createBind(key, opts['bindings'][key])
+// })
+
+// router.startHttpServer({
+//   port: opts['port'],
+//   skelFile: './skel.html',
+//   clientJS: opts['client-js'],
+//   hostJS: opts['host-js'],
+//   httpdRoot: opts['file-dir'],
+//   tls: {
+//     certFile: opts['ca-cert'],
+//     keyFile: opts['ca-key']
+//   }
+// })