Files
purple-explorer/app.js
T
joeuhren 374ab2d7aa Add new settings to disable individual public apis
-All coind + extended public api calls can now be enabled/disabled individually via settings.json; Disabled apis still work internally when disabled; The only noteworthy exception is the extended /ext/getlasttxs endpoint which is the only extended api consumed by the explorer itself, although it works to prevent outside access by default, it is controlled by http headers which can be manipulated and it's extremely likely that a savvy user could enable usage of /ext/getlasttxs for their own needs even if the site has specifically disabled that public api. More thought may be needed to properly resolve that problem, but it may also not be a big deal since it's data that is available to the explorer and in no way hidden or secret
-Disabled apis do not show up on the /info page and will return a "This method is disabled" msg if the endpoint is called
-Applied some code formatting to the /lib/nodeapi.js file
2020-12-28 15:12:56 -07:00

491 lines
18 KiB
JavaScript

var express = require('express')
, path = require('path')
, nodeapi = require('./lib/nodeapi')
, favicon = require('serve-favicon')
, logger = require('morgan')
, cookieParser = require('cookie-parser')
, bodyParser = require('body-parser')
, settings = require('./lib/settings')
, routes = require('./routes/index')
, lib = require('./lib/explorer')
, db = require('./lib/database')
, package_metadata = require('./package.json')
, locale = require('./lib/locale')
, request = require('postman-request');
var app = express();
var apiAccessList = [];
// pass wallet rpc connection info to nodeapi
nodeapi.setWalletDetails(settings.wallet);
// dynamically build the nodeapi cmd access list by adding all non-heavy api cmds that have a value
Object.keys(settings.api_cmds).forEach(function(key, index, map) {
if (key != 'heavies' && settings.api_cmds[key] != null && settings.api_cmds[key] != '')
apiAccessList.push(key);
});
if (settings.heavy) {
// add all heavy api cmds that have a value
Object.keys(settings.api_cmds.heavies).forEach(function(key, index, map) {
if (settings.api_cmds.heavies[key] != null && settings.api_cmds.heavies[key] != '')
apiAccessList.push(key);
});
}
// whitelist the cmds in the nodeapi access list
nodeapi.setAccess('only', apiAccessList);
// determine if cors should be enabled
if (settings.usecors == true) {
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", settings.corsorigin);
res.header('Access-Control-Allow-Methods', 'DELETE, PUT, GET, POST');
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
}
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(favicon(path.join(__dirname, settings.favicon)));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// routes
app.use('/api', nodeapi.app);
app.use('/', routes);
app.use('/ext/getmoneysupply', function(req, res) {
// check if the getmoneysupply api is enabled
if (settings.public_api.ext['getmoneysupply']) {
lib.get_supply(function(supply) {
res.setHeader('content-type', 'text/plain');
res.end((supply ? supply.toString() : '0'));
});
} else
res.end('This method is disabled');
});
app.use('/ext/getaddress/:hash', function(req, res) {
// check if the getaddress api is enabled
if (settings.public_api.ext['getaddress']) {
db.get_address(req.params.hash, false, function(address) {
db.get_address_txs_ajax(req.params.hash, 0, settings.txcount, function(txs, count) {
if (address) {
var last_txs = [];
for (i = 0; i < txs.length; i++) {
if (typeof txs[i].txid !== "undefined") {
var out = 0,
vin = 0,
tx_type = 'vout',
row = {};
txs[i].vout.forEach(function (r) {
if (r.addresses == req.params.hash)
out += r.amount;
});
txs[i].vin.forEach(function (s) {
if (s.addresses == req.params.hash)
vin += s.amount;
});
if (vin > out)
tx_type = 'vin';
row['addresses'] = txs[i].txid;
row['type'] = tx_type;
last_txs.push(row);
}
}
var a_ext = {
address: address.a_id,
sent: (address.sent / 100000000),
received: (address.received / 100000000),
balance: (address.balance / 100000000).toString().replace(/(^-+)/mg, ''),
last_txs: last_txs
};
res.send(a_ext);
} else
res.send({ error: 'address not found.', hash: req.params.hash});
});
});
} else
res.end('This method is disabled');
});
app.use('/ext/gettx/:txid', function(req, res) {
// check if the gettx api is enabled
if (settings.public_api.ext['gettx']) {
var txid = req.params.txid;
db.get_tx(txid, function(tx) {
if (tx) {
lib.get_blockcount(function(blockcount) {
res.send({ active: 'tx', tx: tx, confirmations: settings.confirmations, blockcount: (blockcount ? blockcount : 0)});
});
}
else {
lib.get_rawtransaction(txid, function(rtx) {
if (rtx && rtx.txid) {
lib.prepare_vin(rtx, function(vin) {
lib.prepare_vout(rtx.vout, rtx.txid, vin, ((typeof rtx.vjoinsplit === 'undefined' || rtx.vjoinsplit == null) ? [] : rtx.vjoinsplit), function(rvout, rvin) {
lib.calculate_total(rvout, function(total) {
if (!rtx.confirmations > 0) {
var utx = {
txid: rtx.txid,
vin: rvin,
vout: rvout,
total: total.toFixed(8),
timestamp: rtx.time,
blockhash: '-',
blockindex: -1,
};
res.send({ active: 'tx', tx: utx, confirmations: settings.confirmations, blockcount:-1});
} else {
var utx = {
txid: rtx.txid,
vin: rvin,
vout: rvout,
total: total.toFixed(8),
timestamp: rtx.time,
blockhash: rtx.blockhash,
blockindex: rtx.blockheight,
};
lib.get_blockcount(function(blockcount) {
res.send({ active: 'tx', tx: utx, confirmations: settings.confirmations, blockcount: (blockcount ? blockcount : 0)});
});
}
});
});
});
} else {
res.send({ error: 'tx not found.', hash: txid});
}
});
}
});
} else
res.end('This method is disabled');
});
app.use('/ext/getbalance/:hash', function(req, res) {
// check if the getbalance api is enabled
if (settings.public_api.ext['getbalance']) {
db.get_address(req.params.hash, false, function(address) {
if (address) {
res.setHeader('content-type', 'text/plain');
res.end((address.balance / 100000000).toString().replace(/(^-+)/mg, ''));
} else
res.send({ error: 'address not found.', hash: req.params.hash });
});
} else
res.end('This method is disabled');
});
app.use('/ext/getdistribution', function(req, res) {
// check if the getdistribution api is enabled
if (settings.public_api.ext['getdistribution']) {
db.get_richlist(settings.coin, function(richlist) {
db.get_stats(settings.coin, function(stats) {
db.get_distribution(richlist, stats, function(dist) {
res.send(dist);
});
});
});
} else
res.end('This method is disabled');
});
app.use('/ext/getcurrentprice', function(req, res) {
// check if the getcurrentprice api is enabled
if (settings.public_api.ext['getcurrentprice']) {
db.get_stats(settings.coin, function (stats) {
eval('var p_ext = { "last_price_'+settings.markets.exchange.toLowerCase()+'": stats.last_price, "last_price_usd": stats.last_usd_price, }');
res.send(p_ext);
});
} else
res.end('This method is disabled');
});
app.use('/ext/getbasicstats', function(req, res) {
// check if the getbasicstats api is enabled
if (settings.public_api.ext['getbasicstats']) {
lib.get_blockcount(function(blockcount) {
lib.get_supply(function(supply) {
db.get_stats(settings.coin, function (stats) {
lib.get_masternodecount(function(masternodestotal) {
eval('var p_ext = { "block_count": (blockcount ? blockcount : 0), "money_supply": (supply ? supply : 0), "last_price_'+settings.markets.exchange.toLowerCase()+'": stats.last_price, "last_price_usd": stats.last_usd_price, "masternode_count": masternodestotal.total }');
res.send(p_ext);
});
});
});
});
} else
res.end('This method is disabled');
});
app.use('/ext/getlasttxs/:min', function(req, res) {
// check if the getlasttxs api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
if (settings.public_api.ext['getlasttxs'] || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
var min = req.params.min, start, length;
// split url suffix by forward slash and remove blank entries
var split = req.url.split('/').filter(function(v) { return v; });
// determine how many parameters were passed
switch (split.length) {
case 2:
// capture start and length
start = split[0];
length = split[1];
break;
default:
if (split.length == 1) {
// capture start
start = split[0];
} else if (split.length > 2) {
// capture start and length
start = split[0];
length = split[1];
}
break;
}
// fix parameters
if (typeof length === 'undefined' || isNaN(length) || length > settings.index.last_txs)
length = settings.index.last_txs;
if (typeof start === 'undefined' || isNaN(start) || start < 0)
start = 0;
if (typeof min === 'undefined' || isNaN(min) || min < 0)
min = 0;
else
min = (min * 100000000);
db.get_last_txs(start, length, min, function(data, count) {
res.json({"data":data, "recordsTotal": count, "recordsFiltered": count});
});
} else
res.end('This method is disabled');
});
app.use('/ext/getaddresstxs/:address/:start/:length', function(req,res) {
// fix parameters
if (typeof req.params.length === 'undefined' || isNaN(req.params.length) || req.params.length > settings.txcount)
req.params.length = settings.txcount;
if (typeof req.params.start === 'undefined' || isNaN(req.params.start) || req.params.start < 0)
req.params.start = 0;
if (typeof req.params.min === 'undefined' || isNaN(req.params.min) || req.params.min < 0)
req.params.min = 0;
else
req.params.min = (req.params.min * 100000000);
db.get_address_txs_ajax(req.params.address, req.params.start, req.params.length, function(txs, count) {
var data = [];
for (i = 0; i < txs.length; i++) {
if (typeof txs[i].txid !== "undefined") {
var out = 0;
var vin = 0;
txs[i].vout.forEach(function(r) {
if (r.addresses == req.params.address) {
out += r.amount;
}
});
txs[i].vin.forEach(function(s) {
if (s.addresses == req.params.address) {
vin += s.amount;
}
});
var row = [];
row.push(new Date((txs[i].timestamp) * 1000).toUTCString());
row.push(txs[i].txid);
row.push(out);
row.push(vin);
row.push(txs[i].balance);
data.push(row);
}
}
res.json({"data":data, "recordsTotal": count, "recordsFiltered": count});
});
});
app.post('/claim', function(req, res) {
// initialize the bad-words filter
var bad_word_lib = require('bad-words');
var bad_word_filter = new bad_word_lib();
// clean the message (Display name) of bad words
var message = (req.body.message == null || req.body.message == '' ? '' : bad_word_filter.clean(req.body.message));
// check if the message was filtered
if (message == req.body.message) {
// call the verifymessage api
lib.verify_message(req.body.address, req.body.signature, req.body.message, function(body) {
if (body == false) {
res.json({'status': 'failed', 'error': true, 'message': 'Invalid signature'});
} else if (body == true) {
db.update_label(req.body.address, req.body.message, function(val) {
// check if the update was successful
if (val == '')
res.json({'status': 'success'});
else if (val == 'no_address')
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address ' + req.body.address + ' is not valid or does not have any transactions'});
else
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
});
} else
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
});
} else {
// message was filtered which would change the signature
res.json({'status': 'failed', 'error': true, 'message': 'Display name contains bad words and cannot be saved: ' + message});
}
});
app.use('/ext/connections', function(req,res){
db.get_peers(function(peers){
res.send({data: peers});
});
});
// locals
app.set('title', settings.title);
app.set('explorer_version', package_metadata.version);
app.set('symbol', settings.symbol);
app.set('coin', settings.coin);
app.set('locale', locale);
app.set('display', settings.display);
app.set('markets', settings.markets);
app.set('twitter', settings.twitter);
app.set('facebook', settings.facebook);
app.set('googleplus', settings.googleplus);
app.set('bitcointalk', settings.bitcointalk);
app.set('github', settings.github);
app.set('slack', settings.slack);
app.set('discord', settings.discord);
app.set('telegram', settings.telegram);
app.set('reddit', settings.reddit);
app.set('youtube', settings.youtube);
app.set('website', settings.website);
app.set('genesis_block', settings.genesis_block);
app.set('index', settings.index);
app.set('use_rpc', settings.use_rpc);
app.set('heavy', settings.heavy);
app.set('save_stats_after_sync_blocks', settings.save_stats_after_sync_blocks);
app.set('lock_during_index', settings.lock_during_index);
app.set('txcount', settings.txcount);
app.set('txcount_per_page', settings.txcount_per_page);
app.set('nethash', settings.nethash);
app.set('nethash_units', settings.nethash_units);
app.set('show_sent_received', settings.show_sent_received);
app.set('logo', settings.logo);
// Check if header logo file exists
if (db.fs.existsSync(path.join('./public', settings.headerlogo)))
app.set('headerlogo', settings.headerlogo);
else
app.set('headerlogo', '');
app.set('theme', settings.theme);
app.set('labels', settings.labels);
app.set('homelink', settings.homelink);
app.set('logoheight', settings.logoheight);
app.set('burned_coins', settings.burned_coins);
app.set('public_api', settings.public_api);
app.set('api_cmds', settings.api_cmds);
app.set('sticky_header', settings.sticky_header);
app.set('sticky_footer', settings.sticky_footer);
app.set('footer_height_desktop', settings.footer_height_desktop);
app.set('footer_height_tablet', settings.footer_height_tablet);
app.set('footer_height_mobile', settings.footer_height_mobile);
app.set('social_link_percent_height_desktop', settings.social_link_percent_height_desktop);
app.set('social_link_percent_height_tablet', settings.social_link_percent_height_tablet);
app.set('social_link_percent_height_mobile', settings.social_link_percent_height_mobile);
// determine panel offset based on which panels are enabled
var paneltotal = 5;
var panelcount = (settings.display.networkpnl > 0 ? 1 : 0) +
(settings.display.difficultypnl > 0 ? 1 : 0) +
(settings.display.masternodespnl > 0 ? 1 : 0) +
(settings.display.coinsupplypnl > 0 ? 1 : 0) +
(settings.display.pricepnl > 0 ? 1 : 0) +
(settings.display.marketcappnl > 0 ? 1 : 0) +
(settings.display.logopnl > 0 ? 1 : 0);
app.set('paneloffset', paneltotal + 1 - panelcount);
// determine panel order
var panelorder = new Array();
if (settings.display.networkpnl > 0) panelorder.push({name: 'networkpnl', val: settings.display.networkpnl});
if (settings.display.difficultypnl > 0) panelorder.push({name: 'difficultypnl', val: settings.display.difficultypnl});
if (settings.display.masternodespnl > 0) panelorder.push({name: 'masternodespnl', val: settings.display.masternodespnl});
if (settings.display.coinsupplypnl > 0) panelorder.push({name: 'coinsupplypnl', val: settings.display.coinsupplypnl});
if (settings.display.pricepnl > 0) panelorder.push({name: 'pricepnl', val: settings.display.pricepnl});
if (settings.display.marketcappnl > 0) panelorder.push({name: 'marketcappnl', val: settings.display.marketcappnl});
if (settings.display.logopnl > 0) panelorder.push({name: 'logopnl', val: settings.display.logopnl});
panelorder.sort(function(a,b) { return a.val - b.val; });
for (var i=1; i<6; i++)
app.set('panel'+i.toString(), ((panelorder.length >= i) ? panelorder[i-1].name : ''));
// Dynamically populate market data
var market_data = [];
settings.markets.enabled.forEach(function (market) {
// Check if market file exists
if (db.fs.existsSync('./lib/markets/' + market + '.js')) {
// Load market file
var exMarket = require('./lib/markets/' + market);
// Save market_name and market_logo from market file to settings
eval('market_data.push({id: "' + market + '", name: "' + (exMarket.market_name == null ? '' : exMarket.market_name) + '", logo: "' + (exMarket.market_logo == null ? '' : exMarket.market_logo) + '"});');
}
});
// Sort market data by name
market_data.sort(function(a, b) {
var name1 = a.name.toLowerCase();
var name2 = b.name.toLowerCase();
if (name1 < name2)
return -1;
else if (name1 > name2)
return 1;
else
return 0;
});
app.set('market_data', market_data);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;