From 31e8fe2222dbe3dd0b22999e6e27fbed61d14b5e Mon Sep 17 00:00:00 2001 From: digital dreamer Date: Sun, 4 May 2014 11:52:18 +0200 Subject: [PATCH] Initial commit --- README.md | 113 +++++++++++++++++++++- package.json | 22 +++++ settings.json | 65 +++++++++++++ twister-proxy.js | 244 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 package.json create mode 100644 settings.json create mode 100644 twister-proxy.js diff --git a/README.md b/README.md index 98d4623..f6a8787 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,115 @@ twister-proxy ============= -RPC proxy for running a public server for the Twister P2P microblogging platform. +Version 0.1.1 + +This is an RPC proxy for running a public server for the Twister P2P microblogging network. Public servers allow anyone to easily read news posted on Twister from the web. If a user wants to become active on the network, he/she is directed to instructions on how to install the app. Twister proxy needs twister-core to be able to access the network. + +Twister is in alpha phase, it's still under construction. It is already being used, but it may be unstable, and difficult to compile. This is the project website http://twister.net.co/ + +Twister is open source, the source code is available here: https://github.com/miguelfreitas/twister-core + +## Running a public server + +**1 - install Twister** + +instructions can be found here: http://twister.net.co/?page_id=23 + +**2 - install node.js** + +it's available for all major platforms from here: http://nodejs.org/ + +**3 - install twister-proxy** + +clone it from the repository + +> git clone https://github.com/digital-dreamer/twister-proxy.git + +install it + +> cd twister-proxy + +> npm install + +**4 - run twisterd** + +go to your twister-core folder and run twisterd with the following options: + +> ./twisterd -daemon -rpcallowip=127.0.0.1 -public_server_mode=1 + +This will run twister server in background, allow RPC calls, but only form the same computer, and put it in "public server mode", which is designed for this purpose. + +**5 - run twister-proxy** + +go to the twister-proxy folder and run + +> node twister-proxy.js & + +this will launch a public server on default http port 80. If you need to change any settings, you can edit the settings.json file. + +If you type your server's URL into a web browser, you should see the twister web application. It is now functional, but if you care about privacy for your users, I highly recommend taking one more step and enabling SSL. + +## Enable SSL + +**1 - upgrade OpenSSL to the latest version to protect your server from Heartbleed** + +Visit http://heartbleed.com/ if you want to know more about this issue. + +**2 - generate a key and certificate request** + +> openssl genrsa -des3 -out server-key.pem 2048 + +> openssl req -new -key server-key.pem -out request.csr + +Keep the generated server-key.pem safe. + +If you want to know more about keys: https://www.openssl.org/docs/HOWTO/keys.txt + +**3 - request a certificate** + +You now give the request.csr file to a Certification Authority (CA) - a company that will generate a certificate and give it back to you. + +This guide shows where to get a certificate cheap or for free: + +http://webdesign.about.com/od/ssl/tp/cheapest-ssl-certificates.htm + +**4 - enable SSL in twister-proxy** + +Edit the settings.json file + +* In "ssl_key_file", specify a path to the server-key.pem file that you generated. +* In "ssl_certificate_file", specify a path to the file that you received from your Certificate Authority. +* Change "enable_https" from false to true. + +That's it. If you now run twister-proxy, it will use secure https connections. + +## Production + +**1 - To keep a log, redirect twister proxy output to a file** + +Example: +> node twister-proxy.js > output.log & + + +**2 - You can use the "forever" module to keep the proxy server running** + +A guide can be found here: + +https://blog.nodejitsu.com/keep-a-nodejs-server-up-with-forever/ + +## Troubleshooting + +### Cannot connect to twisterd + +Twister must be running and accepting RPC calls, run it with these parameters: + +> ./twisterd -daemon -rpcallowip=127.0.0.1 -public_server_mode=1 + +If you changed the RPC port, username or password in twister.conf, you need to change it in settings.json too. + +### Configuration file settings.json couldn't be parsed + +You probably damaged settings.json when editing it. If you can spot what went wrong, you can correct it, if not, download the default settings.json and redo your customization. + + +If you get stuck, and need some help setting up a public Twister server, you can ask in the issue sction, even if it is not an actual issue with the code. diff --git a/package.json b/package.json new file mode 100644 index 0000000..0207b7e --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "twister-proxy", + "description": "RPC proxy for running a public server for the Twister P2P microblogging platform.", + "version": "0.1.1", + "author": "digital dreamer ", + "dependencies": + { + "express": ">=2.5.x", + "log-timestamp": ">=0.1.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/digital-dreamer/twister-proxy.git" + }, + "licenses": + [ + { + "type": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + ] +} diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..03efec1 --- /dev/null +++ b/settings.json @@ -0,0 +1,65 @@ +{ + "Server": + { + "ssl_key_file": "insert/path/to/your/server-key-file", + "ssl_certificate_file": "insert/path/to/your/ssl-cetrificate", + "enable_https": false, + + "https_port": 443, + "http_port": 80 + }, + + "RPC": + { + "host": "localhost", + "port": 28332, + "user": "user", + "password": "pwd" + }, + + "CallLimits": + [ + { + "name": "getbestblockhash", + "maxPerMinute": null, + "maxPerMinutePerIP": null + }, + { + "name": "getinfo", + "maxPerMinute": null, + "maxPerMinutePerIP": null + }, + { + "name": "listwalletusers", + "maxPerMinute": null, + "maxPerMinutePerIP": null + }, + { + "name": "getblock", + "maxPerMinute": null, + "maxPerMinutePerIP": null + }, + { + "name": "dhtget", + "maxPerMinute": null, + "maxPerMinutePerIP": null + }, + { + "name": "listusernamespartial", + "maxPerMinute": null, + "maxPerMinutePerIP": null + }, + { + "name": "gettrendinghashtags", + "maxPerMinute": null, + "maxPerMinutePerIP": null + } + ], + + "LogAsAttackThreshold": + { + "callsOverLimits": 30, + "invalidRequests": 30, + "forbiddenCalls": 30 + } +} diff --git a/twister-proxy.js b/twister-proxy.js new file mode 100644 index 0000000..3f74071 --- /dev/null +++ b/twister-proxy.js @@ -0,0 +1,244 @@ +var fs = require("fs"); +var https = require("https"); +var http = require("http"); +var express = require("express"); +var console = require("console"); + +var app = express(); + +try +{ + var settings = JSON.parse(fs.readFileSync("settings.json")); +} +catch(e) +{ + console.log("Error: Configuration file settings.json couldn't be parsed.\nSee Troubleshooting section in README.md for instructions."); + process.exit(1); +} + +if(settings.Server.enable_https) +{ + try + { + var privateKey = fs.readFileSync(settings.Server.ssl_key_file).toString(); + } + catch(e) + { + console.log("Error: unable to load SSL key. Please edit setting.json and put the correct path to your SSL private key file into \"ssl_key_file.\""); + process.exit(1); + } + try + { + var certificate = fs.readFileSync(settings.Server.ssl_certificate_file).toString(); + } + catch(e) + { + console.log("Error: unable to load SSL certificate. Please edit setting.json and put the correct path to your SSL certificate file into \"ssl_certificate_file.\""); + process.exit(1); + } + var credentials = {key: privateKey, cert: certificate}; +} + +maxCallsPerMinute = {}; +maxCallsPerMinutePerIP = {}; +callsRemaining = {}; +perIPCounter = {}; +droppedCallsCounter = {}; + +var invalidRequestCounter = 0; +var forbiddenCallCounter = 0; +var connectionErrorMessageDisplayed = false; + +settings.CallLimits.forEach(function(x) { + maxCallsPerMinute[x.name] = x.maxPerMinute; + maxCallsPerMinutePerIP[x.name] = x.maxPerMinutePerIP; + callsRemaining[x.name] = x.maxPerMinute; + droppedCallsCounter[x.name] = 0; +}); + +CounterInstance = function() +{ + this.overLimit=0; + this.invalidRequests=0; + this.forbiddenCalls=0; + this.callsRemaining={}; + for (x in maxCallsPerMinutePerIP) + { + if(maxCallsPerMinutePerIP[x]!==null) + this.callsRemaining[x]=maxCallsPerMinutePerIP[x]; + } +} + +require("log-timestamp"); + +app.get("*", function(request, response) +{ + if(settings.Server.enable_https&&request.protocol=="http") + { + if(settings.Server.https_port==443) + secureUrl="https://"+request.host+request.path; + else + secureUrl="https://"+request.host+":"+settings.Server.https_port+request.path; + response.writeHead(302, {"Location": secureUrl}); + response.end(); + return; + } + + var webProxy = http.request({host: settings.RPC.host, port: settings.RPC.port, method: request.method, path: request.path, headers: request.headers}, function (proxy_res) + { + proxy_res.pipe(response, {end: true}); + }); + + webProxy.on("error", function(error) + { + if(!connectionErrorMessageDisplayed) + { + console.log("Error: cannot connect to twisterd.\nSee Troubleshooting section in README.md for instructions."); + connectionErrorMessageDisplayed=true; + } + response.send(502); + }); + + request.pipe(webProxy, {end: true}); +}); + +app.post("/", function(request, response) +{ + request.rawBody = ""; + request.setEncoding("utf8"); + + request.addListener("data", function(chunk) + { + request.rawBody += chunk; + }); + + request.addListener("end", function() + { + var remoteIP = request.connection.remoteAddress; + + if(perIPCounter[remoteIP]===undefined) + { + perIPCounter[remoteIP]=new CounterInstance(); + } + try + { + bodyJson=JSON.parse(request.rawBody); + rpcMethod=bodyJson.method; + } + catch(e) + { + perIPCounter[remoteIP].invalidRequests++; + invalidRequestCounter++; + return; + } + if(maxCallsPerMinute[rpcMethod]===undefined) + { + perIPCounter[remoteIP].forbiddenCalls++; + forbiddenCallCounter++; + return; + } + if(maxCallsPerMinute[rpcMethod]!==null) + { + if(callsRemaining[rpcMethod]<1) + { + droppedCallsCounter[rpcMethod]++; + return; + } + else + { + callsRemaining[rpcMethod]--; + } + } + + if(maxCallsPerMinutePerIP[rpcMethod]!==null) + { + if(perIPCounter[remoteIP].callsRemaining[rpcMethod]<1) + { + perIPCounter[remoteIP].overLimit++; + return; + } + else + { + perIPCounter[remoteIP].callsRemaining[rpcMethod]--; + } + } + + var rpcProxy = http.request({host: settings.RPC.host, port: settings.RPC.port, method: request.method, headers: request.headers}, function(proxy_res) + { + proxy_res.on("data", function(chunk) + { + response.write(chunk, "binary"); + }); + + proxy_res.on("end", function(chunk) + { + response.end(); + }); + + proxy_res.on("error", function(error) + { + if(!connectionErrorMessageDisplayed) + { + console.log("Error: cannot connect to twisterd.\nSee Troubleshooting section in README.md for instructions."); + connectionErrorMessageDisplayed=true; + } + response.send(502); + }); + + response.writeHead(proxy_res.statusCode, proxy_res.headers); + }); + + rpcProxy.write(request.rawBody, "binary"); + rpcProxy.end(); + }); +}); + +if(settings.Server.enable_https) +{ + https.createServer(credentials, app).listen(settings.Server.https_port); +} + +http.createServer(app).listen(settings.Server.http_port); + +setInterval(function() +{ + for(method in maxCallsPerMinute) + { + callsRemaining[method] = maxCallsPerMinute[method]; + if(droppedCallsCounter[method]!==0) + { + console.log("Dropped "+droppedCallsCounter[method]+" calls to "+method+" over the limit of "+maxCallsPerMinute[method]); + droppedCallsCounter[method] = 0; + } + }; + if(invalidRequestCounter!==0) + { + console.log("Received "+invalidRequestCounter+" invalid POST requests."); + invalidRequestCounter = 0; + } + if(forbiddenCallCounter!==0) + { + console.log("Denied "+forbiddenCallCounter+" attempts to access forbidden API calls."); + forbiddenCallCounter = 0; + } + + for(ip in perIPCounter) + { + if(perIPCounter[ip].overLimit>=settings.LogAsAttackThreshold.callsOverLimits) + { + console.log("IP "+ip+" tried to send "+perIPCounter[ip].overLimit+" calls more than the limits allow."); + }; + if(perIPCounter[ip].invalidRequests>=settings.LogAsAttackThreshold.invalidRequests) + { + console.log("IP "+ip+" sent "+perIPCounter[ip].invalidRequests+" invalid request that couldn't be parsed."); + }; + if(perIPCounter[ip].forbiddenCalls>=settings.LogAsAttackThreshold.forbiddenCalls) + { + console.log("IP "+ip+" tried to send "+perIPCounter[ip].forbiddenCalls+" calls to forbideen functions."); + }; + } + + perIPCounter = {}; + connectionErrorMessageDisplayed = false; + +}, 60000);