342 lines
9.4 KiB
JavaScript
342 lines
9.4 KiB
JavaScript
const EventEmitter = require('events')
|
|
const qs = require('querystring')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const express = require('express')
|
|
const app = express()
|
|
const https = require('https')
|
|
const serveStatic = require('serve-static')
|
|
const cookieParser = require('cookie-parser')
|
|
const argv = require('minimist')(process.argv.slice(2))
|
|
const got = require('got')
|
|
const morgan = require('morgan')
|
|
const helmet = require('helmet')
|
|
// const WebSocket = require('ws')
|
|
const http = require('http')
|
|
// const SocketIo = require('socket.io')
|
|
const makeDir = require('make-dir')
|
|
const _ = require('lodash/fp')
|
|
|
|
const machineLoader = require('../machine-loader')
|
|
const T = require('../time')
|
|
const logger = require('../logger')
|
|
|
|
const accounts = require('./accounts')
|
|
const config = require('./config')
|
|
const login = require('./login')
|
|
const pairing = require('./pairing')
|
|
const server = require('./server')
|
|
const transactions = require('./transactions')
|
|
const customers = require('../customers')
|
|
const logs = require('../logs')
|
|
const funding = require('./funding')
|
|
const supportServer = require('./admin-support')
|
|
|
|
const NEVER = new Date(Date.now() + 100 * T.years)
|
|
const REAUTHENTICATE_INTERVAL = T.minute
|
|
|
|
const HOSTNAME = process.env.HOSTNAME
|
|
const KEY_PATH = process.env.KEY_PATH
|
|
const CERT_PATH = process.env.CERT_PATH
|
|
const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR
|
|
const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR
|
|
const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR
|
|
|
|
const devMode = argv.dev
|
|
|
|
const version = require('../../package.json').version
|
|
logger.info('Version: %s', version)
|
|
|
|
if (!HOSTNAME) {
|
|
logger.error('no hostname specified.')
|
|
process.exit(1)
|
|
}
|
|
|
|
module.exports = {run}
|
|
|
|
function dbNotify () {
|
|
return got.post('http://localhost:3030/dbChange')
|
|
.catch(e => logger.error('lamassu-server not responding'))
|
|
}
|
|
|
|
const skip = (req, res) => req.path === '/api/status/' && res.statusCode === 200
|
|
|
|
// Note: no rate limiting applied since that would allow an attacker to
|
|
// easily DDoS by just hitting the aggregate rate limit. We assume the
|
|
// attacker has unlimited unique IP addresses.
|
|
//
|
|
// The best we can do at the application level is to make the authentication
|
|
// lookup very fast. There will only be a few users at most, so it's not a problem
|
|
// to keep them in memory, but we need to update right after a new one is added.
|
|
// For now, we believe that probability of sustained DDoS by saturating our ability to
|
|
// fetch from the DB is pretty low.
|
|
|
|
app.use(morgan('dev', {skip}))
|
|
app.use(helmet({noCache: true}))
|
|
app.use(cookieParser())
|
|
app.use(register)
|
|
app.use(authenticate)
|
|
app.use(express.json())
|
|
|
|
app.get('/api/totem', (req, res) => {
|
|
const name = req.query.name
|
|
|
|
if (!name) return res.status(400).send('Name is required')
|
|
|
|
return pairing.totem(HOSTNAME, name)
|
|
.then(totem => res.send(totem))
|
|
})
|
|
|
|
app.get('/api/accounts', (req, res) => {
|
|
accounts.selectedAccounts()
|
|
.then(accounts => res.json({accounts: accounts}))
|
|
})
|
|
|
|
app.get('/api/account/:account', (req, res) => {
|
|
accounts.getAccount(req.params.account)
|
|
.then(account => res.json(account))
|
|
})
|
|
|
|
app.post('/api/account', (req, res) => {
|
|
return accounts.updateAccount(req.body)
|
|
.then(account => res.json(account))
|
|
.then(() => dbNotify())
|
|
})
|
|
|
|
app.get('/api/config/:config', (req, res, next) =>
|
|
config.fetchConfigGroup(req.params.config)
|
|
.then(c => res.json(c))
|
|
.catch(next))
|
|
|
|
app.post('/api/config', (req, res, next) => {
|
|
config.saveConfigGroup(req.body)
|
|
.then(c => res.json(c))
|
|
.then(() => dbNotify())
|
|
.catch(next)
|
|
})
|
|
|
|
app.get('/api/accounts/account/:account', (req, res) => {
|
|
accounts.getAccount(req.params.account)
|
|
.then(r => res.send(r))
|
|
})
|
|
|
|
app.get('/api/machines', (req, res) => {
|
|
machineLoader.getMachineNames()
|
|
.then(r => res.send({machines: r}))
|
|
})
|
|
|
|
app.post('/api/machines', (req, res) => {
|
|
machineLoader.setMachine(req.body)
|
|
.then(() => machineLoader.getMachineNames())
|
|
.then(r => res.send({machines: r}))
|
|
.then(() => dbNotify())
|
|
})
|
|
|
|
app.get('/api/funding', (req, res) => {
|
|
return funding.getFunding()
|
|
.then(r => res.json(r))
|
|
})
|
|
|
|
app.get('/api/funding/:cryptoCode', (req, res) => {
|
|
const cryptoCode = req.params.cryptoCode
|
|
|
|
return funding.getFunding(cryptoCode)
|
|
.then(r => res.json(r))
|
|
})
|
|
|
|
app.get('/api/status', (req, res, next) => {
|
|
return Promise.all([server.status(), config.validateCurrentConfig()])
|
|
.then(([serverStatus, invalidConfigGroups]) => res.send({
|
|
server: serverStatus,
|
|
invalidConfigGroups
|
|
}))
|
|
.catch(next)
|
|
})
|
|
|
|
app.get('/api/transactions', (req, res, next) => {
|
|
return transactions.batch()
|
|
.then(r => res.send({transactions: r}))
|
|
.catch(next)
|
|
})
|
|
|
|
app.get('/api/transaction/:id', (req, res, next) => {
|
|
return transactions.single(req.params.id)
|
|
.then(r => {
|
|
if (!r) return res.status(404).send({Error: 'Not found'})
|
|
return res.send(r)
|
|
})
|
|
})
|
|
|
|
app.patch('/api/transaction/:id', (req, res, next) => {
|
|
if (!req.query.cancel) return res.status(400).send({Error: 'Requires cancel'})
|
|
|
|
return transactions.cancel(req.params.id)
|
|
.then(r => {
|
|
return res.send(r)
|
|
})
|
|
.catch(() => res.status(404).send({Error: 'Not found'}))
|
|
})
|
|
|
|
app.get('/api/customers', (req, res, next) => {
|
|
return customers.batch()
|
|
.then(r => res.send({customers: r}))
|
|
.catch(next)
|
|
})
|
|
|
|
app.get('/api/customer/:id', (req, res, next) => {
|
|
return customers.getById(req.params.id)
|
|
.then(r => {
|
|
if (!r) return res.status(404).send({Error: 'Not found'})
|
|
return res.send(r)
|
|
})
|
|
})
|
|
|
|
app.get('/api/logs/:deviceId', (req, res, next) => {
|
|
return logs.getMachineLogs(req.params.deviceId)
|
|
.then(r => res.send(r))
|
|
.catch(next)
|
|
})
|
|
|
|
app.get('/api/logs', (req, res, next) => {
|
|
return machineLoader.getMachines()
|
|
.then(machines => {
|
|
const firstMachine = _.first(machines)
|
|
if (!firstMachine) return res.status(404).send({Error: 'No machines'})
|
|
return logs.getMachineLogs(firstMachine.deviceId)
|
|
.then(r => res.send(r))
|
|
})
|
|
.catch(next)
|
|
})
|
|
|
|
app.patch('/api/customer/:id', (req, res, next) => {
|
|
if (!req.params.id) return res.status(400).send({Error: 'Requires id'})
|
|
const token = req.token || req.cookies.token
|
|
return customers.update(req.params.id, req.query, token)
|
|
.then(r => res.send(r))
|
|
.catch(() => res.status(404).send({Error: 'Not found'}))
|
|
})
|
|
|
|
app.use((err, req, res, next) => {
|
|
logger.error(err)
|
|
|
|
return res.status(500).send(err.message)
|
|
})
|
|
|
|
const certOptions = {
|
|
key: fs.readFileSync(KEY_PATH),
|
|
cert: fs.readFileSync(CERT_PATH)
|
|
}
|
|
|
|
app.use(serveStatic(path.resolve(__dirname, 'public')))
|
|
|
|
if (!fs.existsSync(ID_PHOTO_CARD_DIR)) {
|
|
makeDir.sync(ID_PHOTO_CARD_DIR)
|
|
}
|
|
|
|
if (!fs.existsSync(FRONT_CAMERA_DIR)) {
|
|
makeDir.sync(FRONT_CAMERA_DIR)
|
|
}
|
|
|
|
if (!fs.existsSync(OPERATOR_DATA_DIR)) {
|
|
makeDir.sync(OPERATOR_DATA_DIR)
|
|
}
|
|
|
|
app.use('/id-card-photo', serveStatic(ID_PHOTO_CARD_DIR, {index: false}))
|
|
app.use('/front-camera-photo', serveStatic(FRONT_CAMERA_DIR, {index: false}))
|
|
app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, {index: false}))
|
|
|
|
function register (req, res, next) {
|
|
const otp = req.query.otp
|
|
|
|
if (!otp) return next()
|
|
|
|
return login.register(otp)
|
|
.then(r => {
|
|
if (r.expired) return res.status(401).send('OTP expired, generate new registration link')
|
|
|
|
// Maybe user is using old registration key, attempt to authenticate
|
|
if (!r.success) return next()
|
|
|
|
const cookieOpts = {
|
|
httpOnly: true,
|
|
secure: true,
|
|
domain: HOSTNAME,
|
|
sameSite: true,
|
|
expires: NEVER
|
|
}
|
|
|
|
const token = r.token
|
|
req.token = token
|
|
res.cookie('token', token, cookieOpts)
|
|
next()
|
|
})
|
|
}
|
|
|
|
function authenticate (req, res, next) {
|
|
const token = req.token || req.cookies.token
|
|
|
|
return login.authenticate(token)
|
|
.then(success => {
|
|
if (!success) return res.status(401).send('Authentication failed')
|
|
next()
|
|
})
|
|
}
|
|
|
|
process.on('unhandledRejection', err => {
|
|
logger.error(err.stack)
|
|
process.exit(1)
|
|
})
|
|
|
|
const socketServer = http.createServer()
|
|
const io = SocketIo(socketServer)
|
|
socketServer.listen(3060)
|
|
const socketEmitter = new EventEmitter()
|
|
|
|
io.on('connection', client => {
|
|
client.on('message', msg => socketEmitter.emit('message', msg))
|
|
})
|
|
|
|
const webServer = https.createServer(certOptions, app)
|
|
const wss = new WebSocket.Server({server: webServer})
|
|
|
|
function establishSocket (ws, token) {
|
|
return login.authenticate(token)
|
|
.then(success => {
|
|
if (!success) return ws.close(1008, 'Authentication error')
|
|
|
|
const listener = data => {
|
|
ws.send(JSON.stringify(data))
|
|
}
|
|
|
|
// Reauthenticate every once in a while, in case token expired
|
|
setInterval(() => {
|
|
return login.authenticate(token)
|
|
.then(success => {
|
|
if (!success) {
|
|
socketEmitter.removeListener('message', listener)
|
|
ws.close()
|
|
}
|
|
})
|
|
}, REAUTHENTICATE_INTERVAL)
|
|
|
|
socketEmitter.on('message', listener)
|
|
ws.send('Testing123')
|
|
})
|
|
}
|
|
|
|
wss.on('connection', ws => {
|
|
const token = qs.parse(ws.upgradeReq.headers.cookie).token
|
|
|
|
return establishSocket(ws, token)
|
|
})
|
|
|
|
function run () {
|
|
const serverPort = devMode ? 8072 : 443
|
|
const supportPort = 8071
|
|
|
|
const serverLog = `lamassu-admin-server listening on port ${serverPort}`
|
|
const supportLog = `lamassu-support-server listening on port ${supportPort}`
|
|
|
|
webServer.listen(serverPort, () => logger.info(serverLog))
|
|
supportServer.run(supportPort).then(logger.info(supportLog))
|
|
}
|