/**
* @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 = `
bootstrapp
`
//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']
// }
// })