*/
validRoutes: /[a-zA-Z][a-zA-Z0-9\-_]*/,
+ /** A map of routes
+ * @prop {Object.Map} routes - all the routes!
+ */
+ routes: {},
+
/** Parameters set on bootup (startHttpServer)
* @prop {string[2]} skelPage - html document split in twain for JS injection
* @prop {string} clientJS - jerverscripps to inject in skelPage for clients
httpdRoot: undefined,
bindJail: undefined,
-
-
/** @func
* @summary Start main HTTP server
* @desc starts up an HTTP or HTTPS server used for routing
if ('httpd' in this)
throw new Error('httpd already running')
if (conf.tls == undefined)
- this.httpd = require('http').createServer(this.httpdListener)
+ this.httpd = require('http').createServer((req, res) =>
+ this.httpdListener(req, res))
else if (!('keyFile' in conf.tls) || !('certFile' in conf.tls))
throw new Error('HTTPS requires a valid key and cert')
else
}
})
this.httpd =
- require('https').createServer(this.httpsOpts, this.httpdListener)
+ require('https').createServer(this.httpsOpts, (request,response) =>
+ this.httpdListener(request,response))
+ .listen(conf.port)
})
- this.httpd.listen(conf.port)
- this.httpdRoot =
- conf.httpdRoot ? require('path').normalize(conf.httpdRoot) : undefined
- while (this.httpdRoot[this.httpdRoot.length - 1] == require('path').sep)
- this.httpdRoot = this.httpdRoot.slice(0,-1)
- this.syncReads(conf.skelFile, conf.clientJS, conf.hostJS)
+ if (conf.httpdRoot) {
+ this.httpdRoot = require('path').normalize(conf.httpdRoot)
+ while (this.httpdRoot[this.httpdRoot.length - 1] == require('path').sep)
+ this.httpdRoot = this.httpdRoot.slice(0,-1)
+ }
+ this.syncReads([conf.skelFile, conf.clientJS, conf.hostJS])
.then((results) => {
this.skelPage = results[conf.skelFile].split('<!--STRAPP_SRC-->')
this.clientJS = results[conf.clientJS]
.catch((err) => {
console.log(err)
})
- console.log(`HTTP${(conf.tls == undefined) ? 'S' : ''} ` +
+ console.log(`HTTP${(conf.tls == undefined) ? '' : 'S'} ` +
`Server Started on port ${conf.port}${this.httpdRoot ?
`, serving files from ${this.httpdRoot}`:''}`)
},
&& this.bindJail != path)
throw new Error(`${routeName}:${path} jailed to ${this.bindJail}`)
if (require('fs').existsSync(path)) {
- this.route[routeName] = {
+ this.routes[routeName] = {
bind: {
path: path,
dir: require('fs').lstatSync(path).isDirectory()
dlog(`Received request ${request.method} ${request.url}`)
let htArgv = request.url.slice(1).split('?')
const routeName = htArgv[0].split('/')[0]
- const route = this.routes[routeName]
+ let route = this.routes[routeName]
/* If the route exists, check if we are a returning host or a new client */
if (route) {
if (route.bind) {
this.serveBind(response, route.bind, htArgv)
}
//TODO: auth better than this (ip spoofing is easy)
- else if (route.host == (request.headers['x-forwarded-for'] ||
+ // but this will require a more involved host-creation process
+ // that isn't just "give you a route if it's available" on visit
+ /* else if (route.origin == (request.headers['x-forwarded-for'] ||
request.connection.remoteAddress))
this.serveHost(response, route, htArgv)
- else
+ else */
this.serveClient(request, response, route)
}
/* If it's a valid routename that doesn't exist, make this client a host */
else if (this.validRoutes.test(routeName)) {
- route = this.createRoute(routeName, this.httpsOpts)
- this.serveHost(response, route, htArgv)
+ this.routes[routeName] = true
+ require('get-port')()
+ .then((port) => {
+ this.createHost(routeName, htArgv, port, request, response)
+ })
+ .catch((err) => {
+ delete this.routes[routeName]
+ console.log(err)
+ })
}
/* Try servicing files if we have a root directory for it */
else if (this.httpdRoot) {
},
/** @func
- * @summary Serve a route to an authorized http host
- * @desc services host application to the client, establishing a socket
- * @arg {http.ServerResponse} response - response object to use
- * @arg {Object} route - the route that belongs to this host
- * @arg {string[]} argv - vector of arguments sent to the host
- */
- serveHost: function (response, route, argv) {
- response.writeHead(200, { 'Content-Type': 'text/html' })
- response.write(`${this.skelPage[0]}` +
- `\tconst _strapp_port = ${route.port}\n` +
- `\tconst _strapp_protocol = ` +
- `${this.httpsOpts ? 'wss' : 'ws'}'\n` +
- `${this.hostJS}\n${this.skelPage[1]}`)
- response.end()
- },
-
- /** @func
- * @summary Create a new route
+ * @summary Create a new route for a host
* @desc makes a new route for the given route name
* @arg {string} routeName - name of the new route
- * @arg {string} host - Origin address from the request that made this
+ * @arg {string[]} argv - Origin address from the request that made this
* route (for security verification on the socket)
- * @arg {Object} [httpsOpts] - key and cert for tls
- * @returns {Object} a route object containing host, socket, and servers
+ * @arg {number|string} port - the port to listen on for websocket
+ * @arg {http.ClientRequest} request - host's request
+ * @arg {http.ServerResponse} response - responder
*/
- createRoute: function (routeName, host, httpsOpts) {
- dlog(`Creating ${httpsOpts ? 'TLS ' : ''}route ${routeName} from ${host}`)
- if (routeName in this.routes)
- throw new Error(`route ${routeName} already exists`)
- const httpd = httpsOpts
- ? require('https').createServer(httpsOpts)
+ createHost: function (routeName, argv, port, request, response) {
+ const origin = (request.headers['x-forwarded-for'] ||
+ request.connection.remoteAddress)
+ dlog(`New ${this.httpsOpts?'TLS ':''}route ${routeName}:${port}=>${origin}`)
+ const httpd = this.httpsOpts
+ ? require('https').createServer(this.httpsOpts)
: require('http').createServer()
const route = {
pendingResponses: new Map([]),
- host: host,
+ origin: origin,
httpd: httpd,
name: routeName,
- port: undefined,
+ port: port,
wsd: undefined,
socket: undefined
}
- require('get-port')().then((port) => {
- route.port = port
- route.httpd.listen(port)
- route.wsd = new require('ws').Server({
- server:route.httpd,
- verifyClient: (info) =>
- info.origin == host && (info.secure || !httpsOpts)
+ route.httpd.listen(port)
+ route.wsd = new (require('ws').Server)({ server: httpd })
+ .on('connection', (socket) => {
+ route.socket = socket
+ socket.on('message', (msg) =>
+ this.hostMessage(msg,route))
})
- route.wsd.on('connection', (socket) =>
- socket.on('message', (msg) =>
- this.hostMessage(msg,route)))
- })
- route.pendingResponses.addResponse = function (key, response) {
+ route.pendingResponses.addResponse = function (key, response_p) {
let responses = this.get(key) || []
- this.set(key, responses.push(response))
+ this.set(key, responses.push(response_p))
}
this.routes[routeName] = route
- return route
+ this.serveHost(response, route, argv)
+ },
+
+ /** @func
+ * @summary Serve a route to an authorized http host
+ * @desc services host application to the client, establishing a socket
+ * @arg {http.ServerResponse} response - response object to use
+ * @arg {Object} route - the route that belongs to this host
+ * @arg {string[]} argv - vector of arguments sent to the host
+ */
+ serveHost: function (response, route, argv) {
+ dlog(`Serving host ${route.origin}`)
+ response.writeHead(200, { 'Content-Type': 'text/html' })
+ response.write(`${this.skelPage[0]}` +
+ `\tconst _strapp_port = ${route.port}\n` +
+ `\tconst _strapp_protocol = ` +
+ `'${this.httpsOpts ? 'wss' : 'ws'}'\n` +
+ `${this.hostJS}\n${this.skelPage[1]}`)
+ response.end()
},
/** @func
* and responds to either the host or the client, or both. Commands
* are whitespace separated strings.
* Commands:
+ * Forward Payload to Client)
* < clientKey payload [header]
* Route 'payload' to the client identified by 'clientKey'.
* The optional 'header' argument is a stringified JSON object,
* which will be written to the HTTP response header
* In case of multiple requests from a single client, the
* oldest request will be serviced on arrival of message
+ * Translate SDP and Forward to Client)
+ * ^ clientKey sdp [header]
+ * Route the JSON object 'sdp' to the client, after translating
+ * for interop between browsers using planB or Unified. Other
+ * than the interop step, this is identical to the '<' command
+ * Error)
* ! errorMessage errorCode [offendingMessage]
* Notify host that an error has occured, providing a message
* and error code. 'offendingMessage', if present, is the
argv = argv.slice(1)
dlog(`Received host message from ${route.name}: ${command}`)
switch (command) {
+ case '^':
+ if (argv.length < 2) {
+ dlog(`Malformed '${command}' command from ${route.origin}`)
+ route.socket.send(`! "Insufficient arguments" 0 ${message}`)
+ break
+ }
+ argv[1] = JSON.parse(argv[1])
+ //TODO: interop step
+ argv[1] = JSON.stringify(argv[1])
+ //TODO: argv[1] = encryptForClient(argv[0], argv[1])
+ /* Fallthrough to '<' behavior after translating argv[1] */
case '<':
const response = route.pendingResponses.get(argv[0]).shift()
if (!response)
case '!':
if (argv.length === 3)
argv[0] += `\nIn message: ${argv[2]}`
- console.log(`Error[${route.host}|${argv[1]}]:${argv[0]}`)
+ console.log(`Error[${route.origin}|${argv[1]}]:${argv[0]}`)
+ break
+ default:
+ route.socket.send(`! "Unknown command '${command}'" 0 ${message}`)
+ dlog(`Host ${route.origin} send unknown command: ${message}`)
break
}
},
* @arg {Object} [readOpts] - options to pass to fs.readFile()
*/
syncReads: (files, readOpts) => new Promise((resolve,reject) => {
- dlog(`syncReads: ${files}`)
+ dlog(`syncing reads from ${files}`)
let count = 0
let results = {}
const read_cb = (fileName) => (err, data) => {
files.forEach((file) =>
require('fs').readFile(file, readOpts, read_cb(file)))
})
-
}
+
+module.exports = exports