From edf27483500abba37fd922e23c5177978b0e8f12 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 09:45:31 +0200 Subject: [PATCH 01/10] Support reading rpc credentials out of bitcoind's cookie file --- app/config.js | 10 +++++++++- app/defaultCredentials.js | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/config.js b/app/config.js index de6e94c..9f8f62f 100644 --- a/app/config.js +++ b/app/config.js @@ -1,3 +1,4 @@ +var fs = require('fs'); var coins = require("./coins.js"); var currentCoin = process.env.BTCEXP_COIN || "BTC"; @@ -7,6 +8,13 @@ try { Object.assign(credentials, require("./credentials.js")) } catch (err) {} +var rpcCred = credentials.rpc; + +if (rpcCred.cookie && !rpcCred.username && !rpcCred.password && fs.existsSync(rpcCred.cookie)) { + [ rpcCred.username, rpcCred.password ] = fs.readFileSync(rpcCred.cookie).toString().split(':', 2); + if (!rpcCred.password) throw new Error('Cookie file '+rpcCred.cookie+' in unexpected format'); +} + module.exports = { cookiePassword: process.env.BTCEXP_COOKIE_PASSWORD || "0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", demoSite: !!process.env.BTCEXP_DEMO, @@ -107,7 +115,7 @@ module.exports = { {name:"RPC Browser", url:"/rpc-browser", desc:"Browse the RPC functionality of this node. See docs and execute commands.", fontawesome:"fas fa-book"}, {name:"RPC Terminal", url:"/rpc-terminal", desc:"Directly execute RPCs against this node.", fontawesome:"fas fa-terminal"}, - + {name:(coins[currentCoin].name + " Fun"), url:"/fun", desc:"See fun/interesting historical blockchain data.", fontawesome:"fas fa-certificate"} ], diff --git a/app/defaultCredentials.js b/app/defaultCredentials.js index 633fadf..4943ab4 100644 --- a/app/defaultCredentials.js +++ b/app/defaultCredentials.js @@ -1,9 +1,13 @@ +var os = require('os'); +var path = require('path'); + module.exports = { rpc: { host: process.env.BTCEXP_BITCOIND_HOST || "127.0.0.1", port: process.env.BTCEXP_BITCOIND_PORT || 8332, username: process.env.BTCEXP_BITCOIND_USER, password: process.env.BTCEXP_BITCOIND_PASS, + cookie: process.env.BTCEXP_BITCOIND_COOKIE || path.join(os.homedir(), '.bitcoin', '.cookie'), }, influxdb:{ From 8b175c8110a8c31126793126db46e739c948ad67 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 11:02:46 +0200 Subject: [PATCH 02/10] Add CLI utility --- app/defaultCredentials.js | 4 +-- bin/cli.js | 55 +++++++++++++++++++++++++++++++++++++++ bin/www | 2 +- package.json | 5 +++- 4 files changed, 62 insertions(+), 4 deletions(-) create mode 100755 bin/cli.js diff --git a/app/defaultCredentials.js b/app/defaultCredentials.js index 4943ab4..9218742 100644 --- a/app/defaultCredentials.js +++ b/app/defaultCredentials.js @@ -11,10 +11,10 @@ module.exports = { }, influxdb:{ - active: !!process.env.BTCEXP_INFLUXDB_ENABLED, + active: !!process.env.BTCEXP_ENABLE_INFLUXDB, host: process.env.BTCEXP_INFLUXDB_HOST || "127.0.0.1", port: process.env.BTCEXP_INFLUXDB_PORT || 8086, - database: process.env.BTCEXP_INFLUXDB_DB || "influxdb", + database: process.env.BTCEXP_INFLUXDB_DBNAME || "influxdb", username: process.env.BTCEXP_INFLUXDB_USER || "admin", password: process.env.BTCEXP_INFLUXDB_PASS || "admin" }, diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..22edb0e --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +const args = require('meow')(` + Usage + $ btc-rpc-explorer [options] + + Options + -p, --port port to bind http server [default: 3002] + --coin crypto-coin to enable [default: BTC] + + -H, --bitcoind-host hostname for bitcoind rpc [default: 127.0.0.1] + -P, --bitcoind-port port for bitcoind rpc [default: 8332] + -c, --bitcoind-cookie path to bitcoind cookie file [default: 8332] + -u, --bitcoind-user username for bitcoind rpc [default: none] + -w, --bitcoind-pass password for bitcoind rpc [default: none] + + --cookie-password secret key for signed cookie hmac generation + --demo enable demoSite mode [default: disabled] + + --ipstack-key api access key for ipstack (for geoip) [default: disabled] + --ganalytics-tracking tracking id for google analytics [default: disabled] + --sentry-url sentry url [default: disabled] + + --enable-influxdb enable influxdb for logging network stats [default: false] + --influxdb-host hostname for influxdb [default: 127.0.0.1] + --influxdb-user username for influxdb [default: admin] + --influxdb-pass password for influxdb [default: admin] + --influxdb-dbname database name for influxdb [default: influxdb] + + -e, --node-env nodejs environment mode [default: production] + -h, --help output usage information + -v, --version output version number + + Example + $ btc-rpc-explorer --port 8080 --bitcoind-port 18443 --bitcoind-cookie ~/.bitcoin/regtest/.cookie + $ btc-rpc-explorer -p 8080 -P 18443 -c ~/.bitcoin/regtest.cookie + + All options may also be specified as environment variables: + $ BTCEXP_PORT=8080 BTCEXP_BITCOIND_PORT=18443 BTCEXP_BITCOIND_COOKIE=~/.bitcoin/regtest/.cookie btc-rpc-explorer + +`, { flags: { port: {alias:'p'}, enableInfluxdb: {type:'boolean'}, nodeEnv: {alias:'e', default:'production'} + , bitcoindHost: {alias:'H'}, bitcoindPort: {alias:'P'}, bitcoindCookie: {alias:'c'} + , bitcoindUser: {alias:'u'}, bitcoindPass: {alias:'w'} + , demo: {type:'boolean'} + } } +).flags; + +const envify = k => k.replace(/([A-Z])/g, '_$1').toUpperCase(); + +Object.keys(args).filter(k => k.length > 1).forEach(k => { + if (args[k] === false) process.env[`BTCEXP_NO_${envify(k)}`] = true; + else process.env[`BTCEXP_${envify(k)}`] = args[k]; +}) + +require('./www'); diff --git a/bin/www b/bin/www index 9f93c1b..c46aed3 100644 --- a/bin/www +++ b/bin/www @@ -2,7 +2,7 @@ var debug = require('debug')('my-application'); var app = require('../app'); -app.set('port', process.env.PORT || 3002); +app.set('port', process.env.PORT || process.env.BTCEXP_PORT || 3002); var server = app.listen(app.get('port'), function() { debug('Express server listening on port ' + server.address().port); diff --git a/package.json b/package.json index 677af28..89b1ea3 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,12 @@ "version": "1.0.0", "description": "Explorer for Bitcoin and RPC-compatible blockchains", "private": false, + "bin": "bin/cli.js", "scripts": { "start": "node ./bin/www", "build": "npm-run-all build:*", - "build:less": "lessc ./public/css/radial-progress.less ./public/css/radial-progress.css" + "build:less": "lessc ./public/css/radial-progress.less ./public/css/radial-progress.css", + "prepare": "npm run build" }, "keywords": [ "bitcoin", @@ -34,6 +36,7 @@ "influx": "5.0.7", "jstransformer-markdown-it": "^2.0.0", "lru-cache": "4.1.3", + "meow": "^5.0.0", "moment": "^2.24.0", "moment-duration-format": "2.2.2", "morgan": "^1.9.1", From 37c962e30e51bde9ae0795aaeef5c34d7f96707b Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 11:44:56 +0200 Subject: [PATCH 03/10] Derive cookie secret using bitcoind's rpc credentials This ensures a unique, hard-to-guess cookie secret for every instance. Also, renamed from "cookiePassword" to "cookieSecret" to better express its meaning. --- README.md | 2 +- app.js | 2 +- app/config.js | 8 +++++++- bin/cli.js | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ef501e9..a6cbffc 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ BTCEXP_BITCOIND_PORT = 8332 BTCEXP_BITCOIND_USER = username BTCEXP_BITCOIND_PASS = password BTCEXP_IPSTACK_KEY = 0000aaaafffffgggggg -BTCEXP_COOKIEPASSWORD = 0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f +BTCEXP_COOKIE_SECRET = 0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f ``` ## Run via Docker diff --git a/app.js b/app.js index fa1a3f5..90f638d 100755 --- a/app.js +++ b/app.js @@ -53,7 +53,7 @@ app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(session({ - secret: config.cookiePassword, + secret: config.cookieSecret, resave: false, saveUninitialized: false })); diff --git a/app/config.js b/app/config.js index 9f8f62f..229755b 100644 --- a/app/config.js +++ b/app/config.js @@ -1,4 +1,5 @@ var fs = require('fs'); +var crypto = require('crypto'); var coins = require("./coins.js"); var currentCoin = process.env.BTCEXP_COIN || "BTC"; @@ -15,8 +16,13 @@ if (rpcCred.cookie && !rpcCred.username && !rpcCred.password && fs.existsSync(rp if (!rpcCred.password) throw new Error('Cookie file '+rpcCred.cookie+' in unexpected format'); } +var cookieSecret = process.env.BTCEXP_COOKIE_SECRET + || (rpcCred.password && crypto.createHmac('sha256', JSON.stringify(rpcCred)) + .update('btc-rpc-explorer-cookie-secret').digest('hex')) + || "0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; + module.exports = { - cookiePassword: process.env.BTCEXP_COOKIE_PASSWORD || "0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + cookieSecret: cookieSecret, demoSite: !!process.env.BTCEXP_DEMO, coin: currentCoin, diff --git a/bin/cli.js b/bin/cli.js index 22edb0e..64141f5 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -14,7 +14,7 @@ const args = require('meow')(` -u, --bitcoind-user username for bitcoind rpc [default: none] -w, --bitcoind-pass password for bitcoind rpc [default: none] - --cookie-password secret key for signed cookie hmac generation + --cookie-secret secret key for signed cookie hmac generation [default: hmac derive from bitcoind pass] --demo enable demoSite mode [default: disabled] --ipstack-key api access key for ipstack (for geoip) [default: disabled] From ab98ff41ed6c291b7a8f624d0f5464d013084043 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 12:14:53 +0200 Subject: [PATCH 04/10] Add optional password protection --- README.md | 3 +++ app.js | 7 +++++++ app/auth.js | 11 +++++++++++ bin/cli.js | 11 ++++++----- package.json | 1 + 5 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 app/auth.js diff --git a/README.md b/README.md index a6cbffc..d9a992f 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ BTCEXP_IPSTACK_KEY = 0000aaaafffffgggggg BTCEXP_COOKIE_SECRET = 0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f ``` +You may enable password protection by setting `BTCEXP_LOGIN=`. +Authenticating is done with http basic auth, using the selected password and an empty (or any) username. + ## Run via Docker 1. `docker build -t btc-rpc-explorer .` diff --git a/app.js b/app.js index 90f638d..1e401cd 100755 --- a/app.js +++ b/app.js @@ -27,6 +27,7 @@ var fs = require('fs'); var electrumApi = require("./app/api/electrumApi.js"); var Influx = require("influx"); var coreApi = require("./app/api/coreApi.js"); +var auth = require('./app/auth.js'); var crawlerBotUserAgentStrings = [ "Googlebot", "Bingbot", "Slurp", "DuckDuckBot", "Baiduspider", "YandexBot", "Sogou", "Exabot", "facebot", "ia_archiver" ]; @@ -46,6 +47,12 @@ app.engine('pug', (path, options, fn) => { app.set('view engine', 'pug'); +// basic http authentication +if (process.env.BTCEXP_LOGIN) { + app.disable('x-powered-by'); + app.use(auth(process.env.BTCEXP_LOGIN)); +} + // uncomment after placing your favicon in /public //app.use(favicon(__dirname + '/public/favicon.ico')); app.use(logger('dev')); diff --git a/app/auth.js b/app/auth.js new file mode 100644 index 0000000..d643716 --- /dev/null +++ b/app/auth.js @@ -0,0 +1,11 @@ +var basicAuth = require('basic-auth'); + +module.exports = pass => (req, res, next) => { + var cred = basicAuth(req); + + if (cred && cred.pass === pass) + return next(); + + res.set('WWW-Authenticate', `Basic realm="Private Area"`) + .sendStatus(401); +} diff --git a/bin/cli.js b/bin/cli.js index 64141f5..b169179 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -6,6 +6,7 @@ const args = require('meow')(` Options -p, --port port to bind http server [default: 3002] + -l, --login protect web interface with a password [default: no password] --coin crypto-coin to enable [default: BTC] -H, --bitcoind-host hostname for bitcoind rpc [default: 127.0.0.1] @@ -27,9 +28,9 @@ const args = require('meow')(` --influxdb-pass password for influxdb [default: admin] --influxdb-dbname database name for influxdb [default: influxdb] - -e, --node-env nodejs environment mode [default: production] - -h, --help output usage information - -v, --version output version number + -e, --node-env nodejs environment mode [default: production] + -h, --help output usage information + -v, --version output version number Example $ btc-rpc-explorer --port 8080 --bitcoind-port 18443 --bitcoind-cookie ~/.bitcoin/regtest/.cookie @@ -38,10 +39,10 @@ const args = require('meow')(` All options may also be specified as environment variables: $ BTCEXP_PORT=8080 BTCEXP_BITCOIND_PORT=18443 BTCEXP_BITCOIND_COOKIE=~/.bitcoin/regtest/.cookie btc-rpc-explorer -`, { flags: { port: {alias:'p'}, enableInfluxdb: {type:'boolean'}, nodeEnv: {alias:'e', default:'production'} +`, { flags: { port: {alias:'p'}, login: {alias:'l'} , bitcoindHost: {alias:'H'}, bitcoindPort: {alias:'P'}, bitcoindCookie: {alias:'c'} , bitcoindUser: {alias:'u'}, bitcoindPass: {alias:'w'} - , demo: {type:'boolean'} + , demo: {type:'boolean'}, enableInfluxdb: {type:'boolean'}, nodeEnv: {alias:'e', default:'production'} } } ).flags; diff --git a/package.json b/package.json index 89b1ea3..e1a307c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "url": "git+https://github.com/janoside/btc-rpc-explorer.git" }, "dependencies": { + "basic-auth": "^2.0.1", "bitcoin-core": "2.0.0", "bitcoinjs-lib": "3.3.2", "body-parser": "~1.18.2", From 44d676650fd673f68b52d43ecee00b1896984621 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 13:30:40 +0200 Subject: [PATCH 05/10] Implement CSRF protection Enabled for all POST endpoints, as well as for GET /rpc-browser when the "execute" query string argument is specified. --- app.js | 6 ++++++ package.json | 1 + routes/baseActionsRouter.js | 39 ++++++++++++++++++++++--------------- views/browser.pug | 1 + views/connect.pug | 2 ++ views/layout.pug | 2 ++ views/search.pug | 1 + views/terminal.pug | 3 +++ 8 files changed, 39 insertions(+), 16 deletions(-) diff --git a/app.js b/app.js index 1e401cd..56192aa 100755 --- a/app.js +++ b/app.js @@ -11,6 +11,7 @@ var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var session = require("express-session"); +var csurf = require("csurf"); var config = require("./app/config.js"); var simpleGit = require('simple-git'); var utils = require("./app/utils.js"); @@ -454,6 +455,11 @@ app.use(function(req, res, next) { next(); }); +app.use(csurf(), (req, res, next) => { + res.locals.csrfToken = req.csrfToken(); + next(); +}); + app.use('/', baseActionsRouter); /// catch 404 and forwarding to error handler diff --git a/package.json b/package.json index e1a307c..eac251e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "body-parser": "~1.18.2", "cookie-parser": "~1.4.3", "crypto-js": "3.1.9-1", + "csurf": "^1.9.0", "debug": "~2.6.0", "decimal.js": "7.2.3", "dotenv": "^6.2.0", diff --git a/routes/baseActionsRouter.js b/routes/baseActionsRouter.js index 9efe2e8..4e741a0 100644 --- a/routes/baseActionsRouter.js +++ b/routes/baseActionsRouter.js @@ -1,4 +1,5 @@ var express = require('express'); +var csurf = require('csurf'); var router = express.Router(); var util = require('util'); var moment = require('moment'); @@ -14,6 +15,8 @@ var coins = require("./../app/coins.js"); var config = require("./../app/config.js"); var coreApi = require("./../app/api/coreApi.js"); +const forceCsrf = csurf({ ignoreMethods: [] }); + router.get("/", function(req, res) { if (req.session.host == null || req.session.host.trim() == "") { if (req.cookies['rpc-host']) { @@ -815,7 +818,7 @@ router.post("/rpc-terminal", function(req, res) { }); }); -router.get("/rpc-browser", function(req, res) { +router.get("/rpc-browser", function(req, res, next) { if (!config.demoSite) { var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; var match = config.ipWhitelistForRpcCommands.exec(ip); @@ -883,26 +886,30 @@ router.get("/rpc-browser", function(req, res) { return; } - console.log("Executing RPC '" + req.query.method + "' with params: [" + argValues + "]"); + forceCsrf(req, res, err => { + if (err) return next(err); + + console.log("Executing RPC '" + req.query.method + "' with params: [" + argValues + "]"); + + client.command([{method:req.query.method, parameters:argValues}], function(err3, result3, resHeaders3) { + console.log("RPC Response: err=" + err3 + ", result=" + result3 + ", headers=" + resHeaders3); + + if (err3) { + if (result3) { + res.locals.methodResult = {error:("" + err3), result:result3}; - client.command([{method:req.query.method, parameters:argValues}], function(err3, result3, resHeaders3) { - console.log("RPC Response: err=" + err3 + ", result=" + result3 + ", headers=" + resHeaders3); + } else { + res.locals.methodResult = {error:("" + err3)}; + } + } else if (result3) { + res.locals.methodResult = result3; - if (err3) { - if (result3) { - res.locals.methodResult = {error:("" + err3), result:result3}; - } else { - res.locals.methodResult = {error:("" + err3)}; + res.locals.methodResult = {"Error":"No response from node."}; } - } else if (result3) { - res.locals.methodResult = result3; - - } else { - res.locals.methodResult = {"Error":"No response from node."}; - } - res.render("browser"); + res.render("browser"); + }); }); } else { res.render("browser"); diff --git a/views/browser.pug b/views/browser.pug index ec14bd5..7e72ef7 100644 --- a/views/browser.pug +++ b/views/browser.pug @@ -46,6 +46,7 @@ block content hr form(method="get") + input(type="hidden", name="_csrf", value=csrfToken) input(type="hidden", name="method", value=method) div(class="h5 mb-3") Arguments diff --git a/views/connect.pug b/views/connect.pug index b6a37c7..1cd8aa5 100644 --- a/views/connect.pug +++ b/views/connect.pug @@ -5,6 +5,8 @@ block content hr form(method="post", action="/connect") + input(type="hidden", name="_csrf", value=csrfToken) + div(class="form-group") label(for="input-host") Host / IP input(id="input-host", type="text", name="host", class="form-control", placeholder="Host / IP", value=host) diff --git a/views/layout.pug b/views/layout.pug index 601688f..39e54b2 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -2,6 +2,7 @@ doctype html html(lang="en") head meta(charset="utf-8") + meta(name="csrf-token", content=csrfToken) meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, shrink-to-fit=no") if (session.uiTheme && session.uiTheme == "dark") @@ -86,6 +87,7 @@ html(lang="en") span Dark form(method="post", action="/search", class="form-inline") + input(type="hidden", name="_csrf", value=csrfToken) div(class="input-group input-group-sm") input(type="text", class="form-control form-control-sm", name="query", placeholder="block height/hash, txid, address", value=(query), style="width: 300px;") div(class="input-group-append") diff --git a/views/search.pug b/views/search.pug index febec71..d74e024 100644 --- a/views/search.pug +++ b/views/search.pug @@ -9,6 +9,7 @@ block content div(class="mb-5") form(method="post", action="/search", class="form") + input(type="hidden", name="_csrf", value=csrfToken) div(class="input-group input-group-lg") input(type="text", class="form-control form-control-sm", name="query", placeholder="block height/hash, txid, address", value=(query), style="width: 300px;") div(class="input-group-append") diff --git a/views/terminal.pug b/views/terminal.pug index fe5c669..1299933 100644 --- a/views/terminal.pug +++ b/views/terminal.pug @@ -33,6 +33,8 @@ block content block endOfBody script. + var csrfToken = $('meta[name=csrf-token]').attr('content'); + $(document).ready(function() { $("#terminal-form").submit(function(e) { e.preventDefault(); @@ -41,6 +43,7 @@ block endOfBody var postData = {}; postData.cmd = cmd; + postData._csrf = csrfToken; $.post( "/rpc-terminal", From 99e2ac2051a5f232ec52cbe0992c2f759d9c4db0 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 14:37:49 +0200 Subject: [PATCH 06/10] Accept bitcoind rpc and influxdb options as a connection URI --- app/defaultCredentials.js | 30 +++++++++++++++++++----------- bin/cli.js | 17 +++++++++++++---- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/app/defaultCredentials.js b/app/defaultCredentials.js index 9218742..664dacd 100644 --- a/app/defaultCredentials.js +++ b/app/defaultCredentials.js @@ -1,22 +1,30 @@ var os = require('os'); var path = require('path'); +var url = require('url'); + +var btcUri = process.env.BTCEXP_BITCOIND_URI ? url.parse(process.env.BTCEXP_BITCOIND_URI, true) : { query: { } }; +var btcAuth = btcUri.auth ? btcUri.auth.split(':') : []; + +var ifxUri = process.env.BTCEXP_INFLUXDB_URI ? url.parse(process.env.BTCEXP_INFLUXDB_URI, true) : { query: { } }; +var ifxAuth = ifxUri.auth ? ifxUri.auth.split(':') : []; +var ifxActive = !!process.env.BTCEXP_ENABLE_INFLUXDB || Object.keys(process.env).some(k => k.startsWith('BTCEXP_INFLUXDB_')); module.exports = { rpc: { - host: process.env.BTCEXP_BITCOIND_HOST || "127.0.0.1", - port: process.env.BTCEXP_BITCOIND_PORT || 8332, - username: process.env.BTCEXP_BITCOIND_USER, - password: process.env.BTCEXP_BITCOIND_PASS, - cookie: process.env.BTCEXP_BITCOIND_COOKIE || path.join(os.homedir(), '.bitcoin', '.cookie'), + host: btcUri.hostname || process.env.BTCEXP_BITCOIND_HOST || "127.0.0.1", + port: btcUri.port || process.env.BTCEXP_BITCOIND_PORT || 8332, + username: btcAuth[0] || process.env.BTCEXP_BITCOIND_USER, + password: btcAuth[1] || process.env.BTCEXP_BITCOIND_PASS, + cookie: btcUri.query.cookie || process.env.BTCEXP_BITCOIND_COOKIE || path.join(os.homedir(), '.bitcoin', '.cookie'), }, influxdb:{ - active: !!process.env.BTCEXP_ENABLE_INFLUXDB, - host: process.env.BTCEXP_INFLUXDB_HOST || "127.0.0.1", - port: process.env.BTCEXP_INFLUXDB_PORT || 8086, - database: process.env.BTCEXP_INFLUXDB_DBNAME || "influxdb", - username: process.env.BTCEXP_INFLUXDB_USER || "admin", - password: process.env.BTCEXP_INFLUXDB_PASS || "admin" + active: ifxActive, + host: ifxUri.hostname || process.env.BTCEXP_INFLUXDB_HOST || "127.0.0.1", + port: ifxUri.port || process.env.BTCEXP_INFLUXDB_PORT || 8086, + database: ifxUri.pathname && ifxUri.pathname.substr(1) || process.env.BTCEXP_INFLUXDB_DBNAME || "influxdb", + username: ifxAuth[0] || process.env.BTCEXP_INFLUXDB_USER || "admin", + password: ifxAuth[1] || process.env.BTCEXP_INFLUXDB_PASS || "admin" }, // optional: enter your api access key from ipstack.com below diff --git a/bin/cli.js b/bin/cli.js index b169179..83a5654 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -9,6 +9,7 @@ const args = require('meow')(` -l, --login protect web interface with a password [default: no password] --coin crypto-coin to enable [default: BTC] + -b, --bitcoind-uri connection URI for bitcoind rpc (overrides the options below) -H, --bitcoind-host hostname for bitcoind rpc [default: 127.0.0.1] -P, --bitcoind-port port for bitcoind rpc [default: 8332] -c, --bitcoind-cookie path to bitcoind cookie file [default: 8332] @@ -23,7 +24,9 @@ const args = require('meow')(` --sentry-url sentry url [default: disabled] --enable-influxdb enable influxdb for logging network stats [default: false] + --influxdb-uri connection URI for influxdb (overrides the options below) --influxdb-host hostname for influxdb [default: 127.0.0.1] + --influxdb-port port for influxdb [default: 8086] --influxdb-user username for influxdb [default: admin] --influxdb-pass password for influxdb [default: admin] --influxdb-dbname database name for influxdb [default: influxdb] @@ -32,16 +35,22 @@ const args = require('meow')(` -h, --help output usage information -v, --version output version number - Example + Examples $ btc-rpc-explorer --port 8080 --bitcoind-port 18443 --bitcoind-cookie ~/.bitcoin/regtest/.cookie $ btc-rpc-explorer -p 8080 -P 18443 -c ~/.bitcoin/regtest.cookie - All options may also be specified as environment variables: + Or using connection URIs + $ btc-rpc-explorer -b bitcoin://bob:myPassword@127.0.0.1:18443/ + $ btc-rpc-explorer -b bitcoin://127.0.0.1:18443/?cookie=$HOME/.bitcoin/regtest/.cookie + $ btc-rpc-explorer --influxdb-uri influx://bob:myPassword@127.0.0.1:8086/dbName + + All options may also be specified as environment variables $ BTCEXP_PORT=8080 BTCEXP_BITCOIND_PORT=18443 BTCEXP_BITCOIND_COOKIE=~/.bitcoin/regtest/.cookie btc-rpc-explorer + `, { flags: { port: {alias:'p'}, login: {alias:'l'} - , bitcoindHost: {alias:'H'}, bitcoindPort: {alias:'P'}, bitcoindCookie: {alias:'c'} - , bitcoindUser: {alias:'u'}, bitcoindPass: {alias:'w'} + , bitcoindUri: {alias:'b'}, bitcoindHost: {alias:'H'}, bitcoindPort: {alias:'P'} + , bitcoindCookie: {alias:'c'}, bitcoindUser: {alias:'u'}, bitcoindPass: {alias:'w'} , demo: {type:'boolean'}, enableInfluxdb: {type:'boolean'}, nodeEnv: {alias:'e', default:'production'} } } ).flags; From bfcbcd07a463c8eb782540e81d34226ffd8bca3a Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 14:41:55 +0200 Subject: [PATCH 07/10] Add --no-rates to disable fetching of currency exchange rates --- app/utils.js | 2 ++ bin/cli.js | 1 + 2 files changed, 3 insertions(+) diff --git a/app/utils.js b/app/utils.js index 9b8130f..68924fb 100644 --- a/app/utils.js +++ b/app/utils.js @@ -276,6 +276,8 @@ function getBlockTotalFeesFromCoinbaseTxAndBlockHeight(coinbaseTx, blockHeight) } function refreshExchangeRates() { + if (process.env.BTCEXP_NO_RATES) return; + if (coins[config.coin].exchangeRateData) { request(coins[config.coin].exchangeRateData.jsonUrl, function(error, response, body) { if (!error && response && response.statusCode && response.statusCode == 200) { diff --git a/bin/cli.js b/bin/cli.js index 83a5654..4522be3 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -18,6 +18,7 @@ const args = require('meow')(` --cookie-secret secret key for signed cookie hmac generation [default: hmac derive from bitcoind pass] --demo enable demoSite mode [default: disabled] + --no-rates disable fetching of currency exchange rates [default: enabled] --ipstack-key api access key for ipstack (for geoip) [default: disabled] --ganalytics-tracking tracking id for google analytics [default: disabled] From d1afd3ad16ba0f5942b828b85b71ec181f16e0c6 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 18:59:43 +0200 Subject: [PATCH 08/10] Remove IP whitelist, require authentication password instead (when not in demo mode) --- app/auth.js | 4 +++- app/config.js | 3 --- routes/baseActionsRouter.js | 36 +++++++++--------------------------- 3 files changed, 12 insertions(+), 31 deletions(-) diff --git a/app/auth.js b/app/auth.js index d643716..4442f89 100644 --- a/app/auth.js +++ b/app/auth.js @@ -3,8 +3,10 @@ var basicAuth = require('basic-auth'); module.exports = pass => (req, res, next) => { var cred = basicAuth(req); - if (cred && cred.pass === pass) + if (cred && cred.pass === pass) { + req.authenticated = true; return next(); + } res.set('WWW-Authenticate', `Basic realm="Private Area"`) .sendStatus(401); diff --git a/app/config.js b/app/config.js index 229755b..7bd2a71 100644 --- a/app/config.js +++ b/app/config.js @@ -106,9 +106,6 @@ module.exports = { credentials: credentials, - // Edit "ipWhitelistForRpcCommands" regex to limit access to RPC Browser / Terminal to matching IPs - ipWhitelistForRpcCommands:/^(127\.0\.0\.1)?(\:\:1)?$/, - siteTools:[ {name:"Node Status", url:"/node-status", desc:"Summary of this node: version, network, uptime, etc.", fontawesome:"fas fa-broadcast-tower"}, {name:"Peers", url:"/peers", desc:"Detailed info about the peers connected to this node.", fontawesome:"fas fa-sitemap"}, diff --git a/routes/baseActionsRouter.js b/routes/baseActionsRouter.js index 4e741a0..9866e7b 100644 --- a/routes/baseActionsRouter.js +++ b/routes/baseActionsRouter.js @@ -746,30 +746,18 @@ router.get("/address/:address", function(req, res) { }); router.get("/rpc-terminal", function(req, res) { - if (!config.demoSite) { - var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - var match = config.ipWhitelistForRpcCommands.exec(ip); - - if (!match) { - res.send("RPC Terminal / Browser may not be accessed from '" + ip + "'. This restriction can be modified in your config.js file."); - - return; - } + if (!config.demoSite && !req.authenticated) { + res.send("RPC Terminal / Browser may not be accessed without logging-in. This restriction can be modified in your config.js file."); + return; } res.render("terminal"); }); router.post("/rpc-terminal", function(req, res) { - if (!config.demoSite) { - var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - var match = config.ipWhitelistForRpcCommands.exec(ip); - - if (!match) { - res.send("RPC Terminal / Browser may not be accessed from '" + ip + "'. This restriction can be modified in your config.js file."); - - return; - } + if (!config.demoSite && !req.authenticated) { + res.send("RPC Terminal / Browser may not be accessed without logging-in. This restriction can be modified in your config.js file."); + return; } var params = req.body.cmd.trim().split(/\s+/); @@ -819,15 +807,9 @@ router.post("/rpc-terminal", function(req, res) { }); router.get("/rpc-browser", function(req, res, next) { - if (!config.demoSite) { - var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - var match = config.ipWhitelistForRpcCommands.exec(ip); - - if (!match) { - res.send("RPC Terminal / Browser may not be accessed from '" + ip + "'. This restriction can be modified in your config.js file."); - - return; - } + if (!config.demoSite && !req.authenticated) { + res.send("RPC Terminal / Browser may not be accessed without logging-in. This restriction can be modified in your config.js file."); + return; } coreApi.getHelp().then(function(result) { From 769ab9f6605cef62cea8d4d1123e5e641af30268 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 20:02:53 +0200 Subject: [PATCH 09/10] New configuration options: BTCEXP_RPC_ALLOWALL and BTCEXP_RPC_BLACKLIST --- app/config.js | 5 ++++- bin/cli.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/config.js b/app/config.js index 7bd2a71..101bb08 100644 --- a/app/config.js +++ b/app/config.js @@ -26,7 +26,10 @@ module.exports = { demoSite: !!process.env.BTCEXP_DEMO, coin: currentCoin, - rpcBlacklist:[ + rpcBlacklist: + process.env.BTCEXP_RPC_ALLOWALL ? [] + : process.env.BTCEXP_RPC_BLACKLIST ? process.env.BTCEXP_RPC_BLACKLIST.split(',').filter(Boolean) + : [ "addnode", "backupwallet", "bumpfee", diff --git a/bin/cli.js b/bin/cli.js index 4522be3..62c64f7 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -16,6 +16,8 @@ const args = require('meow')(` -u, --bitcoind-user username for bitcoind rpc [default: none] -w, --bitcoind-pass password for bitcoind rpc [default: none] + --rpc-allowall allow all rpc commands [default: false] + --rpc-blacklist comma separated list of rpc commands to block [default: see in config.js] --cookie-secret secret key for signed cookie hmac generation [default: hmac derive from bitcoind pass] --demo enable demoSite mode [default: disabled] --no-rates disable fetching of currency exchange rates [default: enabled] @@ -52,7 +54,8 @@ const args = require('meow')(` `, { flags: { port: {alias:'p'}, login: {alias:'l'} , bitcoindUri: {alias:'b'}, bitcoindHost: {alias:'H'}, bitcoindPort: {alias:'P'} , bitcoindCookie: {alias:'c'}, bitcoindUser: {alias:'u'}, bitcoindPass: {alias:'w'} - , demo: {type:'boolean'}, enableInfluxdb: {type:'boolean'}, nodeEnv: {alias:'e', default:'production'} + , demo: {type:'boolean'}, rpcAllowall: {type:'boolean'} + , enableInfluxdb: {type:'boolean'}, nodeEnv: {alias:'e', default:'production'} } } ).flags; From fffa10eec8e092a81c45af55b7211f255ebb662c Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Tue, 5 Feb 2019 20:33:14 +0200 Subject: [PATCH 10/10] Fix typo, 365 days/year --- routes/baseActionsRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/baseActionsRouter.js b/routes/baseActionsRouter.js index 9866e7b..96da05c 100644 --- a/routes/baseActionsRouter.js +++ b/routes/baseActionsRouter.js @@ -44,7 +44,7 @@ router.get("/", function(req, res) { promises.push(coreApi.getMempoolInfo()); promises.push(coreApi.getMiningInfo()); - var chainTxStatsIntervals = [ 144, 144 * 7, 144 * 30, 144 * 265 ]; + var chainTxStatsIntervals = [ 144, 144 * 7, 144 * 30, 144 * 365 ]; res.locals.chainTxStatsLabels = [ "24 hours", "1 week", "1 month", "1 year", "All time" ]; for (var i = 0; i < chainTxStatsIntervals.length; i++) { promises.push(coreApi.getChainTxStats(chainTxStatsIntervals[i]));