From 4ac972b80f19f55c2e221cda7c8a6aee920891be Mon Sep 17 00:00:00 2001 From: ken Date: Sat, 8 Jul 2017 21:02:38 +0000 Subject: [PATCH] WIP --- router.js | 357 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 346 insertions(+), 11 deletions(-) diff --git a/router.js b/router.js index 2f0441a..f17964b 100644 --- a/router.js +++ b/router.js @@ -4,19 +4,354 @@ * @author Ken Grimes * @version 0.0.1 * @license AGPL-3.0 - * @copyright jk software 2017 + * @copyright Strapp.io */ const fs = require('fs') -module.exports = (opts) => { - const startHttpd = (listener) => { - if (opts['no-tls']) - return require('http').createServer(listener) - if (fs.existsSync(opts['ca-cert'])) - - } - return { - httpd: startHttpd(), - wsd: startWsd() +const dlog = (msg) => console.log(msg) + +exports = { + /** Regular expression for valid routes + * @var {Object.RegEx} validRoutes - matches valid route names + */ + validRoutes: /[a-zA-Z][a-zA-Z0-9\-_]*/, + + /** @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() + */ + 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) => fs.readFile(file, readOpts, read_cb(file))) + }), + + /** @func + * @summary Main router listener + * @desc listens for client requests and services routes/files + * @arg {http.ClientRequest} request + * @arg {http.ServerResponse} response + */ + listener: function (request,response) { + dlog(`Received request ${request.method} ${request.url}`) + const htArgv = request.url.slice(1).split('?') + let routePath = htArgv[0].split('/')[0] + let routeName = routePath[0] + let route = this.routes[routeName] + if (route) { + if (route.host == (request.headers['x-forwarded-for'] || + request.connection.remoteAddress)) + this.serveHost(response, route, htArgv) + else + this.serveRoute(response, route) + } + else if (this.validRoutes.test(routeName)) { + route = this.createRoute(routeName, this.httpsOpts) + this.serveHost(response, route, htArgv) + } + else { + this.serveFile(response, routePath) + } + }, + + /** @func + * @summary Serve a route to an http client + * @desc routes may be bound to the filesystem, or to an outgoing host + * @arg {http.ServerResponse} response - response object to use + * @arg {string} routeName - name of the route to follow + */ + serveRoute: function (response, routeName) { + }, + + /** @func + * @summary Create a new route + * @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 + * route (for security verification on the socket) + * @arg {Object} [httpsOpts] - key and cert for tls + */ + 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) + : 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, + socket: undefined + } + wsd.on('connection', (socket) => + socket.on('message', (msg) => + this.hostMessage(msg,route))) + this.routes[routeName] = route + return route + }, + + /** @func + * @summary handle host message + * @desc receives a message from a host, handles the command (first character), + * and responds to either the host or the client, or both. Commands + * are whitespace separated strings. + * Commands: + * < clientKey requestID payload + * Route 'payload' to the client identified by 'clientKey', in + * response to the request identified by 'requestID' + * ! errorMessage errorCode [offendingMessage] + * Notify host that an error has occured, providing a message + * and error code. 'offendingMessage', if present, is the + * message received from the remote that triggered the error. + * @arg {string} message - raw string from the host + * @arg {Object} route - the route over + */ + hostMessage: function (message, route) { + const argv = message.split(' ') + const command = argv[0][0] + argv = argv.slice(1) + dlog(`Received host message from ${route.name}: ${command}`) + switch (command) { + case '<': + if (argv.length == 3) { + const response = route.pendingResponses.get(argv[0] + argv[1]) + response.writeHead(200, { 'Content-Type': 'application/octet-stream' }) + response.write(argv[2]) + response.end() + } + else { + route.socket.send(`! "Insufficient arguments" 0 ${message}`) + } + break + case '!': + if (argv.length == 3) + argv[0] += `\nIn message: ${argv[2]}` + console.log(`Error[${route.host}|${argv[1]}]:${argv[0]}`) + break + } + }, + + /** @func + * @summary Serve a file to an http client after a request + * @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 + */ + serveFile: function (response, filePath) { + }, + + /** @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 {Object} [tls] - if present, startHttpServer will start in tls + * mode. supported properties: + * 'certfile': certificate file location + * 'keyfile': key file location + */ + startHttpServer: function (port, tls) { + if ('httpd' in this) + throw new Error('httpd already running') + if (tls == undefined) + this.httpd = require('http').createServer(this.listener) + 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.listener) + }) + this.httpd.listen(port) + console.log(`HTTP${(tls == undefined) ? 'S' : ''} ` + + `Server Started on Port ${port}`) } } + +// exports.create = (opts) => { return { +// opts: opts, +// listener: function (request, response) { +// console.log('server handling request') +// const serveFile = (fPath) => { +// fs.readFile(fPath, { encoding: 'utf8' }, (err, data) => { +// if (err || data == undefined) { +// response.writeHead(404) +// response.end() +// } +// else { +// response.writeHead(200, { 'Content-Type': mime.lookup(fPath) }) +// response.write(data) +// response.end() +// } +// }) +// } +// const htArgv = request.url.slice(1).split("?") +// let routePath = htArgv[0].split('/') +// let routeName = routePath[0] + + +// if (routeName === '' || routeName === 'index.html') +// serveFile(opts['index']) +// else if (routeName in opts['bindings']) { +// let localPath = path.normalize(opts['bindings'][routeName].concat(path.sep + routePath.slice(1).join(path.sep))) +// if (localPath.includes(opts['bindings'][routeName])) { +// fs.readdir(localPath, (err, files) => { +// if (err) +// serveFile(localPath) +// else +// serveFile(`${localPath}/index.html`) +// }) +// } +// else { +// console.log(`SEC: ${localPath} references files not in route`) +// } +// } +// /* TODO: Handle reconnecting host */ +// else if (routeName in router.routes) { +// const route = router.routes[routeName] +// const clients = route['clients'] +// const headerData = request.headers['x-strapp-type'] + + + + +// /* Client is INIT GET */ +// if (headerData === undefined) { +// console.log('client init GET') +// response.writeHead(200, { 'Content-Type': 'text/html' }) +// response.write(`${router.skelPage[0]}${router.clientJS}${router.skelPage[1]}`) +// response.end() +// //TODO: if route.socket == undefined: have server delay this send until host connects +// // (this happens when a client connects to an active route with no currently-online host) +// } +// else if (headerData.localeCompare('ice-candidate-request') === 0) { +// console.log('Server: received ice-candidate-request from Client') +// let pubKey = request.headers['x-client-pubkey'] +// clients.set(pubKey, response) +// pubKey = '{ "pubKey": "' + pubKey + '" }' +// route.socket.send(pubKey) +// } +// else if (headerData.localeCompare('ice-candidate-submission') === 0) { +// console.log('Server: recieved ice-candidate-submission from Client') +// let data = [] +// request.on('data', (chunk) => { +// data.push(chunk) +// }).on('end', () => { +// console.log('Sending ice-candidate-submission to Host') +// data = Buffer.concat(data).toString(); +// clients.set(JSON.parse(data)['pubKey'], response) +// route.socket.send(data) +// }) +// } +// else if (headerData.localeCompare('client-sdp-offer') === 0){ /* Client sent offer, waiting for answer */ +// console.log('Server: Sending client offer to host') +// clients.set(JSON.parse(request.headers['x-client-offer'])['pubKey'], response) +// route.socket.send(request.headers['x-client-offer']) +// } else { +// console.log('Unhandled stuff') +// console.log(request.headers) +// } + +// } +// else { +// router.routes[routeName] = true +// const newRoute = {} +// newRoute.clients = new Map([]) +// newRoute.host = request.headers['x-forwarded-for'] || request.connection.remoteAddress +// getport().then( (port) => { +// newRoute.port = port +// if (opts['no-tls']) +// newRoute.httpd = http.createServer() +// else +// newRoute.httpd = https.createServer(router.httpsOpts) +// newRoute.httpd.listen(newRoute.port) +// newRoute.wsd = new ws.Server( { server: newRoute.httpd } ) +// newRoute.wsd.on('connection', (sock) => { +// console.log(`${routeName} server has been established`) +// newRoute.socket = sock + +// /* Handle all messages from host */ +// sock.on('message', (hostMessage) => { +// hostMessage = JSON.parse(hostMessage) +// response = newRoute.clients.get(hostMessage['clientPubKey']) + +// /* If the host response is a answer */ +// if (hostMessage['cmd'].localeCompare('< sdp pubKey') === 0) { +// console.log('Server: Sending host answer to client') +// response.writeHead(200, { 'Content-Type': 'application/json' }) +// response.write(JSON.stringify(hostMessage)) +// response.end() +// } +// else if (hostMessage['cmd'].localeCompare('< ice pubKey') === 0){ +// /* if the host response is an ice candidate */ +// console.log('Server: Handling host ICE message') +// let iceState = hostMessage['iceState'] +// /* If there are any ice candidates, send them back */ +// switch(iceState) { +// case "a": +// response.writeHead('200', {'x-strapp-type': 'ice-candidate-available'}) +// response.write(JSON.stringify(hostMessage)) +// response.end() +// break +// case "g": +// console.log('Server: Host is still gathering candidates, keep trying') +// response.writeHead('200', {'x-strapp-type': 'ice-state-gathering'}) +// response.write(JSON.stringify(hostMessage)) +// response.end() +// break +// case "c": +// console.log('Server: Host has completed gathering candidates') +// response.writeHead('200', {'x-strapp-type': 'ice-state-complete'}) +// response.write(JSON.stringify(hostMessage)) +// response.end() +// break +// default: +// console.log('unhandled iceState from host') +// break +// } +// } + +// }) +// }) + +// console.log(`Listening for websocket ${newRoute.host} on port ${newRoute.port}`) +// router.routes[routeName] = newRoute +// }).then(() => { +// response.writeHead(200, { 'Content-Type': 'text/html' }) +// response.write(`${router.skelPage[0]}` + +// `\tconst _strapp_port = ${newRoute.port}\n` + +// `\tconst _strapp_protocol = '${router.wsProtocol}'\n` + +// `${router.hostJS}\n${router.skelPage[1]}`) +// response.end() +// }) +// } +// }, +// startHttpd: () => require('http').createServer(this.listener) +// } + -- 2.18.0