From 855546f886a6bb619deb7ab726981615bb3c079e Mon Sep 17 00:00:00 2001 From: Josh Harvey Date: Sun, 6 Nov 2016 20:07:49 +0000 Subject: [PATCH] WIP --- bin/lamassu-server | 36 +----------- lib/app.js | 113 +++++++++++++++++------------------- lib/plugins.js | 2 +- lib/postgresql_interface.js | 35 +---------- lib/routes.js | 25 +++++++- package.json | 5 +- yarn.lock | 72 ++++++++++++++++++++++- 7 files changed, 157 insertions(+), 131 deletions(-) diff --git a/bin/lamassu-server b/bin/lamassu-server index a7cc615d..923b3dd2 100755 --- a/bin/lamassu-server +++ b/bin/lamassu-server @@ -1,39 +1,7 @@ #!/usr/bin/env node -'use strict' - -var fs = require('fs') -var options = require('../lib/options') -process.env.LAMASSU_ENV = process.env.LAMASSU_ENV || options.logLevel || 'info' - -var createServer = require('../lib/app.js') -var argv = require('minimist')(process.argv.slice(2)) - -var port = process.env.PORT || 3000 -var httpOnly = options.httpOnly || argv.http - -if (!httpOnly) { - try { - options.https = { - key: fs.readFileSync(options.keyPath), - cert: fs.readFileSync(options.certPath) - } - } catch (err) { - console.log('Please configure your certificate.') - console.log(err) - process.exit(1) - } -} - -options.mock = argv.mock - -console.log('DEBUG23') +const app = require('../lib/app') process.on('unhandledRejection', err => console.log(err.stack)) -createServer(options) -.then(server => { - console.log('DEBUG22') - return server.listen(port, () => console.log('lamassu-server listening on port ' + - port + ' ' + (httpOnly ? '(http)' : '(https)'))) -}) +app.run() diff --git a/lib/app.js b/lib/app.js index 99a81ad7..139f7f34 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,33 +1,23 @@ -var http = require('http') -var https = require('https') -var express = require('express') -var bodyParser = require('body-parser') -var routes = require('./routes') -var plugins = require('./plugins') -var logger = require('./logger') -var configManager = require('./config-manager') +const fs = require('fs') +const http = require('http') +const https = require('https') +const express = require('express') +const routes = require('./routes') +const plugins = require('./plugins') +const logger = require('./logger') +var argv = require('minimist')(process.argv.slice(2)) -const helmet = require('helmet') +const configManager = require('./config-manager') +const options = require('./options') -const pair = require('./pair') +const devMode = argv.dev || argv.http || options.http -module.exports = function (options) { - var app = express() - var server - - app.use(helmet()) - app.use(bodyParser.json()) - - const psqlUrl = options.postgresql - if (!psqlUrl) { - console.log('Missing postgresql entry in configuration file') - process.exit(1) - } +function run () { + const app = express() + const localApp = express() const seedPath = options.seedPath || './seeds/seed.txt' - plugins.init(psqlUrl, seedPath) - - console.log('DEBUG6') + plugins.init(seedPath) return configManager.load() .then(config => { @@ -35,42 +25,47 @@ module.exports = function (options) { plugins.startPolling() plugins.startCheckingNotification() - var authMiddleware - - if (options.https) { - const serverOptions = { - key: options.https.key, - cert: options.https.cert, - requestCert: true - } - - server = https.createServer(serverOptions, app) - - authMiddleware = function (req, res, next) { - const deviceId = req.connection.getPeerCertificate().fingerprint - console.log(deviceId) - - return pair.isPaired(deviceId) - .then(r => { - if (r) { - req.deviceId = deviceId - return next() - } - - throw new Error('Unauthorized') - }) - .catch(e => res.status(403).end()) - } - } else { - server = http.createServer(app) - - authMiddleware = function (req, res, next) { - return next() - } + const httpsServerOptions = { + key: fs.readFileSync(options.keyPath), + cert: fs.readFileSync(options.certPath), + requestCert: true } - if (options.mock) logger.info('In mock mode') + const server = devMode + ? http.createServer(app) + : https.createServer(httpsServerOptions, app) - return server + const port = devMode + ? 3000 + : 443 + + const localPort = 3030 + const localServer = http.createServer(localApp) + + if (options.devMode) logger.info('In dev mode') + + const opts = { + app, + localApp, + devMode, + plugins + } + + routes.init(opts) + + server.listen(port, () => { + console.log('lamassu-server listening on port ' + + port + ' ' + (devMode ? '(http)' : '(https)')) + }) + + localServer.listen(localPort, 'localhost', () => { + console.log('lamassu-server listening on local port ' + localPort) + }) + }) + .catch(err => { + console.log(err) + process.exit(1) }) } + +module.exports = {run} diff --git a/lib/plugins.js b/lib/plugins.js index 5ba4c578..af0bf147 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -58,7 +58,7 @@ const coins = { let alertFingerprint = null let lastAlertTime = null -exports.init = function init (connectionString, seedPath) { +exports.init = function init (seedPath) { const masterSeed = new Buffer(fs.readFileSync(seedPath, 'utf8').trim(), 'hex') hkdf = new HKDF('sha256', 'lamassu-server-salt', masterSeed) } diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index 19d5029f..433df18f 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -2,8 +2,8 @@ 'use strict' const BigNumber = require('bignumber.js') +const db = require('./db') const pgp = require('pg-promise')() -var psqlUrl = require('../lib/options').postgresql const logger = require('./logger') @@ -28,10 +28,6 @@ function getInsertQuery (tableName, fields) { return query } -function connect () { - return pgp(psqlUrl) -} - // logs inputted bill and overall tx status (if available) exports.recordBill = function recordBill (deviceId, rec) { console.log('DEBUG10: %j', rec) @@ -61,8 +57,6 @@ exports.recordBill = function recordBill (deviceId, rec) { console.log('DEBUG11: %j', values) - const db = connect() - return db.none(getInsertQuery('bills', fields), values) .catch(err => { if (isUniqueViolation(err)) return logger.warn('Attempt to report bill twice') @@ -75,7 +69,6 @@ exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) { 'note, device_time) VALUES ($1, $2, $3, $4)' const values = [deviceId, event.eventType, event.note, event.deviceTime] - const db = connect() return db.none(sql, values) } @@ -102,13 +95,11 @@ exports.addOutgoingTx = function addOutgoingTx (deviceId, tx) { tx.error ] - const db = connect() return db.none(getInsertQuery('cash_in_txs', fields), values) } exports.sentCoins = function sentCoins (tx, toSend, fee, error, txHash) { const sql = 'update cash_in_txs set tx_hash=$1, error=$2 where id=$3' - const db = connect() return db.none(sql, [txHash, error, tx.id]) } @@ -131,7 +122,6 @@ exports.addInitialIncoming = function addInitialIncoming (deviceId, tx) { tx.error ] - const db = connect() return db.none(getInsertQuery('cash_out_txs', fields), values) } @@ -159,7 +149,6 @@ function insertDispense (deviceId, tx, cartridges) { false, tx.error ] - const db = connect() return db.none(sql, values) } @@ -168,7 +157,6 @@ exports.addIncomingPhone = function addIncomingPhone (tx, notified) { WHERE id=$3 AND phone IS NULL` const values = [tx.phone, notified, tx.id] - const db = connect() return db.result(sql, values) .then(results => { @@ -210,7 +198,6 @@ exports.fetchPhoneTxs = function fetchPhoneTxs (phone, dispenseTimeout) { 'AND (EXTRACT(EPOCH FROM (COALESCE(confirmation_time, now()) - created))) * 1000 < $3' const values = [phone, false, dispenseTimeout] - const db = connect() return db.any(sql, values) .then(rows => normalizeTxs(rows)) @@ -218,7 +205,6 @@ exports.fetchPhoneTxs = function fetchPhoneTxs (phone, dispenseTimeout) { exports.fetchTx = function fetchTx (txId) { const sql = 'SELECT * FROM cash_out_txs WHERE id=$1' - const db = connect() return db.one(sql, [txId]) .then(row => normalizeTx(row)) @@ -227,7 +213,6 @@ exports.fetchTx = function fetchTx (txId) { exports.addDispenseRequest = function addDispenseRequest (tx) { const sql = 'update cash_out_txs set dispensed=$1 where id=$2 and dispensed=$3' const values = [true, tx.id, false] - const db = connect() return db.result(sql, values) .then(results => { @@ -245,7 +230,6 @@ exports.addDispense = function addDispense (deviceId, tx, cartridges) { return insertDispense(deviceId, tx, cartridges) .then(() => { const sql2 = 'insert into cash_out_actions (cash_out_txs_id, action) values ($1, $2)' - const db = connect() return db.none(sql2, [tx.id, 'dispensed']) }) @@ -255,7 +239,6 @@ exports.cartridgeCounts = function cartridgeCounts (deviceId) { const sql = 'SELECT id, count1, count2 FROM dispenses ' + 'WHERE device_id=$1 AND refill=$2 ' + 'ORDER BY id DESC LIMIT 1' - const db = connect() return db.oneOrNone(sql, [deviceId, true]) .then(row => { @@ -272,7 +255,6 @@ exports.machineEvent = function machineEvent (rec) { const values = [rec.id, rec.deviceId, rec.eventType, rec.note, rec.deviceTime] const deleteSql = 'DELETE FROM machine_events WHERE (EXTRACT(EPOCH FROM (now() - created))) * 1000 > $1' const deleteValues = [TTL] - const db = connect() return db.none(sql, values) .then(() => db.none(deleteSql, deleteValues)) @@ -280,14 +262,12 @@ exports.machineEvent = function machineEvent (rec) { exports.devices = function devices () { const sql = 'SELECT device_id, name FROM devices' - const db = connect() return db.any(sql) } exports.machineEvents = function machineEvents () { const sql = 'SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events' - const db = connect() return db.any(sql, []) } @@ -301,7 +281,6 @@ exports.fetchOpenTxs = function fetchOpenTxs (statuses, age) { 'FROM cash_out_txs ' + 'WHERE ((EXTRACT(EPOCH FROM (now() - created))) * 1000)<$1 ' + 'AND status IN ' + _statuses - const db = connect() return db.any(sql, [age]) .then(rows => normalizeTxs(rows)) @@ -315,7 +294,6 @@ exports.fetchUnnotifiedTxs = function fetchUnnotifiedTxs (age, waitPeriod) { AND phone IS NOT NULL AND status IN ('instant', 'confirmed') AND (redeem=$4 OR ((EXTRACT(EPOCH FROM (now() - created))) * 1000)>$5)` - const db = connect() return db.any(sql, [age, false, false, true, waitPeriod]) .then(rows => normalizeTxs(rows)) @@ -336,7 +314,6 @@ exports.updateTxStatus = function updateTxStatus (tx, status) { const TransactionMode = pgp.txMode.TransactionMode const isolationLevel = pgp.txMode.isolationLevel const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable}) - const db = connect() function transaction (t) { const sql = 'select status, confirmation_time from cash_out_txs where id=$1' @@ -381,7 +358,6 @@ exports.updateTxStatus = function updateTxStatus (tx, status) { exports.updateRedeem = function updateRedeem (txId) { const sql = 'UPDATE cash_out_txs SET redeem=$1 WHERE id=$2' const values = [true, txId] - const db = connect() return db.none(sql, values) .then(() => { @@ -393,7 +369,6 @@ exports.updateRedeem = function updateRedeem (txId) { exports.updateNotify = function updateNotify (tx) { const sql = 'UPDATE cash_out_txs SET notified=$1 WHERE id=$2' const values = [true, tx.id] - const db = connect() return db.none(sql, values) .then(() => { @@ -410,7 +385,6 @@ function insertCachedRequest (deviceId, txId, path, method, body) { 'method', 'body' ] - const db = connect() const sql = getInsertQuery('cached_responses', fields) return db.none(sql, [deviceId, txId, path, method, body]) @@ -424,7 +398,6 @@ exports.cachedResponse = function (deviceId, txId, path, method) { and method=$4` const values = [deviceId, txId, path, method] - const db = connect() return insertCachedRequest(deviceId, txId, path, method, {pendingRequest: true}) .then(() => ({})) @@ -441,7 +414,6 @@ function pruneCachedResponses () { where (EXTRACT(EPOCH FROM (now() - created))) * 1000 < $1` const values = [CACHED_SESSION_TTL] - const db = connect() return db.none(sql, values) } @@ -455,7 +427,6 @@ exports.cacheResponse = function (deviceId, txId, path, method, body) { and method=$5` const values = [body, deviceId, txId, path, method] - const db = connect() return db.none(sql, values) } @@ -463,7 +434,6 @@ exports.cacheResponse = function (deviceId, txId, path, method, body) { exports.nextCashOutSerialHD = function nextCashOutSerialHD (txId, cryptoCode) { const sql = `select hd_serial from cash_out_hds where crypto_code=$1 order by hd_serial desc limit 1` - const db = connect() const attempt = () => db.oneOrNone(sql, [cryptoCode]) .then(row => { @@ -486,7 +456,6 @@ exports.fetchLiveHD = function fetchLiveHD () { ((extract(epoch from (now() - cash_out_txs.created))) * 1000)<$3` const values = ['confirmed', false, LIVE_SWEEP_TTL] - const db = connect() return db.any(sql, values) } @@ -496,14 +465,12 @@ exports.fetchOldHD = function fetchLiveHD () { where confirmed order by last_checked limit 10` - const db = connect() return db.any(sql) } exports.markSwept = function markSwept (txId) { const sql = 'update cash_out_hds set swept=$1 where id=$2' - const db = connect() return db.none(sql, [true, txId]) } diff --git a/lib/routes.js b/lib/routes.js index 2c338699..69071d55 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,5 +1,7 @@ 'use strict' +const helmet = require('helmet') +const bodyParser = require('body-parser') const BigNumber = require('bignumber.js') const logger = require('./logger') const configManager = require('./config-manager') @@ -332,13 +334,34 @@ function filterOldRequests (req, res, next) { next() } +function authorize (req, res, next) { + const deviceId = req.connection.getPeerCertificate().fingerprint + console.log(deviceId) + + return pair.isPaired(deviceId) + .then(r => { + if (r) { + req.deviceId = deviceId + return next() + } + + throw new Error('Unauthorized') + }) + .catch(e => res.status(403).end()) +} + function init (opts) { plugins = opts.plugins - const authMiddleware = opts.authMiddleware const app = opts.app const localApp = opts.localApp + const authMiddleware = opts.devMode + ? (req, res, next) => next() + : authorize + + app.use(helmet()) + app.use(bodyParser.json()) app.use(filterOldRequests) app.post('*', cacheAction) diff --git a/package.json b/package.json index 3482f8a6..8a210d71 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "license": "Unlicense", "author": "Lamassu (https://lamassu.is)", "dependencies": { + "ajv": "^4.8.2", "async": "~0.2.9", "bignumber.js": "^2.3.0", "body-parser": "^1.15.1", @@ -21,6 +22,7 @@ "lamassu-config": "lamassu/lamassu-config#alpha", "lamassu-identitymind": "^1.2.9", "lamassu-kraken": "^1.0.3", + "lamassu-mock-wallet": "^1.0.0", "lamassu-smtp2go": "^1.0.3", "lamassu-twilio": "^1.1.1", "migrate": "^0.2.2", @@ -48,6 +50,7 @@ "test": "mocha --recursive test" }, "devDependencies": { - "http-proxy": "^1.15.2" + "http-proxy": "^1.15.2", + "json-schema-generator": "^2.0.3" } } diff --git a/yarn.lock b/yarn.lock index 6d31191f..81da68a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,13 @@ accepts@~1.3.3: mime-types "~2.1.11" negotiator "0.6.1" +ajv: + version "4.8.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.8.2.tgz#65486936ca36fea39a1504332a78bebd5d447bdc" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + ansi-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" @@ -99,6 +106,10 @@ bl@~1.1.2: dependencies: readable-stream "~2.0.5" +bluebird@*: + version "3.4.6" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" + body-parser@^1.15.1: version "1.15.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.2.tgz#d7578cf4f1d11d5f6ea804cef35dc7a7ff6dae67" @@ -165,6 +176,10 @@ chalk@^1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -660,14 +675,40 @@ jsbn@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" +json-promise@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/json-promise/-/json-promise-1.1.8.tgz#7b74120422d16ddb449aa3170403fc69ad416402" + dependencies: + bluebird "*" + +json-schema-generator: + version "2.0.3" + resolved "https://registry.yarnpkg.com/json-schema-generator/-/json-schema-generator-2.0.3.tgz#e1a77df63cd0db4e1c8dfac03e08e2e4781502d3" + dependencies: + json-promise "^1.1.8" + mkdirp "^0.5.0" + optimist "^0.6.1" + pretty-data "^0.40.0" + request "^2.47.0" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + jsonpointer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" @@ -796,6 +837,10 @@ lamassu-kraken@^1.0.3: lodash "^4.8.1" promptly "^1.1.0" +lamassu-mock-wallet: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lamassu-mock-wallet/-/lamassu-mock-wallet-1.0.0.tgz#6d3ab723332e814fd50463c4917db1ce398845a8" + lamassu-smtp2go@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/lamassu-smtp2go/-/lamassu-smtp2go-1.0.3.tgz#dd9a3efcfb5f3d47016b4858536070a129b64958" @@ -901,10 +946,20 @@ minimist@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" +mkdirp@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + ms@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" @@ -978,6 +1033,13 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + packet-reader@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-0.2.0.tgz#819df4d010b82d5ea5671f8a1a3acf039bcd7700" @@ -1117,6 +1179,10 @@ postgres-interval@~1.0.0: dependencies: xtend "^4.0.0" +pretty-data@^0.40.0: + version "0.40.0" + resolved "https://registry.yarnpkg.com/pretty-data/-/pretty-data-0.40.0.tgz#572aa8ea23467467ab94b6b5266a6fd9c8fddd72" + pretty-ms@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-2.1.0.tgz#4257c256df3fb0b451d6affaab021884126981dc" @@ -1241,7 +1307,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@>=2.27.0: +request@^2.47.0, request@>=2.27.0: version "2.78.0" resolved "https://registry.yarnpkg.com/request/-/request-2.78.0.tgz#e1c8dec346e1c81923b24acdb337f11decabe9cc" dependencies: @@ -1551,6 +1617,10 @@ winston@^2.3.0: isstream "0.1.x" stack-trace "0.0.x" +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + wreck@^3.0.0, wreck@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/wreck/-/wreck-3.0.0.tgz#5151840a6c46d785bc81035a2e67b4e33406ba11"