Browse Source

Merge branch 'shesek-master'

fix-133-memory-crash
Dan Janosik 6 years ago
parent
commit
e70cdd2fce
  1. 5
      README.md
  2. 15
      app.js
  3. 13
      app/auth.js
  4. 24
      app/config.js
  5. 32
      app/defaultCredentials.js
  6. 2
      app/utils.js
  7. 69
      bin/cli.js
  8. 2
      bin/www
  9. 7
      package.json
  10. 41
      routes/baseActionsRouter.js
  11. 1
      views/browser.pug
  12. 2
      views/connect.pug
  13. 2
      views/layout.pug
  14. 1
      views/search.pug
  15. 3
      views/terminal.pug

5
README.md

@ -53,9 +53,12 @@ BTCEXP_BITCOIND_PORT = 8332
BTCEXP_BITCOIND_USER = username
BTCEXP_BITCOIND_PASS = password
BTCEXP_IPSTACK_KEY = 0000aaaafffffgggggg
BTCEXP_COOKIEPASSWORD = 0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
BTCEXP_COOKIE_SECRET = 0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
```
You may enable password protection by setting `BTCEXP_LOGIN=<password>`.
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 .`

15
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");
@ -27,6 +28,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 +48,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'));
@ -53,7 +61,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
}));
@ -447,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

13
app/auth.js

@ -0,0 +1,13 @@
var basicAuth = require('basic-auth');
module.exports = pass => (req, res, next) => {
var cred = basicAuth(req);
if (cred && cred.pass === pass) {
req.authenticated = true;
return next();
}
res.set('WWW-Authenticate', `Basic realm="Private Area"`)
.sendStatus(401);
}

24
app/config.js

@ -1,3 +1,5 @@
var fs = require('fs');
var crypto = require('crypto');
var coins = require("./coins.js");
var currentCoin = process.env.BTCEXP_COIN || "BTC";
@ -7,12 +9,27 @@ 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');
}
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,
rpcBlacklist:[
rpcBlacklist:
process.env.BTCEXP_RPC_ALLOWALL ? []
: process.env.BTCEXP_RPC_BLACKLIST ? process.env.BTCEXP_RPC_BLACKLIST.split(',').filter(Boolean)
: [
"addnode",
"backupwallet",
"bumpfee",
@ -92,9 +109,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"},

32
app/defaultCredentials.js

@ -1,18 +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,
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_INFLUXDB_ENABLED,
host: process.env.BTCEXP_INFLUXDB_HOST || "127.0.0.1",
port: process.env.BTCEXP_INFLUXDB_PORT || 8086,
database: process.env.BTCEXP_INFLUXDB_DB || "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

2
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) {

69
bin/cli.js

@ -0,0 +1,69 @@
#!/usr/bin/env node
const args = require('meow')(`
Usage
$ btc-rpc-explorer [options]
Options
-p, --port <port> port to bind http server [default: 3002]
-l, --login <password> protect web interface with a password [default: no password]
--coin <coin> crypto-coin to enable [default: BTC]
-b, --bitcoind-uri <uri> connection URI for bitcoind rpc (overrides the options below)
-H, --bitcoind-host <host> hostname for bitcoind rpc [default: 127.0.0.1]
-P, --bitcoind-port <port> port for bitcoind rpc [default: 8332]
-c, --bitcoind-cookie <path> path to bitcoind cookie file [default: 8332]
-u, --bitcoind-user <user> username for bitcoind rpc [default: none]
-w, --bitcoind-pass <pass> password for bitcoind rpc [default: none]
--rpc-allowall allow all rpc commands [default: false]
--rpc-blacklist <methods> comma separated list of rpc commands to block [default: see in config.js]
--cookie-secret <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 <key> api access key for ipstack (for geoip) [default: disabled]
--ganalytics-tracking <tid> tracking id for google analytics [default: disabled]
--sentry-url <sentry-url> sentry url [default: disabled]
--enable-influxdb enable influxdb for logging network stats [default: false]
--influxdb-uri <uri> connection URI for influxdb (overrides the options below)
--influxdb-host <host> hostname for influxdb [default: 127.0.0.1]
--influxdb-port <port> port for influxdb [default: 8086]
--influxdb-user <user> username for influxdb [default: admin]
--influxdb-pass <pass> password for influxdb [default: admin]
--influxdb-dbname <db> database name for influxdb [default: influxdb]
-e, --node-env <env> nodejs environment mode [default: production]
-h, --help output usage information
-v, --version output version number
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
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'}
, bitcoindUri: {alias:'b'}, bitcoindHost: {alias:'H'}, bitcoindPort: {alias:'P'}
, bitcoindCookie: {alias:'c'}, bitcoindUser: {alias:'u'}, bitcoindPass: {alias:'w'}
, demo: {type:'boolean'}, rpcAllowall: {type:'boolean'}
, enableInfluxdb: {type:'boolean'}, nodeEnv: {alias:'e', default:'production'}
} }
).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');

2
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);

7
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",
@ -20,11 +22,13 @@
"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",
"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",
@ -34,6 +38,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",

41
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']) {
@ -41,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]));
@ -743,31 +746,19 @@ 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.");
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.");
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+/);
var cmd = params.shift();
@ -815,17 +806,11 @@ router.post("/rpc-terminal", function(req, res) {
});
});
router.get("/rpc-browser", 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.");
router.get("/rpc-browser", function(req, res, next) {
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) {
res.locals.gethelp = result;
@ -883,6 +868,9 @@ router.get("/rpc-browser", function(req, res) {
return;
}
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) {
@ -904,6 +892,7 @@ router.get("/rpc-browser", function(req, res) {
res.render("browser");
});
});
} else {
res.render("browser");
}

1
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

2
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)

2
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")

1
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")

3
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",

Loading…
Cancel
Save