From: ken Date: Sun, 9 Jul 2017 22:54:42 +0000 (+0000) Subject: test-ready X-Git-Url: https://git.kengrimes.com/?p=henge%2Fkiak.git;a=commitdiff_plain;h=c2c2c7b9b8be9a2e11576e8d26d044319019ad1d test-ready --- diff --git a/router.js b/router.js index 4326453..da11e8a 100644 --- a/router.js +++ b/router.js @@ -6,78 +6,195 @@ * @license AGPL-3.0 * @copyright Strapp.io */ -const fs = require('fs') const dlog = (msg) => console.log(msg) exports = { /** Regular expression for valid routes - * @var {Object.RegEx} validRoutes - matches valid route names + * @prop {Object.RegEx} validRoutes - matches valid route names */ validRoutes: /[a-zA-Z][a-zA-Z0-9\-_]*/, /** Parameters set on bootup (startHttpServer) - * @var {string[2]} skelPage - html document split in twain for JS injection - * @var {string} clientJS - jerverscripps to inject in skelPage for clients - * @var {string} hostJS - jerverscripps for the hosts - * @var {string} httpdRoot - a normalized path for http-servable files + * @prop {string[2]} skelPage - html document split in twain for JS injection + * @prop {string} clientJS - jerverscripps to inject in skelPage for clients + * @prop {string} hostJS - jerverscripps for the hosts + * @prop {string} httpdRoot - a normalized path for http-servable files + * @prop {string} bindJail - jail bindings to this path */ skelPage: undefined, clientJS: undefined, hostJS: undefined, httpdRoot: undefined, - - /** @func - * @summary Synchronize Reading Multiple Files - * @desc reads an array of files into an object, whose keys are the - * input filenames, and values are the data read - * @arg {string[]} files - array of file names to read - * @arg {Object} [readOpts] - options to pass to fs.readFile() + bindJail: undefined, + + + + /** @func + * @summary Start main HTTP server + * @desc starts up an HTTP or HTTPS server used for routing + * @arg {Object} conf - object containing configuration properties + * @prop {number|string} conf.port - local system port to bind to + * @prop {string} conf.skelFile - location of the skeleton HTML page + * @prop {string} conf.clientJS - client JS file + * @prop {string} conf.hostJS - host JS file + * @prop {string} [conf.httpdRoot] - root path of http-accessible files, if + * undefined no files are accessible + * @prop {Object} [conf.tls] - if present, startHttpServer will use tls + * @prop {string} [conf.tls.certFile] - tls certificate file + * @prop {string} [conf.tls.keyFile] - tls public key file */ - syncReads: (files, readOpts) => new Promise((resolve,reject) => { - dlog(`syncReads: ${files}`) - let count = 0 - let results = {} - const read_cb = (fileName) => (err, data) => { - if (err) - reject(err) - else - results[fileName] = data - if (++count === files.length) - resolve(results) + startHttpServer: function (conf) { + if ('httpd' in this) + throw new Error('httpd already running') + if (conf.tls == undefined) + this.httpd = require('http').createServer(this.httpdListener) + else if (!('keyFile' in conf.tls) || !('certFile' in conf.tls)) + throw new Error('HTTPS requires a valid key and cert') + else + this.syncReads([conf.tls.keyFile, conf.tls.certFile]).then((results) => { + Object.defineProperty(this, 'httpsOpts', { + value: { + key: results[conf.tls.keyFile], + cert: results[conf.tls.certFile] + } + }) + this.httpd = + require('https').createServer(this.httpsOpts, this.httpdListener) + }) + 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) + .then((results) => { + this.skelPage = results[conf.skelFile].split('') + this.clientJS = results[conf.clientJS] + this.hostJS = results[conf.hostJS] + }) + .catch((err) => { + console.log(err) + }) + console.log(`HTTP${(conf.tls == undefined) ? 'S' : ''} ` + + `Server Started on port ${conf.port}${this.httpdRoot ? + `, serving files from ${this.httpdRoot}`:''}`) + }, + + /** @func + * @summary Create a binding for the server + * @desc makes a new route which is bound to a file or a path. routes + * bound to files always serve that file, regardless of any + * additional path parameters provided by the URI + * @arg {string} routeName - the route to create + * @arg {string} path - the path to the file or directory to bind + */ + createBind: function (routeName, path) { + dlog(`Binding ${routeName} to ${path}`) + if (routeName in this.routes) + throw new Error(`route ${routeName} already exists`) + path = require('path').normalize(path) + if (this.bindJail + && path.indexOf(`${this.bindJail}/`) !== 0 + && this.bindJail != path) + throw new Error(`${routeName}:${path} jailed to ${this.bindJail}`) + if (require('fs').existsSync(path)) { + this.route[routeName] = { + bind: { + path: path, + dir: require('fs').lstatSync(path).isDirectory() + } + } } - if (readOpts == undefined) - readOpts = { encoding: 'utf8' } - files.forEach((file) => fs.readFile(file, readOpts, read_cb(file))) - }), - + else + throw new Error(`${path} not found, ${routeName} not bound`) + }, + /** @func - * @summary Main router listener - * @desc listens for http client requests and services routes/files - * @arg {http.ClientRequest} request - * @arg {http.ServerResponse} response + * @summary Router + * @desc listens for http client requests and services routes/files + * @arg {http.ClientRequest} request + * @arg {http.ServerResponse} response */ httpdListener: function (request,response) { dlog(`Received request ${request.method} ${request.url}`) - const htArgv = request.url.slice(1).split('?') + let htArgv = request.url.slice(1).split('?') const routeName = htArgv[0].split('/')[0] const route = this.routes[routeName] + /* If the route exists, check if we are a returning host or a new client */ if (route) { - if (route.host == (request.headers['x-forwarded-for'] || + if (route.bind) { + htArgv[0] = htArgv[0].replace(`${routeName}`,'') + if (htArgv[0][0] === '/') + htArgv[0] = htArgv[0].slice(1) + this.serveBind(response, route.bind, htArgv) + } + else if (route.host == (request.headers['x-forwarded-for'] || request.connection.remoteAddress)) this.serveHost(response, route, htArgv) 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) } + /* Try servicing files if we have a root directory for it */ + else if (this.httpdRoot) { + let realPath = require('path').join(this.httpdRoot, htArgv[0]) + if (realPath == this.httpdRoot) + realPath += '/index.html' + if (realPath.indexOf(`${this.httpdRoot}/`) == 0) { + const stat_cb = (err, stat) => { + if (err) { + response.writeHead(404) + response.end() + console.log(err) + } + else if (stat.isDirectory()) { + realPath += '/index.html' + require('fs').lstat(realPath, stat_cb) + } + else if (stat.isFile()) + this.serveFile(response, realPath) + else { + response.writeHead(403) + response.end() + } + } + require('fs').lstat(realPath, stat_cb) + } + else { + response.writeHead(400) + response.end() + } + } + /* Unhandled */ else { - this.serveFile(response, htArgv[0]) + response.writeHead(404) + response.end() } }, + /** @func + * @summary Serves a binding to a client + * @desc Resolves a binding and serves a response to the client + * @arg {http.ServerResponse} response - the response to use + * @arg {Object} bind - the binding to serve the client + * @arg {string[]} argv - path and arguments for the bind + */ + serveBind: function (response, bind, argv) { + dlog(`Serving binding ${bind.path}/${argv[0]}`) + if (bind.dir) { + if (argv[0] == '') + argv[0] = 'index.html' + this.serveFile(response, require('path').join(bind.path, argv[0])) + } + else + this.serveFile(response, bind.path) + }, + /** @func * @summary Serve a route to an http client * @desc routes may be bound to the filesystem, or to an outgoing host @@ -96,11 +213,6 @@ exports = { response.write(`${this.skelPage[0]}${this.clientJS}${this.skelPage[1]}`) response.end() break - if (pubKey) { - route.pendingResponses.addResponse(pubKey, response) - route.socket.send(`${type} ${pubKey}`) - } - break case 'ice-candidate-request': case 'ice-candidate-submission': case 'client-sdp-offer': @@ -154,25 +266,31 @@ exports = { const httpd = httpsOpts ? require('https').createServer(httpsOpts) : require('http').createServer() - const wsd = new require('ws').Server({ - server: httpd, - verifyClient: (info) => info.origin == host && (info.secure || !httpsOpts) - }) const route = { pendingResponses: new Map([]), host: host, httpd: httpd, - wsd: wsd, name: routeName, + port: undefined, + 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.wsd.on('connection', (socket) => + socket.on('message', (msg) => + this.hostMessage(msg,route))) + }) route.pendingResponses.addResponse = function (key, response) { let responses = this.get(key) || [] this.set(key, responses.push(response)) } - wsd.on('connection', (socket) => - socket.on('message', (msg) => - this.hostMessage(msg,route))) this.routes[routeName] = route return route }, @@ -231,80 +349,47 @@ exports = { * @desc reads files from the system to be distributed to clients, and * buffers recently accessed files * @arg {http.ServerResponse} response - the response object to use - * @arg {string} filePath - location of the file on disk to service + * @arg {string} filePath - relative location of the file */ - serveFile: function (response, filePath) { - if (this.clientCanAccessFile(filePath)) { - //TODO: Make a buffer to hold recently used files, and only read if we - // have to (don't forget to preserve mimetype) - fs.readFile(filePath, { encoding: 'utf8' }, (err, data) => { - if (err || data == undefined) - response.writeHead(404) - else { - response.writeHead(200, { - 'Content-Type': require('mime').lookup(filePath) - }) - response.write(data) - } - response.end() - }) - } - else { - response.writeHead(403) + serveFile: function (response, filePath, rootPath) { + //TODO: Make a buffer to hold recently used files, and only read if we + // have to (don't forget to preserve mimetype) + require('fs').readFile(filePath, { encoding: 'utf8' }, (err, data) => { + if (err || data == undefined) + response.writeHead(404) + else { + response.writeHead(200, { + 'Content-Type': require('mime').lookup(filePath) + }) + response.write(data) + } response.end() - } + }) }, - - /** @func - * @summary Test if client can access a file - * @return {Bool} true if the filePath is authorized - */ - clientCanAccessFile: (filePath) => require('path') - .normalize(this.httpdRoot + filePath) - .indexOf(this.httpdRoot + '/') === 0, - - /** @func - * @summary Start main HTTP server - * @desc starts up an HTTP or HTTPS server used for routing - * @arg {number|string} port - local system port to bind to - * @arg {string} skelFile - location of the skeleton HTML page to use - * @arg {string} clientJS - location of the client's JS distributable - * @arg {string} hostJS - location of the host's JS distributable - * @arg {string} [httpdRoot] - root path of http-accessible files, if not - * provided no files will be accessible - * @arg {Object} [tls] - if present, startHttpServer will start in tls - * mode. supported properties: - * 'certfile': certificate file location - * 'keyfile': key file location + + /** @func + * @summary Synchronize Reading Multiple Files + * @desc reads an array of files into an object, whose keys are the + * input filenames, and values are the data read + * @arg {string[]} files - array of file names to read + * @arg {Object} [readOpts] - options to pass to fs.readFile() */ - startHttpServer: function (port, skelFile, clientJS, hostJS, httpdRoot, tls) { - if ('httpd' in this) - throw new Error('httpd already running') - if (tls == undefined) - this.httpd = require('http').createServer(this.httpdListener) - else if (!('key' in tls) || !('cert' in tls)) - throw new Error('HTTPS requires a valid key and cert') - else - this.syncReads([tls.keyfile, tls.certfile]).then((results) => { - Object.defineProperty(this, 'httpsOpts', { - value: { - key: results[tls.keyfile], - cert: results[tls.certfile] - } - }) - this.httpd = - require('https').createServer(httpsOpts, this.httpdListener) - }) - this.httpd.listen(port) - this.httpdRoot = httpdRoot ? require('path').normalize(httpdRoot):undefined - while (this.httpdRoot[this.httpdRoot.length - 1] == require('path').sep) - this.httpdRoot = this.httpdRoot.slice(0,-1) - this.skelPage = fs.readFileSync('./skel.html', { encoding: 'utf8' }) - .split('') - this.hostJS = fs.readFileSync(hostJS) - this.clientJS = fs.readFileSync(clientJS) - console.log(`HTTP${(tls == undefined) ? 'S' : ''} ` + - `Server Started on port ${port}${this.httpdRoot ? ', serving' + - `files from ${this.httpdRoot}`:''}`) - } + syncReads: (files, readOpts) => new Promise((resolve,reject) => { + dlog(`syncReads: ${files}`) + let count = 0 + let results = {} + const read_cb = (fileName) => (err, data) => { + if (err) + reject(err) + else + results[fileName] = data + if (++count === files.length) + resolve(results) + } + if (readOpts == undefined) + readOpts = { encoding: 'utf8' } + files.forEach((file) => + require('fs').readFile(file, readOpts, read_cb(file))) + }) + } diff --git a/strapp.js b/strapp.js new file mode 100644 index 0000000..ebca88d --- /dev/null +++ b/strapp.js @@ -0,0 +1,27 @@ +/** +* @file Node entry and main driver +* @author Jordan Lavatai, Ken Grimes +* @version 0.0.1 +* @license AGPL-3.0 +* @copyright loljk 2017 +* @summ ary HTTP(S) Router that uses the first directory in the requested URL +* as the route name +*/ +const opts = require('./opts.js') +const router = require('./router.js') + +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'] + } +})