├── nodelb.js ├── throttleIP.js ├── package.json ├── clusterSwitch.js ├── errorLog.js ├── wsproxy.js ├── originServer.js ├── wsproxypool.js ├── loadBalancer.js └── README.md /nodelb.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const threads = require('./clusterSwitch'); 3 | const rp = require('./loadBalancer'); 4 | const redis = require('./originServer'); 5 | const ws = require('./wsproxy'); 6 | const wspool = require('./wsproxypool'); 7 | const errorLog = require('./errorLog'); 8 | 9 | const lb = {}; 10 | 11 | const lib = { 12 | threads, 13 | rp, 14 | redis, 15 | ws, 16 | wspool, 17 | errorLog, 18 | }; 19 | 20 | lb.deploy = (featureLib, options, cb) => { 21 | return lib[featureLib](options, cb); 22 | }; 23 | 24 | module.exports = lb; 25 | -------------------------------------------------------------------------------- /throttleIP.js: -------------------------------------------------------------------------------- 1 | const ipAddresses = {}; 2 | 3 | const ddosStopper = (bReq, bRes, delay, requests) => { 4 | const ip = (bReq.headers['x-forwarded-for'] || '').split(',')[0] || bReq.connection.remoteAddress; 5 | // if ip address does exist in our list of client ip addresses 6 | // we want to make sure that they cannot make a request within 100 ms of their previous request 7 | ipAddresses[ip] ? ipAddresses[ip]++ : ipAddresses[ip] = 1; 8 | setTimeout(() => delete ipAddresses[ip], delay); 9 | 10 | if (ipAddresses[ip] > requests) { 11 | bRes.statusCode = 500; 12 | return bRes.end('500 Server Error'); 13 | } 14 | }; 15 | 16 | module.exports = ddosStopper; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodexchange", 3 | "version": "1.5.0", 4 | "description": "node library for scaling and load balancing servers", 5 | "main": "nodexchange.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/DataHiveDJW/nodexchange" 12 | }, 13 | "keywords": [ 14 | "load", 15 | "balancer", 16 | "reverse", 17 | "proxy", 18 | "node", 19 | "server", 20 | "scaling", 21 | "server", 22 | "cache" 23 | ], 24 | "author": "djw", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/DataHiveDJW/nodexchange/issues" 28 | }, 29 | "homepage": "https://github.com/DataHiveDJW/nodexchange#readme", 30 | "dependencies": { 31 | "crypto": "0.0.3", 32 | "redis": "^2.7.1", 33 | "ws": "^2.3.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /clusterSwitch.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | 3 | const clusterSwitch = {}; 4 | 5 | /** 6 | * Initialize Node Clusters 7 | * @param {Object} -- Server Object 8 | * @param {Number} -- Server Port 9 | * @public 10 | */ 11 | 12 | clusterSwitch.init = (server, port) => { 13 | if (cluster.isMaster) { 14 | const numThreads = require('os').cpus().length; 15 | // creates workers for threads based on threads 16 | for (let i = 0; i < numThreads; i += 1) { 17 | cluster.fork(); 18 | } 19 | // Lets user know ID of thread workers 20 | cluster.on('online', (thread) => console.log('Thread ' + thread.process.pid + ' is online')); 21 | // When worker dies while executing code, creates new one. 22 | cluster.on('exit', (thread, code, signal) => { 23 | // to inform the user of dead threads and the information behind it 24 | // refer to cluster docs for what these mean 25 | console.log(`thread ${thread.process.pid} died with code: ${code} and signal: ${signal}`); 26 | cluster.fork(); 27 | }); 28 | } else { 29 | server.listen(port); 30 | console.log('Server running at port ' + port); 31 | } 32 | }; 33 | 34 | module.exports = () => clusterSwitch.init; 35 | -------------------------------------------------------------------------------- /errorLog.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const errorLog = {}; 4 | 5 | /** 6 | * Initialize Error Log 7 | * @param {String} -- File Path 8 | * @public 9 | * Example: 10 | * `errorLog.Init(path.join(__dirname + '/healthCheck.log'));` 11 | */ 12 | 13 | errorLog.init = (path) => { 14 | if (path === null) throw 'Error: A file path is a required parameter for errorLog.init' 15 | errorLog.path = path; 16 | } 17 | 18 | /** 19 | * Begins writing of errors to file path created in errorLog.Init 20 | * @param {Object} -- Error Object 21 | * @return {File} -- Overwrites old file, persisting old error data and adding new data together 22 | * @public 23 | */ 24 | 25 | const queue = []; 26 | 27 | errorLog.write = (error) => { 28 | queue.push(error); 29 | } 30 | 31 | // let logStream = fs.createWriteStream(errorLog.path, {flags:'a'}); 32 | 33 | // const errorStream = (stream) => { 34 | // logStream.write(stream); 35 | // } 36 | 37 | errorLog.readWrite = () => { 38 | if (errorLog.path && queue.length > 0) { 39 | let error = queue.shift(); 40 | fs.readFile(errorLog.path, (err, data) => { 41 | if (err) console.log(err, 'Read File error'); 42 | let date = new Date(); 43 | fs.writeFile(errorLog.path, data ? data + date + ': ' + error + '\n' : date + ': ' + error + '\n', 'utf-8', (err) => { 44 | if (err) console.log(err, 'Write File error'); 45 | }) 46 | }) 47 | } else { 48 | return; 49 | } 50 | } 51 | 52 | module.exports = (firstTime = true) => { 53 | if (firstTime) setInterval(() => errorLog.readWrite(), 2000); 54 | return errorLog; 55 | } -------------------------------------------------------------------------------- /wsproxy.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const eventEmitter = require('events'); 3 | 4 | const findTarget = (servers) => { 5 | // console.log(servers[0].openSockets, servers[1].openSockets, servers[2].openSockets); 6 | let target = null; 7 | for (let i = 0; i < servers.length; i += 1) { 8 | if (target === null && servers[i].active) target = servers[i]; 9 | if (target.openSockets > servers[i].openSockets && servers[i].active) { 10 | target = servers[i]; 11 | } 12 | } 13 | return target; 14 | }; 15 | 16 | 17 | // findTarget 18 | // check to see if pool identifier has been allocated to a server yet 19 | 20 | class WsProxy extends eventEmitter { 21 | constructor() { 22 | super(); 23 | this.tunnels = []; 24 | this.init = (server, options, isSecure = false) => { 25 | if (options === null || options === undefined) throw 'Error: Options parameter not provided' 26 | const wss = new WebSocket.Server({ server }); 27 | wss.on('connection', (clientWs) => { 28 | // console.log(options[0].openSockets, options[1].openSockets, options[2].openSockets); 29 | // console.log(this.tunnels.length + ' open sockets'); 30 | const messageQueue = []; 31 | let tunnelOpen = false; 32 | const targetServer = findTarget(options); 33 | targetServer.openSockets += 1; 34 | const targetWs = new WebSocket((isSecure ? 'wss://' : 'ws://').concat(targetServer.hostname).concat(':').concat(targetServer.port)); 35 | clientWs.on('message', (message) => { 36 | if (tunnelOpen) { 37 | targetWs.send(message); 38 | // console.log(message); 39 | } else { 40 | messageQueue.push(message); 41 | } 42 | }); 43 | targetWs.on('open', () => { 44 | this.tunnels.push({ 45 | client: clientWs, 46 | targetSocket: targetWs, 47 | targetServer: { hostname: targetServer.hostname, port: targetServer.port }, 48 | }); 49 | while (messageQueue.length > 0) { 50 | targetWs.send(messageQueue.shift()); 51 | } 52 | tunnelOpen = true; 53 | targetWs.on('message', (message) => { 54 | clientWs.send(message); 55 | }); 56 | clientWs.on('close', () => { 57 | // console.log('client disconnected'); 58 | targetWs.close(); 59 | }); 60 | targetWs.on('close', () => { 61 | targetServer.openSockets -= 1; 62 | let serverIndex; 63 | const currServer = this.tunnels.filter((item, i) => { 64 | if (item.targetSocket === targetWs) { 65 | serverIndex = i; 66 | return true; 67 | } 68 | }); 69 | // console.log(currServer); 70 | // console.log(currServer[0].targetServer.port, ' disconnected'); 71 | // console.log(this.tunnels.length + ' open sockets'); 72 | this.tunnels.splice(serverIndex, 1); 73 | clientWs.close(); 74 | }); 75 | }); 76 | }); 77 | }; 78 | } 79 | } 80 | 81 | module.exports = () => new WsProxy(); 82 | -------------------------------------------------------------------------------- /originServer.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | const crypto = require('crypto'); 3 | 4 | const originServer = {}; 5 | 6 | originServer.checkSession = (sessionId, cb) => { 7 | originServer.client.get(sessionId, (err, reply) => { 8 | if (err) return cb(err); 9 | return cb(null, reply); 10 | }); 11 | }; 12 | 13 | originServer.setSession = (sessionId, cookieKey, cb) => { 14 | originServer.client.set(sessionId, cookieKey, (err, reply) => { 15 | if (err) return cb(err); 16 | return cb(null, reply); 17 | }); 18 | }; 19 | 20 | originServer.init = (options) => { 21 | if (!options.port || !options.host) throw 'Error: Options parameter needs BOTH a Options.port property & a Options.host property'; 22 | 23 | originServer.client = redis.createClient(options.port, options.host); 24 | originServer.client.on('connect', () => { 25 | console.log('redis connected'); 26 | }); 27 | return originServer; 28 | }; 29 | 30 | const cookieParse = (cookies, target = null) => { 31 | if (!cookies) return null; 32 | 33 | const cookieObj = {}; 34 | let arr = cookies.split(';'); 35 | arr = arr.map((value) => value.trim(' ')); 36 | 37 | let cookieSplit; 38 | for (let i = 0; i < arr.length; i += 1) { 39 | cookieSplit = arr[i].split('='); 40 | if (target === null) { 41 | cookieObj[cookieSplit[0]] = cookieSplit[1]; 42 | } else if (cookieSplit[0] === target) return cookieSplit[1]; 43 | } 44 | return target === null ? cookieObj : null; 45 | }; 46 | 47 | originServer.verifySession = (req, cookieKey, cb) => { 48 | const key = cookieParse(req.headers.cookie, cookieKey); 49 | if (key) { 50 | originServer.checkSession(key, (err, reply) => { 51 | if (reply === cookieKey) return cb(true); 52 | return cb(false); 53 | }); 54 | } else return cb(false); 55 | }; 56 | 57 | const hash = (string) => { 58 | const generatedHash = crypto.createHash('sha256') 59 | .update(string, 'utf8') 60 | .digest('hex'); 61 | return generatedHash; 62 | }; 63 | 64 | originServer.authenticate = (req, res, cookieKey, uniqueId, cb) => { 65 | if (uniqueId === null || uniqueId === undefined) throw 'Please provide an ID to hash'; 66 | if (!cookieKey || cookieKey === undefined) throw 'Please provide a key'; 67 | 68 | const key = hash(uniqueId); 69 | originServer.setSession(key, cookieKey, (err, reply) => { 70 | res.writeHead(200, { 71 | 'Set-Cookie': cookieKey.concat('=').concat(key), 72 | 'Content-Type': 'application/JSON', 73 | }); 74 | cb(err, reply); 75 | }); 76 | }; 77 | 78 | module.exports = originServer.init; 79 | 80 | 81 | 82 | // Main objects/methods for docs: 83 | 84 | 85 | // Initialization: 86 | // ----------- 87 | // const originServer = require('./../serverLb/library/originServer'); 88 | // const options = { 89 | // host: '127.0.0.1', 90 | // port: 6379, 91 | // }; 92 | // const rs = originServer(options); 93 | // ----------- 94 | 95 | 96 | 97 | // Methods: 98 | // ----------- 99 | // rs.authenticate(req, res, cookieKey, uniqueId, cb) 100 | // rs.verifySession(req, cookieKey, cb) 101 | // ----------- 102 | -------------------------------------------------------------------------------- /wsproxypool.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const eventEmitter = require('events'); 3 | 4 | class WsProxyPool extends eventEmitter { 5 | constructor() { 6 | super(); 7 | this.tunnels = []; 8 | this.pools = {}; 9 | 10 | this.findTarget = (servers, poolId) => { 11 | // console.log(servers[0].openSockets, servers[1].openSockets, servers[2].openSockets); 12 | let target = null; 13 | for (let i = 0; i < servers.length; i += 1) { 14 | if (target === null && servers[i].active) target = servers[i]; 15 | else if (target.openSockets > servers[i].openSockets && servers[i].active) { 16 | target = servers[i]; 17 | } 18 | } 19 | this.pools[poolId] = target; 20 | console.log(this.pools); 21 | target.openSockets = 1; 22 | return target; 23 | }; 24 | 25 | this.createTunnel = (clientWs, options, poolId, isSecure) => { 26 | const messageQueue = []; 27 | let tunnelOpen = false; 28 | let targetServer; 29 | if (this.pools[poolId]) { 30 | targetServer = this.pools[poolId]; 31 | targetServer.openSockets += 1; 32 | } else targetServer = this.findTarget(options, poolId); 33 | const targetWs = new WebSocket((isSecure ? 'wss://' : 'ws://').concat(targetServer.hostname).concat(':').concat(targetServer.port)); 34 | clientWs.on('message', (message) => { 35 | if (tunnelOpen) { 36 | targetWs.send(message); 37 | } else { 38 | messageQueue.push(message); 39 | } 40 | }); 41 | targetWs.on('open', () => { 42 | this.tunnels.push({ 43 | client: clientWs, 44 | targetSocket: targetWs, 45 | targetServer: { hostname: targetServer.hostname, port: targetServer.port }, 46 | }); 47 | while (messageQueue.length > 0) { 48 | targetWs.send(messageQueue.shift()); 49 | } 50 | tunnelOpen = true; 51 | targetWs.on('message', (message) => { 52 | clientWs.send(message); 53 | }); 54 | clientWs.on('close', () => { 55 | // console.log('client disconnected'); 56 | targetWs.close(); 57 | }); 58 | targetWs.on('close', () => { 59 | targetServer.openSockets -= 1; 60 | let serverIndex; 61 | const currServer = this.tunnels.filter((item, i) => { 62 | if (item.targetSocket === targetWs) { 63 | serverIndex = i; 64 | return true; 65 | } 66 | }); 67 | // console.log(currServer); 68 | // console.log(currServer[0].targetServer.port, ' disconnected'); 69 | // console.log(this.tunnels.length + ' open sockets'); 70 | this.tunnels.splice(serverIndex, 1); 71 | clientWs.close(); 72 | }); 73 | }); 74 | }; 75 | 76 | this.init = (server, options, isSecure = false) => { 77 | if (options === null || options === undefined) throw 'Error: Options parameter not provided'; 78 | const wss = new WebSocket.Server({ server }); 79 | wss.on('connection', (clientWs) => { 80 | // console.log(options[0].openSockets, options[1].openSockets, options[2].openSockets); 81 | // console.log(this.tunnels.length + ' open sockets'); 82 | let poolId = null; 83 | clientWs.on('message', (message) => { 84 | if (poolId === null) { 85 | let pMessage = {}; 86 | try { 87 | pMessage = JSON.parse(message); 88 | } catch (err) { 89 | console.log('Error: Websocket message dropped. All messages will be dropped until receiving object with key "socketPoolId" when websocket pool feature is deployed.'); 90 | } finally { 91 | if (pMessage.socketPoolId !== undefined) { 92 | poolId = pMessage.socketPoolId; 93 | this.createTunnel(clientWs, options, poolId, isSecure); 94 | } 95 | } 96 | } 97 | }); 98 | }); 99 | }; 100 | } 101 | } 102 | 103 | module.exports = () => new WsProxyPool(); 104 | -------------------------------------------------------------------------------- /loadBalancer.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const http = require('http'); 3 | const EventEmitter = require('events'); 4 | const el = require('./errorLog'); 5 | const throttleIP = require('./throttleIP'); 6 | 7 | const errorLog = el(false); 8 | 9 | class LoadBalancer extends EventEmitter { 10 | constructor() { 11 | super(); 12 | this.algo = 'lc'; 13 | this.cache = {}; 14 | this.options = []; 15 | this.routes = {}; 16 | this.getCache = this.getCache.bind(this); 17 | this.addOptions = this.addOptions.bind(this); 18 | this.setRoutes = this.setRoutes.bind(this); 19 | this.healthCheck = this.healthCheck.bind(this); 20 | this.clearCache = this.clearCache.bind(this); 21 | this.isStatic = this.isStatic.bind(this); 22 | this.shouldCache = this.shouldCache.bind(this); 23 | this.cacheContent = this.cacheContent.bind(this); 24 | this.init = this.init.bind(this); 25 | this.lbInit = this.lbInit.bind(this); 26 | }; 27 | 28 | /** 29 | * Sets Load-Balancing Algorithm to Round-Robin style 30 | * @public 31 | */ 32 | 33 | setAlgoRR() { 34 | this.algo = 'rr'; 35 | } 36 | 37 | /** 38 | * Sets Load-Balancing Algorithm to Least Connection style 39 | * @public 40 | */ 41 | 42 | setAlgoLC() { 43 | this.algo = 'lc'; 44 | } 45 | 46 | /** 47 | * Retrieves current cache from library and returns out to user 48 | * @public 49 | */ 50 | 51 | getCache() { 52 | return this.cache; 53 | } 54 | 55 | /** 56 | * Stores desired application routes for reverse-proxy to cache responses for 57 | * @param {Array} -- Nested Array of Request Type & Route 58 | * @public 59 | * Example: 60 | * `rp.setRoutes([['GET', '/'], ['GET', '/html']]);` 61 | */ 62 | 63 | setRoutes(routes) { 64 | if (routes === null || routes === undefined) throw 'Set Routes received input that was either null or undefined'; 65 | if (!Array.isArray(routes)) throw 'Error: setRoutes expects an input of type "Array", per documentation it expects a nested Array'; 66 | 67 | for (let i = 0; i < routes.length; i++) { 68 | let temp = routes[i][0].concat(routes[i][1]); 69 | this.routes[temp] = true; 70 | } 71 | }; 72 | 73 | /** 74 | * Stores specific target server hostname and port information to direct requests to 75 | * @param {Object} -- Options object includes specific hostname and port info. for target servers 76 | * @public 77 | * Example: 78 | * `const options = []; 79 | * for (let i = 2; i < process.argv.length; i += 2) { 80 | * options.push({ 81 | * hostname: process.argv[i], 82 | * port: process.argv[i + 1], 83 | * }); 84 | * }` 85 | */ 86 | 87 | addOptions(options) { 88 | if (!Array.isArray(options)) throw 'Error: addOptions expects an input of type "Array"'; 89 | if (options === null || options === undefined) throw 'Error: Options is a required parameter for addOptions'; 90 | 91 | for (let i = 1; i < options.length; i += 1) { 92 | options[i].openSockets = 0; 93 | options[i].openRequests = 0; 94 | options[i].active = true; 95 | this.options.push(options[i]); 96 | } 97 | } 98 | 99 | /** 100 | * Pings all target servers on an interval (if provided) or when method is called 101 | * @param {Number} -- Interval in miliseconds setTimeout (Default: Null) 102 | * @param {Boolean} -- True or False if server needs SSL to check HTTPS requests (Default: False) 103 | * @public 104 | */ 105 | 106 | healthCheck(interval = null, ssl = false) { 107 | /** 108 | * Healthcheck sends dummy requests to servers to check server health 109 | * Alters 'active' property boolean value based on result of health check 110 | */ 111 | const options = this.options; 112 | 113 | let protocol; 114 | ssl ? protocol = https : protocol = http; 115 | 116 | // Loops through servers in options & sends mock requests to each 117 | for (let i = 0; i < options.length; i += 1) { 118 | protocol.get(options[i], (res) => { 119 | if (res.statusCode > 100 && res.statusCode < 400) { 120 | if (options[i].active === false) options[i].active = true; 121 | } else { 122 | options[i].active = false; 123 | } 124 | res.on('end', () => { 125 | // response from server received, reset value to true if prev false 126 | if (options[i].active === false) options[i].active = true; 127 | }); 128 | }).on('error', (e) => { 129 | e.name = "HealthCheck Error"; 130 | errorLog.write(e); 131 | // if error occurs, set boolean of 'active' to false to ensure no further requests to server 132 | if (e) options[i].active = false; 133 | }); 134 | } 135 | //if interval param is provided, repeats checks on provided interval 136 | if (interval !== null) { 137 | setTimeout(() => { 138 | this.healthCheck(interval, ssl); 139 | }, interval); 140 | } 141 | } 142 | 143 | /** 144 | * Clears reverse-proxy internal cache 145 | * @param {Number} -- Interval in miliseconds for setTimeout (Default: Null) 146 | * @public 147 | */ 148 | 149 | clearCache(interval = null, cb = null) { 150 | if (interval !== null) { 151 | setTimeout(() => { 152 | this.clearCache(interval, cb); 153 | }, interval); 154 | } 155 | this.cache = {}; 156 | if (cb) return cb(); 157 | } 158 | 159 | /** 160 | * Checks if request is considered 'static' - HTML, CSS, JS file 161 | * Method is not available to users 162 | * @param {Object} -- Browser request object 163 | * @return {Boolean} -- True/False if 'static' 164 | * @private 165 | */ 166 | 167 | isStatic(bReq) { 168 | // Returns true if matching any of 3 file types, otherwise returns false 169 | return bReq.url.slice(bReq.url.length - 5) === '.html' || bReq.url.slice(bReq.url.length - 4) === '.css' || bReq.url.slice(bReq.url.length - 3) === '.js'; 170 | }; 171 | 172 | /** 173 | * Checks return result from isStatic method & if route exists in routes object 174 | * Returns boolean based off result of either returning true 175 | * Method is not available to users 176 | * @param {Object} -- Browser request object 177 | * @param {Object} -- Routes object 178 | * @return {Boolean} -- True/false if 'static' or exists in routes object 179 | * @private 180 | */ 181 | 182 | shouldCache(bReq, routes) { 183 | return this.isStatic(bReq) || routes[bReq.method + bReq.url]; 184 | }; 185 | 186 | /** 187 | * Caches response in reverse-proxy internal cache for future identical requests 188 | * Calls shouldCache and awaits boolean return value 189 | * Method is not available to users 190 | * @param {Object} -- Response body 191 | * @param {Object} -- Internal Cache 192 | * @param {Object} -- Browser request object 193 | * @param {Object} -- Routes object 194 | * @private 195 | */ 196 | 197 | cacheContent(body, cache, bReq, routes) { 198 | if (this.shouldCache(bReq, routes)) cache[bReq.method + bReq.url] = body; 199 | } 200 | 201 | /** 202 | * Determines type of request protocol: HTTP or HTTPS 203 | * If request is not to be cached, pipe through to target servers, else cache compiled response 204 | * @param {Object} -- Options object 205 | * @param {Object} -- Response Body 206 | * @param {Object} -- Specific server object 207 | * @param {Object} -- Internal Cache 208 | * @param {Object} -- Routes object 209 | * @param {Object} -- Browser request object 210 | * @param {Object} -- Browser response object 211 | * @param {Boolean} -- SSL boolean value 212 | * @public 213 | */ 214 | 215 | determineProtocol(options, body, target, cache, routes, bReq, bRes, ssl) { 216 | let protocol; 217 | ssl ? protocol = https : protocol = http; 218 | return protocol.request(options, (sRes) => { 219 | bRes.writeHead(200, sRes.headers); 220 | if (!this.shouldCache(bReq, routes)) { 221 | sRes.pipe(bRes); 222 | target.openRequests -= 1; 223 | } else { 224 | sRes.on('data', (data) => { 225 | body += data; 226 | }); 227 | sRes.on('end', (err) => { 228 | if (err) errorLog.write(err); 229 | target.openRequests -= 1; 230 | this.cacheContent(body, cache, bReq, routes); 231 | bRes.end(body); 232 | }); 233 | } 234 | }); 235 | } 236 | 237 | /** 238 | * Initalize Load-balancer / Reverse-proxy 239 | * @param {Object} -- Browser request object 240 | * @param {Object} -- Browser response object 241 | * @param {Boolean} -- SSL boolean (if using SSL) (Default: False) 242 | * @param {Number} -- Delay provided for DDOS throttle (Default: 0) 243 | * @param {Number} -- Request count for DDOS throttle (Default: 0) 244 | * @public 245 | */ 246 | 247 | init(bReq, bRes, ssl = false, delay = 0, requests = 0) { 248 | if (!bReq) throw 'Error: The browser request was not provided to init'; 249 | if (!bRes) throw 'Error: The browser response was not provided to init'; 250 | if ((delay > 0 && requests <= 0) || (delay <= 0 && requests > 0)) { 251 | throw 'Error: both delay and requests need to be defined to throttle ip addresses'; 252 | } 253 | if (delay > 0 && requests > 0 && throttleIP(bReq, bRes, delay, requests) !== undefined) { 254 | return throttleIP(bReq, bRes, delay, requests) 255 | } 256 | const options = this.options; 257 | const cache = this.cache; 258 | const routes = this.routes; 259 | 260 | if (cache[bReq.method + bReq.url]) { 261 | // check cache if response exists, else pass it on to target servers 262 | this.emit('cacheRes'); 263 | bRes.end(cache[bReq.method + bReq.url]); 264 | } else { 265 | this.emit('targetRes'); 266 | let body = ''; 267 | // checks for valid request & edge case removes request to '/favicon.ico' 268 | if (bReq.url !== null && bReq.url !== '/favicon.ico') { 269 | let INDEXTEST = 0; 270 | let target = null; 271 | options.push(options.shift()); 272 | if (this.algo === 'rr') { 273 | while (!options[0].active) options.push(options.shift()); 274 | target = options[0]; 275 | } else if (this.algo === 'lc') { 276 | while (!options[0].active) options.push(options.shift()); 277 | const min = {}; 278 | min.reqs = options[0].openRequests; 279 | min.option = 0; 280 | for (let i = 1; i < options.length; i += 1) { 281 | if (options[i].openRequests < min.reqs && options[i].active) { 282 | min.reqs = options[i].openRequests; 283 | min.option = i; 284 | INDEXTEST = i; 285 | } 286 | } 287 | target = options[min.option]; 288 | } 289 | 290 | const serverOptions = {}; 291 | serverOptions.method = bReq.method; 292 | serverOptions.path = bReq.url; 293 | serverOptions.headers = bReq.headers; 294 | serverOptions.hostname = target.hostname; 295 | serverOptions.port = target.port; 296 | 297 | target.openRequests += 1; 298 | 299 | const originServer = this.determineProtocol(serverOptions, body, target, cache, routes, bReq, bRes, ssl); 300 | 301 | originServer.on('error', e => { 302 | e.name = 'Target Server Error'; 303 | errorLog.write(e); 304 | }); 305 | bReq.pipe(originServer); 306 | } 307 | } 308 | } 309 | 310 | /** 311 | * Injects target server collection (options) into library 312 | * @param {Object} -- Options object 313 | * @param {Function} -- Callback exists if you want to take an action before server start/after injection 314 | * @return {Object} -- Returns loadBalancer object 315 | * @private 316 | */ 317 | 318 | lbInit(options, cb) { 319 | if (options === null || options === undefined) throw 'Error: Options is a required parameter for this method'; 320 | this.options = options; 321 | this.options.forEach((option) => { 322 | option.openSockets = 0; 323 | option.openRequests = 0; 324 | option.active = true; 325 | }); 326 | if (cb) cb(); 327 | return this; 328 | } 329 | } 330 | 331 | const loadBalancer = new LoadBalancer(); 332 | 333 | module.exports = loadBalancer.lbInit; 334 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ``` 4 | $ npm install nodexchange 5 | ``` 6 | 7 | # Features 8 | 9 | ### 1. Reverse-Proxy features such as : 10 | 11 | * Internal caching of static files and desired route responses 12 | 13 | * Internal health checks 14 | 15 | * Cache clearing mechanisms 16 | 17 | ### 2. Load-Balancing features such as : 18 | 19 | * Least-connections / round-robin load-balancing algorithm for http/https and web sockets 20 | 21 | ### 3. Additional Features : 22 | 23 | * Error logging 24 | 25 | * IP throttling 26 | 27 | * Direct compability with Redis for session storage 28 | 29 | * Direct compability with the Node Cluster module for multi-threading Node instances 30 | 31 | # Reverse Proxy Setup 32 | 33 | Include our library in your application using: 34 | 35 | ```javascript 36 | const lb = require(‘nodexchange’); 37 | ``` 38 | 39 | ## Options — 40 | 41 | Options is a collection of the addresses to the target servers, consisting of their hostnames and ports. 42 | In order to create the reverse proxy object, it will need this input upon deployment. 43 | 44 | ### Example: 45 | 46 | ```javascript 47 | const options = [ 48 | { 49 | hostname: '127.0.0.1', 50 | port: 3000, 51 | }, 52 | { 53 | hostname: '127.0.0.1', 54 | port: 4000, 55 | }, 56 | { 57 | hostname: '127.0.0.1', 58 | port: 5000, 59 | }, 60 | ]; 61 | ``` 62 | 63 | ## lb.deploy ( string, array( options ), function[optional] ) — 64 | 65 | lb.deploy triggers the creation of the reverse proxy object. 66 | 67 | **First parameter (string):** is a configuration argument for the reverse proxy server which in this case must be: ’rp’ 68 | 69 | **Second parameter (array):** will be the options collection created previously (see above) 70 | 71 | **Third parameter (function) - optional:** callback function executed upon initializing objects for reverse-proxy 72 | 73 | **‘rp’ is the only valid string input for the first parameter to trigger your reverse proxy setup** 74 | 75 | ### Example: 76 | ```javascript 77 | const rp = lb.deploy(‘rp’, options); 78 | ``` 79 | 80 | lb.deploy has six specific strings that can be used in this library. 81 | 82 | *****To see the other use cases and strings for lb.deploy in this library, click these links:***** 83 | 84 | * [Websocket Deploy Section](https://github.com/DataHiveDJW/nodexchange/blob/master/README.md#websockets-setup) 85 | 86 | * [Error Log Deploy Section](https://github.com/DataHiveDJW/nodexchange/blob/master/README.md#error-log-setup) 87 | 88 | * [Redis Deploy Section](https://github.com/DataHiveDJW/nodexchange/blob/master/README.md#redis-sessions-setup) 89 | 90 | * [Multi-Threading Deploy Section](https://github.com/DataHiveDJW/nodexchange/blob/master/README.md#threads-setup) 91 | 92 | ## rp.addOptions ( options ) — 93 | 94 | **Options (array of objects)** 95 | 96 | You can use rp.addOptions to append your existing options collection. This method will not overwrite your previous collection. 97 | 98 | ### Example: 99 | 100 | ```javascript 101 | const newOptions = 102 | [ 103 | { hostname: '127.0.0.1', port: '3000' }, 104 | { hostname: '127.0.65.120', port: '4000' } 105 | ] 106 | rp.addOptions(newOptions); 107 | ``` 108 | 109 | ## rp.setRoutes ( nestedArray ) — 110 | 111 | **The nestedArray parameter:** is stored in the reverse proxy server as an object of what routes in your application you would like cached upon first request. 112 | 113 | Convention implies that you will declare this nestedArray as ‘routes’. 114 | Each subarray of routes takes two strings: ‘method’ & ‘url’: 115 | 116 | ```javascript 117 | const routes = [['method', 'URL'], ['method', 'URL']]; 118 | ``` 119 | 120 | **Method (string):** are the usual type of requests (e.g. ‘GET’, ‘POST’, ‘DELETE’, ‘PUT’); 121 | 122 | **URL (string):** will be the portion of your specific route (e.g. ‘/users’, ‘/puppies’); 123 | 124 | rp.setRoutes can be called multiple times and will add the new routes to the routes cache 125 | 126 | ### Example: 127 | 128 | ```javascript 129 | const routes = [['GET', '/puppies'], ['POST', '/login']]; 130 | ``` 131 | 132 | ## rp.init ( req , res, boolean[optional], number[optional], number[optional] ) — 133 | 134 | **This method sends/ends the response to the client** 135 | 136 | This method initializes the reverse proxy. 137 | The reverse proxy server will cache static files (.HTML, .CSS., .JS) & routes from rp.setRoutes method. 138 | 139 | This method does the following: 140 | Checks cache for existence of incoming ‘req’ 141 | Accepts ‘req’ and pipes it to target servers if it does not exist in cache 142 | Receives ‘res’ back from target servers, appends header to response, and then pipes/ends response back to browser 143 | 144 | **Third parameter (boolean):** to set up your protocol input true for https and false for http 145 | 146 | ### DDoS Considerations 147 | 148 | *****fourth parameter must be used with fifth parameter for the purpose of ip throttling***** 149 | 150 | **Fourth parameter (number) - optional:** milliseconds allowed between n (defined below) number of client requests per ip - 500 Server Error will result from violating ip throttling rules setup with fourth and fifth parameters. 151 | 152 | **Fifth parameter (number) - optional:** number of requests per ip address allowed within duration set in fourth parameter before responding with a 500 Server Error. 153 | 154 | ### Example: 155 | 156 | ```javascript 157 | const server = http.createServer((req, res) => { 158 | rp.init(req, res, false, 5000, 10); 159 | }).listen(1337); 160 | console.log('Server running at 127.0.0.1:1337'); 161 | ``` 162 | 163 | ## rp.healthCheck ( interval[optional] , ssl[optional]) — 164 | 165 | rp.healthCheck sends pings from the reverse proxy to all target servers with a test requests to review target server health. 166 | Uses internal boolean logic to toggle target servers as active or inactive based on results of pings. 167 | 168 | **First parameter (number) - optional:** accepts an interval in ms (milliseconds) 169 | 170 | **Second parameter (number) - optional:** accepts a boolean indicating whether the protocol is http or https. This parameter defaults to false, setting an http protocol. For https, set this parameter to true. 171 | 172 | If the interval parameter is NULL, rp.healthCheck can be called by user discretion. 173 | If interval has a value, rp.healthCheck will run on that given interval value (e.g. every 5 minutes). 174 | 175 | ### Example: 176 | 177 | ```javascript 178 | // interval = 10000 milliseconds 179 | rp.healthCheck(10000); 180 | 181 | // interval is null 182 | rp.healthCheck(); 183 | ``` 184 | 185 | ## rp.clearCache ( interval[optional] ) — 186 | 187 | Accepts an interval parameter in ms (milliseconds). 188 | 189 | rp.clearCache clears the internal cache of the reverse proxy server. 190 | 191 | If the interval parameter is NULL, rp.clearCache can be called by user discretion for a one-time cache clear. 192 | If interval has a value, rp.clearCache will run on that given interval value (e.g. every 5 minutes). 193 | 194 | rp.clearCache is an integral method in this library ensure cached content is fresh. This method also aids in preventing the cache from consuming too much RAM in media heavy applications and bogging down the performance of the proxy server. 195 | 196 | It is recommended to utilize this method in some capacity in your application. 197 | 198 | ### Example: 199 | 200 | ```javascript 201 | // interval = 10000 milliseconds 202 | rp.clearCache(10000); 203 | 204 | // interval is null 205 | rp.clearCache(); 206 | ``` 207 | 208 | ## rp.getCache ( ) - 209 | 210 | Returns an object of what is currently cached. 211 | 212 | ## rp.setAlgoLC ( ) - 213 | 214 | Sets the load-balancing algorithm to least-connections. This is the default 215 | algorithm in NodeXchange. 216 | 217 | ## rp.setAlgoRR ( ) — 218 | 219 | Sets the load-balancing algorithm to round-robin. 220 | 221 | # Websockets Setup 222 | 223 | The websockets feature extends http/https routing and load-balancing to websocket connections. There are two websockets options, pooling and non-pooling. The pooling option expects a pool id message from the client before connecting the persistent web socket tunnel to the appropriate target server handling that web socket pool. The non-pooling options creates persistent web socket tunnels with target servers on a least connection basis. 224 | 225 | ## lb.deploy ( string ) — 226 | 227 | **First parameter (string):** is a configuration argument for the load-balance library which in this case must be: ’wspool’ to initialize websocket proxying for the pooling option or 'ws' for the non-pooling option 228 | 229 | ### Example: 230 | ```javascript 231 | const ws = lb.deploy('wspool'); // or lb.deploy('ws'); 232 | ``` 233 | 234 | ## ws.init ( server, options, boolean[optional] ) — 235 | This method commences websocket routing. 236 | 237 | **server (previously instantiated http(s) server object)** 238 | 239 | The server parameter expects the object returned from the http/https createServer method. The websockets feature leverages the same port as http/https server. 240 | 241 | **options (array of objects)** 242 | 243 | If further target server options are added, you can use rp.addOptions to update your existing options collection. 244 | This method will not overwrite your previous collection. 245 | 246 | **boolean (ssl flag)** 247 | 248 | The third boolean parameter defaults to false. If ssl communication is required between proxy server and target servers, 'true' should be used for this argument. 249 | 250 | **IMPORTANT NOTE ON POOLING FEATURE** 251 | All web socket messages from client will be dropped until message is received with socketPoolId. Format of message must be "{'socketPoolId': 5}" where '5' is the pool id in this case (ie unique id for chatroom or lobby etc). Upon receiving this message, the web socket tunnel will be connected with the appropriate target server and messages will routed accordingly. 252 | 253 | ### Example: 254 | ```javascript 255 | const server = http.createServer((req, res) => { 256 | rp.init(req, res); 257 | }).listen(1337); 258 | 259 | ws.init(server, options, true); 260 | ``` 261 | 262 | # Error Log Setup 263 | 264 | Handling a multitude of servers for your application requires constant monitoring of the health of each individual server. To coincide with our health check functionality, we provided some simple to use methods to create an error log path that can cleanly and readibly store the results of errors from health checks. 265 | 266 | ## lb.deploy ( string ) — 267 | 268 | **First parameter (string):** is a configuration argument for the error log library which in this case must be ’errorLog’ to gain access to the init and write methods. 269 | 270 | ### Example: 271 | ```javascript 272 | const errorLog = lb.deploy('errorLog'); 273 | ``` 274 | 275 | ## errorLog.init ( string ) -- 276 | 277 | Accepts a string as its sole parameter which provides your desired file path for the log file to be generated at. 278 | This method will simply store the file path. 279 | 280 | ```javascript 281 | errorLog.init(path.join(__dirname + '/healthCheck.log')); 282 | ``` 283 | 284 | ## errorLog.write ( object ) -- 285 | 286 | Accepts the error object as it's parameter to be written to the log file. 287 | Method will read the previous copy of the file before re-writing it and concatenating the old data with the new data. 288 | 289 | ```javascript 290 | res.on('end', () => { 291 | // response from server received, reset value to true if prev false 292 | if (options[i].active === false) options[i].active = true; 293 | }); 294 | }).on('error', (e) => { 295 | e.name = "HealthCheck Error"; 296 | errorLog.write(e); 297 | ``` 298 | 299 | # Redis Sessions Setup 300 | Nodexchange comes packaged with a lightweight controller to store and read session data from a Redis server, providing a central session data store between multiple target servers. 301 | 302 | A Redis server must be setup as a prerequisite to utilizing the Redis Sessions object on the target server. 303 | [see Redis documentation for more information on setting up your personal Redis instance](https://redis.io/documentation) 304 | The deploy method requires the Redis server address in the options argument (host/ip and port) and creates/returns the ‘rs’ (Redis sessions) object. 305 | 306 | ```javascript 307 | const lb = require(‘nodexchange’); 308 | const options = { 309 | host: '127.0.0.1', // —> string hostname or IP address 310 | port: 6379, // —> integer port number 311 | }; 312 | 313 | const rs = lb.deploy(‘redis’, options); 314 | ``` 315 | 316 | ## rs.authenticate(req, res, cookieKey(string), uniqueId(string), cb(function)) 317 | 318 | Encrypts (SHA-256 via crypto module) and saves session cookie in Redis 319 | Sets cookie in header (DOES NOT END RESPONSE) 320 | 321 | **Req (object):** client request object 322 | 323 | **Res (object):** client response object 324 | 325 | **cookieKey (string):** name of cookie as seen in browser 326 | 327 | **uniqueId (string):** uniqueId per cookieKey (e.g. username) 328 | 329 | **Cb (function):** callback function executed after redis save - includes redis error and reply arguments -- 330 | 331 | Example: `(err, reply) => {. . .}` 332 | 333 | ## rs.verifySession(req, cookieKey(string), cb(function)) 334 | Parses cookies in request header 335 | Validates session cookies against central redis store 336 | Returns true or false based on cookie validity 337 | 338 | **Req (object):** client request object 339 | 340 | **cookieKey (string):** name of cookie as seen in browser (targets this cookie name exclusively when validating) 341 | 342 | **Cb (function):** callback function with result argument true or false -- 343 | 344 | Example: `(sessionVerified) => {. . .}` 345 | 346 | # Threads Setup 347 | Since node is built on a single-threaded foundation, we provide the option to use all threads on target servers using the Node cluster module. Utilizing this module, the target servers will be able to sustain a much higher load for synchronous tasks than when node is running single-threaded solely. 348 | 349 | For example, a hypothetical single-threaded target server handles 100 concurrent requests. A Node server running 4 threads in an ideal setting will be able to handle 200 concurrent requests (not accounting for OS IPC overhead). 350 | 351 | The threads will balance requests to the target server through the cluster module’s native round-robin algorithm (except on Windows which has it's own scheduling policy). 352 | 353 | See more details at [Node's Cluster Module Docs](https://nodejs.org/api/cluster.html#cluster_how_it_works) 354 | 355 | ## threads(server(object), port(number)); 356 | 357 | The threads function commences cpu thread forking - this process includes executing the 'listen' method on the input server object (**IMPORTANT NOTE: REMOVE THE LISTEN METHOD FROM SERVER OBJECT IF USING THREADS**) 358 | 359 | **server (previously instantiated http/https server object)** 360 | 361 | The server parameter expects the object returned from the http/https createServer method 362 | 363 | **port (number):** number indicating at what port will the server/threads respond to (e.g. 80) 364 | 365 | A simple set up to getting threads started: 366 | 367 | ```javascript 368 | const lb = require('nodexchange'); 369 | const threads = lb.deploy(‘threads’); 370 | 371 | const port = 3000; 372 | const server = http.createServer((bReq, bRes) => { 373 | // . . . 374 | }); 375 | threads(server, port); 376 | ``` 377 | --------------------------------------------------------------------------------