diff --git a/bin/lamassu-admin-server b/bin/lamassu-admin-server index 70383c72..55e2d9b3 100755 --- a/bin/lamassu-admin-server +++ b/bin/lamassu-admin-server @@ -22,6 +22,7 @@ const pairing = require('../lib/admin/pairing') const server = require('../lib/admin/server') const transactions = require('../lib/admin/transactions') const T = require('../lib/time') +const logger = require('../lib/logger') const NEVER = new Date(Date.now() + 100 * T.years) @@ -29,6 +30,9 @@ const devMode = argv.dev let serverConfig +const version = require('../package.json').version +logger.info('Version: %s', version) + try { const homeConfigPath = path.resolve(os.homedir(), '.lamassu', 'lamassu.json') serverConfig = JSON.parse(fs.readFileSync(homeConfigPath)) @@ -161,7 +165,9 @@ function register (req, res, next) { return login.register(otp) .then(r => { if (r.expired) return res.status(401).send('OTP expired, generate new registration link') - if (!r.success) return res.status(401).send('Registration failed') + + // Maybe user is using old registration key, attempt to authenticate + if (!r.success) return next() const cookieOpts = { httpOnly: true, diff --git a/lib/app.js b/lib/app.js index 3810e9fa..484d5c85 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,7 +1,6 @@ const fs = require('fs') const http = require('http') const https = require('https') -const express = require('express') const argv = require('minimist')(process.argv.slice(2)) const routes = require('./routes') @@ -11,7 +10,10 @@ const verifySchema = require('./verify-schema') const settingsLoader = require('./settings-loader') const options = require('./options') -const devMode = argv.dev || argv.http || options.http +const devMode = argv.dev || options.http + +const version = require('../package.json').version +logger.info('Version: %s', version) function run () { let count = 0 @@ -31,9 +33,6 @@ function run () { } function runOnce () { - const app = express() - const localApp = express() - return verifySchema.valid() .then(() => settingsLoader.loadLatest()) .then(settings => { @@ -46,23 +45,15 @@ function runOnce () { } const server = devMode - ? http.createServer(app) - : https.createServer(httpsServerOptions, app) + ? http.createServer(routes.app) + : https.createServer(httpsServerOptions, routes.app) const port = 3000 const localPort = 3030 - const localServer = http.createServer(localApp) + const localServer = http.createServer(routes.localApp) if (options.devMode) logger.info('In dev mode') - const opts = { - app, - localApp, - devMode - } - - routes.init(opts) - server.listen(port, () => { console.log('lamassu-server listening on port ' + port + ' ' + (devMode ? '(http)' : '(https)')) diff --git a/lib/db.js b/lib/db.js index 6523e2d4..ce217abd 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,4 +1,13 @@ -const pgp = require('pg-promise')() +const Pgp = require('pg-promise') const psqlUrl = require('../lib/options').postgresql +const logger = require('./logger') -module.exports = pgp(psqlUrl) +const pgp = Pgp({ + pgNative: true, + error: (_, e) => { + if (e.cn) logger.error('Database not reachable.') + } +}) + +const db = pgp(psqlUrl) +module.exports = db diff --git a/lib/logger.js b/lib/logger.js index ff1d9411..5225d2cd 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -8,8 +8,4 @@ const logger = new winston.Logger({ ] }) -// log version -var version = require('../package.json').version -logger.info('Version: %s', version) - module.exports = logger diff --git a/lib/pairing.js b/lib/pairing.js index daf3259c..df8e79e2 100644 --- a/lib/pairing.js +++ b/lib/pairing.js @@ -43,8 +43,8 @@ function authorizeCaDownload (caToken) { function isPaired (deviceId) { const sql = 'select device_id from devices where device_id=$1 and paired=TRUE' - return db.one(sql, [deviceId]) - .then(() => true) + return db.oneOrNone(sql, [deviceId]) + .then(row => row && row.device_id === deviceId) } module.exports = {pair, authorizeCaDownload, isPaired} diff --git a/lib/routes.js b/lib/routes.js index a166904c..07e29089 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -2,10 +2,10 @@ const morgan = require('morgan') const helmet = require('helmet') -const RateLimit = require('express-rate-limit') const bodyParser = require('body-parser') const BigNumber = require('bignumber.js') const _ = require('lodash/fp') +const express = require('express') const options = require('./options') const logger = require('./logger') @@ -17,9 +17,7 @@ const settingsLoader = require('./settings-loader') const plugins = require('./plugins') const helpers = require('./route-helpers') const poller = require('./poller') -const T = require('./time') - -module.exports = {init} +const argv = require('minimist')(process.argv.slice(2)) const CLOCK_SKEW = 60 * 1000 const REQUEST_TTL = 3 * 60 * 1000 @@ -27,6 +25,8 @@ const REQUEST_TTL = 3 * 60 * 1000 const pids = {} const reboots = {} +const devMode = argv.dev || options.http + function poll (req, res, next) { const deviceId = req.deviceId const deviceTime = req.deviceTime @@ -153,7 +153,7 @@ function ca (req, res) { return pairing.authorizeCaDownload(token) .then(ca => res.json({ca})) - .catch(() => res.status(408).end()) + .catch(() => res.sendStatus(403)) } function pair (req, res, next) { @@ -320,107 +320,91 @@ function authorize (req, res, next) { return next() } - throw httpError('Unauthorized', 403) + return res.sendStatus(403) }) .catch(next) } -function init (opts) { - const skip = options.logLevel === 'debug' - ? () => false - : (req, res) => _.includes(req.path, ['/poll', '/state']) && res.statusCode === 200 +const skip = options.logLevel === 'debug' +? () => false +: (req, res) => _.includes(req.path, ['/poll', '/state']) && res.statusCode === 200 - const app = opts.app - const localApp = opts.localApp +const configRequiredRoutes = [ + '/poll', + '/trade', + '/send', + '/cash_out', + '/dispense_ack', + '/event', + '/verify_user', + '/verify_transaction', + '/phone_code' +] - const authMiddleware = opts.devMode - ? (req, res, next) => next() - : authorize +const app = express() +const localApp = express() - const configRequiredRoutes = [ - '/poll', - '/trade', - '/send', - '/cash_out', - '/dispense_ack', - '/event', - '/verify_user', - '/verify_transaction', - '/phone_code' - ] +app.use(helmet({noCache: true})) +app.use(bodyParser.json()) +app.use(morgan('dev', {skip})) - const limiter = new RateLimit({ - windowMs: T.minute, - max: 10, - delayMs: 0, - delayAfter: 0, - keyGenerator: () => 'everybody' +// These two have their own authorization +app.post('/pair', populateDeviceId, pair) +app.get('/ca', ca) + +app.use(populateDeviceId) +if (!devMode) app.use(authorize) +app.use(configRequiredRoutes, populateSettings) +app.use(filterOldRequests) +app.post('*', cacheAction) + +app.get('/poll', poll) +app.post('/trade', trade) +app.post('/send', send) +app.post('/state', stateChange) +app.post('/cash_out', cashOut) +app.post('/dispense_ack', dispenseAck) + +app.post('/event', deviceEvent) +app.post('/verify_user', verifyUser) +app.post('/verify_transaction', verifyTx) + +app.post('/phone_code', phoneCode) +app.post('/update_phone', updatePhone) +app.get('/phone_tx', fetchPhoneTx) +app.post('/register_redeem/:txId', registerRedeem) +app.get('/await_dispense/:txId', waitForDispense) +app.post('/dispense', dispense) + +app.use(errorHandler) + +localApp.get('/pid', (req, res) => { + const deviceId = req.query.device_id + const pidRec = pids[deviceId] + res.json(pidRec) +}) + +localApp.post('/reboot', (req, res) => { + const pid = req.body.pid + const deviceId = req.body.deviceId + + if (!deviceId || !pid) { + return res.sendStatus(400) + } + + reboots[deviceId] = pid + res.sendStatus(200) +}) + +localApp.post('/dbChange', (req, res, next) => { + return settingsLoader.loadLatest() + .then(poller.reload) + .then(() => logger.info('Config reloaded')) + .catch(err => { + logger.error(err) + res.sendStatus(500) }) - - app.use(morgan('dev', {skip})) - app.use(helmet()) - app.use(populateDeviceId) - app.use(configRequiredRoutes, populateSettings) - app.use(bodyParser.json()) - app.use(filterOldRequests) - app.post('*', cacheAction) - - app.post('/pair', limiter, pair) - app.get('/ca', limiter, ca) - - app.get('/poll', authMiddleware, poll) - - app.post('/trade', authMiddleware, trade) - app.post('/send', authMiddleware, send) - app.post('/state', authMiddleware, stateChange) - app.post('/cash_out', authMiddleware, cashOut) - app.post('/dispense_ack', authMiddleware, dispenseAck) - - app.post('/event', authMiddleware, deviceEvent) - app.post('/verify_user', authMiddleware, verifyUser) - app.post('/verify_transaction', authMiddleware, verifyTx) - - app.post('/phone_code', authMiddleware, phoneCode) - app.post('/update_phone', authMiddleware, updatePhone) - app.get('/phone_tx', authMiddleware, fetchPhoneTx) - app.post('/register_redeem/:txId', authMiddleware, registerRedeem) - app.get('/await_dispense/:txId', authMiddleware, waitForDispense) - app.post('/dispense', authMiddleware, dispense) - - app.use('*', errorHandler) - - localApp.get('/pid', (req, res) => { - const deviceId = req.query.device_id - const pidRec = pids[deviceId] - res.json(pidRec) - }) - - localApp.post('/reboot', (req, res) => { - const pid = req.body.pid - const deviceId = req.body.deviceId - - if (!deviceId || !pid) { - return res.sendStatus(400) - } - - reboots[deviceId] = pid - res.sendStatus(200) - }) - - localApp.post('/dbChange', (req, res, next) => { - return settingsLoader.loadLatest() - .then(poller.reload) - .then(() => logger.info('Config reloaded')) - .catch(err => { - logger.error(err) - res.sendStatus(500) - }) - }) - - setInterval(pruneIdempotents, 60000) - - return app -} +}) function populateDeviceId (req, res, next) { const deviceId = ((typeof req.connection.getPeerCertificate === 'function' && @@ -449,3 +433,7 @@ function populateSettings (req, res, next) { .then(() => next()) .catch(next) } + +setInterval(pruneIdempotents, 60000) + +module.exports = {app, localApp} diff --git a/package.json b/package.json index aed98364..d3acd3b2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "node-hkdf-sync": "^1.0.0", "numeral": "^2.0.1", "pg": "^6.1.0", + "pg-native": "^1.10.0", "pg-promise": "^5.4.4", "pify": "^2.3.0", "pretty-ms": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 4e762539..255d9f3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,7 +192,7 @@ bignumber.js@^3.0.0, bignumber.js@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-3.0.1.tgz#807652d10e39de37e9e3497247edc798bb746f76" -bindings@^1.2.1: +bindings@1.2.1, bindings@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" @@ -1480,6 +1480,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libpq@^1.7.0: + version "1.8.5" + resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.8.5.tgz#9ef5bb509e1658908c19474088031db3dc7d4e48" + dependencies: + bindings "1.2.1" + nan "^2.3.0" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -1641,7 +1648,7 @@ ms@0.7.2, ms@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" -nan@^2.0.5, nan@^2.2.0: +nan@^2.0.5, nan@^2.2.0, nan@^2.3.0: version "2.4.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" @@ -1843,6 +1850,14 @@ pg-minify@0.4: version "0.4.1" resolved "https://registry.yarnpkg.com/pg-minify/-/pg-minify-0.4.1.tgz#a642c6bd256c7da833066590b1e414334a1f6e19" +pg-native@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/pg-native/-/pg-native-1.10.0.tgz#abe299214afa2be51db5f5104e14770c738230fd" + dependencies: + libpq "^1.7.0" + pg-types "1.6.0" + readable-stream "1.0.31" + pg-pool@1.*: version "1.6.0" resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.6.0.tgz#2e300199927b6d7db6be71e2e3435dddddf07b41" @@ -1869,6 +1884,10 @@ pg-types@1.*: postgres-date "~1.0.0" postgres-interval "~1.0.0" +pg-types@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.6.0.tgz#3872a0f199143025497f4ee2a65fdaf00d7ea8b3" + pg@5.1: version "5.1.0" resolved "https://registry.yarnpkg.com/pg/-/pg-5.1.0.tgz#073b9b36763ad8a5478dbb85effef45e739ba9d8" @@ -2061,6 +2080,15 @@ readable-stream@1.0.27-1: isarray "0.0.1" string_decoder "~0.10.x" +readable-stream@1.0.31: + version "1.0.31" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.31.tgz#8f2502e0bc9e3b0da1b94520aabb4e2603ecafae" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readable-stream@1.1.x: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"