diff --git a/bin/cert-gen.sh b/bin/cert-gen.sh new file mode 100755 index 00000000..c76e4361 --- /dev/null +++ b/bin/cert-gen.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# This is for setting up cryptographic certificates for a development environment +set -e + +DOMAIN=localhost + +LOG_FILE=/tmp/cert-gen.log +CERT_DIR=$PWD/certs +KEY_DIR=$PWD/certs + +CONFIG_DIR=$HOME/.lamassu + +mkdir -p $CERT_DIR +mkdir -p $CONFIG_DIR >> $LOG_FILE 2>&1 + +echo "Generating seed..." +SEEDS_DIR=seeds +SEED_FILE=$SEEDS_DIR/seed.txt +mkdir -p $SEEDS_DIR >> $LOG_FILE 2>&1 +SEED=$(openssl rand -hex 32) +echo $SEED > $SEED_FILE + +echo "Generating SSL certificates (takes a few seconds)..." + +CA_KEY_PATH=$KEY_DIR/Lamassu_OP_Root_CA.key +CA_PATH=$CERT_DIR/Lamassu_OP_Root_CA.pem +SERVER_KEY_PATH=$KEY_DIR/Lamassu_OP.key +SERVER_CERT_PATH=$CERT_DIR/Lamassu_OP.pem + +openssl genrsa \ + -out $CA_KEY_PATH \ + 4096 >> $LOG_FILE 2>&1 + +openssl req \ + -x509 \ + -new \ + -nodes \ + -key $CA_KEY_PATH \ + -days 3560 \ + -out $CA_PATH \ + -subj "/C=IS/ST=/L=Reykjavik/O=Lamassu Operator CA/CN=lamassu-operator.is" \ + >> $LOG_FILE 2>&1 + +openssl genrsa \ + -out $SERVER_KEY_PATH \ + 4096 >> $LOG_FILE 2>&1 + +openssl req -new \ + -key $SERVER_KEY_PATH \ + -out /tmp/Lamassu_OP.csr.pem \ + -subj "/C=IS/ST=/L=Reykjavik/O=Lamassu Operator/CN=$DOMAIN" \ + >> $LOG_FILE 2>&1 + +openssl x509 \ + -req -in /tmp/Lamassu_OP.csr.pem \ + -CA $CA_PATH \ + -CAkey $CA_KEY_PATH \ + -CAcreateserial \ + -out $SERVER_CERT_PATH \ + -days 3650 >> $LOG_FILE 2>&1 + +rm /tmp/Lamassu_OP.csr.pem + +cat < $CONFIG_DIR/lamassu.json +{ + "postgresql": "psql://lamassu:lamassu@localhost/lamassu", + "seedPath": "$SEED_FILE", + "caPath": "$CA_PATH", + "certPath": "$SERVER_CERT_PATH", + "keyPath": "$SERVER_KEY_PATH", + "hostname": "$DOMAIN", + "logLevel": "debug" +} +EOF + +echo "Done." diff --git a/bin/lamassu-server b/bin/lamassu-server index d86a9c90..33712533 100755 --- a/bin/lamassu-server +++ b/bin/lamassu-server @@ -4,6 +4,7 @@ const app = require('../lib/app') process.on('unhandledRejection', err => { console.log('Unhandled rejection') + console.dir(err) console.log(err.stack) }) diff --git a/lib/pairing.js b/lib/pairing.js index 8c0e3430..daf3259c 100644 --- a/lib/pairing.js +++ b/lib/pairing.js @@ -1,10 +1,9 @@ -const path = require('path') const fs = require('fs') const pify = require('pify') const readFile = pify(fs.readFile) const db = require('./db') - -const CA_PATH = path.resolve(__dirname, '..', 'certs', 'root-ca.crt.pem') +const options = require('./options') +const logger = require('./logger') function pullToken (token) { const sql = `delete from pairing_tokens @@ -14,26 +13,31 @@ function pullToken (token) { } function pair (token, deviceId) { - pullToken(token) + return pullToken(token) .then(r => { if (r.expired) return false const insertSql = `insert into devices (device_id, name) values ($1, $2) on conflict (device_id) - do update set name=$1, paired=TRUE, display=TRUE` + do update set name=$2, paired=TRUE, display=TRUE` return db.none(insertSql, [deviceId, r.name]) .then(() => true) }) - .catch(() => false) + .catch(err => { + logger.debug(err) + return false + }) } function authorizeCaDownload (caToken) { return pullToken(caToken) -} + .then(r => { + if (r.expired) throw new Error('Expired') -function ca () { - return readFile(CA_PATH) + const caPath = options.caPath + return readFile(caPath, {encoding: 'utf8'}) + }) } function isPaired (deviceId) { @@ -43,4 +47,4 @@ function isPaired (deviceId) { .then(() => true) } -module.exports = {pair, authorizeCaDownload, ca, isPaired} +module.exports = {pair, authorizeCaDownload, isPaired} diff --git a/lib/plugins.js b/lib/plugins.js index 666da3c4..2ecd2c15 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -171,16 +171,11 @@ exports.configure = function configure (config) { const cryptoCodes = getCryptoCodes() - console.log('DEBUG30') - return configManager.loadAccounts() .then(accounts => { cryptoCodes.forEach(cryptoCode => { - console.log('DEBUG31') const cryptoScopedConfig = configManager.cryptoScoped(cryptoCode, cachedConfig) - console.log('DEBUG31.1') - // TICKER [required] configure (or load) loadOrConfigPlugin( tickerPlugins[cryptoCode], @@ -195,8 +190,6 @@ exports.configure = function configure (config) { } ) - console.log('DEBUG31.2') - // Give each crypto a different derived seed so as not to allow any // plugin to spend another plugin's funds const cryptoSeed = hkdf.derive(cryptoCode, 32) @@ -298,7 +291,6 @@ exports.pollQueries = function pollQueries (deviceId) { function _sendCoins (toAddress, cryptoAtoms, cryptoCode) { return new Promise((resolve, reject) => { _sendCoinsCb(toAddress, cryptoAtoms, cryptoCode, (err, txHash) => { - console.log('DEBUG12: %j, %j', err, txHash) if (err) return reject(err) return resolve(txHash) }) @@ -308,7 +300,6 @@ function _sendCoins (toAddress, cryptoAtoms, cryptoCode) { function _sendCoinsCb (toAddress, cryptoAtoms, cryptoCode, cb) { const walletPlugin = walletPlugins[cryptoCode] const transactionFee = null - logger.debug('Sending coins [%s] to: %s', cryptoCode, toAddress) if (cryptoCode === 'BTC') { walletPlugin.sendBitcoins(toAddress, cryptoAtoms.truncated().toNumber(), transactionFee, cb) @@ -320,12 +311,9 @@ function _sendCoinsCb (toAddress, cryptoAtoms, cryptoCode, cb) { // NOTE: This will fail if we have already sent coins because there will be // a db unique db record in the table already. function executeTx (deviceId, tx) { - console.log('DEBUG16: %j', tx) return db.addOutgoingTx(deviceId, tx) .then(() => _sendCoins(tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)) .then(txHash => { - console.log('DEBUG13: %j', txHash) - const fee = null // Need to fill this out in plugins const toSend = {cryptoAtoms: tx.cryptoAtoms, fiat: tx.fiat} @@ -412,7 +400,6 @@ exports.cashOut = function cashOut (deviceId, tx) { } exports.dispenseAck = function (deviceId, tx) { - console.log('DEBUG23: %j', tx) const config = getConfig(deviceId) const cartridges = [ config.currencies.topCashOutDenomination, config.currencies.bottomCashOutDenomination ] @@ -767,7 +754,6 @@ function checkNotification () { return sendMessage(rec) }) .then(results => { - console.log('DEBUG25') if (results && results.length > 0) logger.debug('Successfully sent alerts') }) .catch(err => { diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index da84b42a..bca24b30 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -29,7 +29,6 @@ function getInsertQuery (tableName, fields) { // logs inputted bill and overall tx status (if available) exports.recordBill = function recordBill (deviceId, rec) { - console.log('DEBUG10: %j', rec) const fields = [ 'id', 'device_id', @@ -54,8 +53,6 @@ exports.recordBill = function recordBill (deviceId, rec) { rec.fiat ] - console.log('DEBUG11: %j', values) - return db.none(getInsertQuery('bills', fields), values) .catch(err => { if (isUniqueViolation(err)) return logger.warn('Attempt to report bill twice') @@ -127,24 +124,19 @@ exports.addInitialIncoming = function addInitialIncoming (deviceId, tx) { function insertDispense (deviceId, tx, cartridges) { const fields = [ 'device_id', 'cash_out_txs_id', - 'dispense1', 'reject1', 'count1', - 'dispense2', 'reject2', 'count2', - 'refill', 'error' + 'dispense1', 'reject1', + 'dispense2', 'reject2', 'error' ] const sql = getInsertQuery('dispenses', fields) - console.log('DEBUG24: %j', tx) - const dispense1 = tx.bills[0].actualDispense const dispense2 = tx.bills[1].actualDispense const reject1 = tx.bills[0].rejected const reject2 = tx.bills[1].rejected - const count1 = cartridges[0].count - const count2 = cartridges[1].count const values = [ deviceId, tx.id, - dispense1, reject1, count1, dispense2, reject2, count2, + dispense1, reject1, dispense2, reject2, false, tx.error ] diff --git a/lib/routes.js b/lib/routes.js index 6f484139..389933dd 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,5 +1,6 @@ 'use strict' +const morgan = require('morgan') const helmet = require('helmet') const bodyParser = require('body-parser') const BigNumber = require('bignumber.js') @@ -76,20 +77,13 @@ function poll (req, res) { const config = plugins.getConfig(deviceId) - console.log('DEBUG30') - plugins.pollQueries(deviceId) .then(results => { - console.log('DEBUG31') - const cartridges = results.cartridges const reboot = pid && reboots[deviceId] && reboots[deviceId] === pid - console.log('DEBUG2: %j, %s, %s', reboots, reboot, pid) const langs = config.languages.machineLanguages - console.log('DEBUG33.1') - const locale = { currency: config.currencies.fiatCurrency, localeInfo: { @@ -126,7 +120,6 @@ function poll (req, res) { } function trade (req, res, next) { - console.log('DEBUG24') const tx = req.body tx.cryptoAtoms = new BigNumber(tx.cryptoAtoms) @@ -183,22 +176,20 @@ function ca (req, res) { const token = req.query.token return pairing.authorizeCaDownload(token) - .then(valid => { - if (valid) return res.json({ca: pairing.ca()}) - - return res.status(408).end() - }) + .then(ca => res.json({ca})) + .catch(() => res.status(408).end()) } -function pair (req, res) { +function pair (req, res, next) { const token = req.query.token const deviceId = getDeviceId(req) return pairing.pair(token, deviceId) .then(valid => { - if (valid) return cacheAndRespond(req, res) + if (valid) return res.end() throw httpError('Pairing failed') }) + .catch(next) } function phoneCode (req, res) { @@ -233,20 +224,17 @@ function registerRedeem (req, res) { .then(() => cacheAndRespond(req, res)) } -function waitForDispense (req, res) { +function waitForDispense (req, res, next) { logger.debug('waitForDispense') return plugins.fetchTx(req.params.txId) - .then(tx => { - logger.debug('tx fetched') - logger.debug(tx) - if (!tx) return res.sendStatus(404) - if (tx.status === req.query.status) return res.sendStatus(304) - res.json({tx}) - }) - .catch(err => { - logger.error(err) - res.sendStatus(500) - }) + .then(tx => { + logger.debug('tx fetched') + logger.debug(tx) + if (!tx) return res.sendStatus(404) + if (tx.status === req.query.status) return res.sendStatus(304) + res.json({tx}) + }) + .catch(next) } function dispense (req, res) { @@ -261,12 +249,12 @@ function isUniqueViolation (err) { } function cacheAction (req, res, next) { - console.log('DEBUG22: %s', req.path) + const requestId = req.headers['request-id'] + if (!requestId) return next() const sql = `insert into idempotents (request_id, device_id, body, status, pending) values ($1, $2, $3, $4, $5)` - const requestId = req.headers['request-id'] const deviceId = getDeviceId(req) db.none(sql, [requestId, deviceId, {}, 204, true]) @@ -284,21 +272,25 @@ function cacheAction (req, res, next) { } function updateCachedAction (req, body, status) { + const requestId = req.headers['request-id'] + if (!requestId) return Promise.resolve() + const sql = `update idempotents set body=$1, status=$2, pending=$3 where request_id=$4 and device_id=$5 and pending=$6` - const requestId = req.headers['request-id'] const deviceId = getDeviceId(req) return db.none(sql, [body, status, false, requestId, deviceId, true]) } -function postErrorHandler (err, req, res, next) { +function errorHandler (err, req, res, next) { const statusCode = err.code || 500 const json = {error: err.message} + logger.debug(err) + return updateCachedAction(req, json, statusCode) - .then(() => res.status(statusCode).json({})) + .then(() => res.status(statusCode).json(json)) } function cacheAndRespond (req, res, _body, _status) { @@ -361,6 +353,7 @@ function init (opts) { ? (req, res, next) => next() : authorize + app.use(morgan('dev')) app.use(helmet()) app.use(bodyParser.json()) app.use(filterOldRequests) @@ -388,7 +381,7 @@ function init (opts) { app.get('/await_dispense/:txId', authMiddleware, waitForDispense) app.post('/dispense', authMiddleware, dispense) - app.post('*', postErrorHandler) + app.use('*', errorHandler) localApp.get('/pid', (req, res) => { const deviceId = req.query.device_id diff --git a/migrations/019-remove-dispense-counts.js b/migrations/019-remove-dispense-counts.js new file mode 100644 index 00000000..fec34664 --- /dev/null +++ b/migrations/019-remove-dispense-counts.js @@ -0,0 +1,14 @@ +var db = require('./db') + +exports.up = function (next) { + var sql = [ + 'alter table dispenses drop column count1', + 'alter table dispenses drop column count2', + 'alter table dispenses drop column refill' + ] + db.multi(sql, next) +} + +exports.down = function (next) { + next() +} diff --git a/package.json b/package.json index 14602749..eee1e49a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "lamassu-twilio": "^1.1.1", "migrate": "^0.2.2", "minimist": "^1.2.0", + "morgan": "^1.7.0", "node-hkdf-sync": "^1.0.0", "numeral": "^1.5.3", "pg": "^6.1.0", diff --git a/yarn.lock b/yarn.lock index f6dc4717..66cbec07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -86,6 +86,10 @@ base64url@^2.0.0, base64url@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" +basic-auth@~1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.0.4.tgz#030935b01de7c9b94a824b29f3fccb750d3a5290" + bcrypt-pbkdf@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" @@ -969,6 +973,16 @@ mkdirp@^0.5.0: dependencies: minimist "0.0.8" +morgan: + version "1.7.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.7.0.tgz#eb10ca8e50d1abe0f8d3dad5c0201d052d981c62" + dependencies: + basic-auth "~1.0.3" + debug "~2.2.0" + depd "~1.1.0" + on-finished "~2.3.0" + on-headers "~1.0.1" + ms@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" @@ -1040,6 +1054,10 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"