Merge branch 'dev' into feat/lam-1291/stress-testing

* dev: (85 commits)
  chore: console.log debug leftovers
  fix: third level navigation links
  fix: show subheader on refresh
  fix: machines/:id routing
  fix: customer route
  chore: update wallet nodes
  feat: shorten long addresses in funding page
  feat: shorten long addresses
  refactor: support copied text different from presented text
  chore: udpate react, downshift and routing
  refactor: use Wizard component on first route
  fix: autocomplete component rendering
  feat: skip2fa option on .env
  fix: drop contraint before dropping index
  chore: stop using alias imports
  fix: re-instate urlResolver
  chore: server code formatting
  chore: reformat code
  chore: adding eslint and prettier config
  chore: typo
  ...
This commit is contained in:
siiky 2025-05-20 11:57:32 +01:00
commit e10493abc6
1398 changed files with 60329 additions and 157527 deletions

View file

@ -0,0 +1,15 @@
const addRWBytes = () => (req, res, next) => {
const handle = () => {
res.removeListener('finish', handle)
res.removeListener('close', handle)
res.bytesRead = req.connection.bytesRead
res.bytesWritten = req.connection.bytesWritten
}
res.on('finish', handle)
res.on('close', handle)
next()
}
module.exports = addRWBytes

View file

@ -0,0 +1,22 @@
const pairing = require('../pairing')
const logger = require('../logger')
const authorize = function (req, res, next) {
return pairing
.isPaired(req.deviceId)
.then(deviceName => {
if (deviceName) {
req.deviceName = deviceName
return next()
}
logger.error(`Device ${req.deviceId} not found`)
return res.status(403).json({ error: 'Forbidden' })
})
.catch(error => {
logger.error(error)
return next()
})
}
module.exports = authorize

View file

@ -0,0 +1,16 @@
const pairing = require('../pairing')
const logger = require('../logger')
function ca(req, res) {
const token = req.query.token
return pairing
.authorizeCaDownload(token)
.then(ca => res.json({ ca }))
.catch(error => {
logger.error(error.message)
return res.status(403).json({ error: 'forbidden' })
})
}
module.exports = ca

View file

@ -0,0 +1,13 @@
const logger = require('../logger')
function errorHandler(err, req, res) {
const statusCode = err.name === 'HTTPError' ? err.code || 500 : 500
const json = { error: err.message }
if (statusCode >= 400) logger.error(err)
return res.status(statusCode).json(json)
}
module.exports = errorHandler

View file

@ -0,0 +1,31 @@
const state = require('./state')
const logger = require('../logger')
const CLOCK_SKEW = 60 * 1000
const REQUEST_TTL = 3 * 60 * 1000
const THROTTLE_CLOCK_SKEW = 60 * 1000
function filterOldRequests(req, res, next) {
const deviceTime = req.deviceTime
const deviceId = req.deviceId
const timestamp = Date.now()
const delta = timestamp - Date.parse(deviceTime)
const shouldTrigger =
!state.canLogClockSkewMap[deviceId] ||
timestamp - state.canLogClockSkewMap[deviceId] >= THROTTLE_CLOCK_SKEW
if (delta > CLOCK_SKEW && shouldTrigger) {
state.canLogClockSkewMap[deviceId] = timestamp
logger.error(
'Clock skew with lamassu-machine[%s] too high [%ss], adjust lamassu-machine clock',
req.deviceName,
(delta / 1000).toFixed(2),
)
}
if (delta > REQUEST_TTL) return res.status(408).json({ error: 'stale' })
next()
}
module.exports = filterOldRequests

View file

@ -0,0 +1,15 @@
const { getOperatorId } = require('../operator')
function findOperatorId(req, res, next) {
return getOperatorId('middleware')
.then(operatorId => {
res.locals.operatorId = operatorId
return next()
})
.catch(e => {
console.error('Error while computing operator id\n' + e)
next(e)
})
}
module.exports = findOperatorId

View file

@ -0,0 +1,29 @@
const crypto = require('crypto')
const IS_STRESS_TESTING = process.env.LAMASSU_STRESS_TESTING === 'YES'
function sha256(buf) {
if (!buf) return null
const hash = crypto.createHash('sha256')
hash.update(buf)
return hash.digest('hex').toString('hex')
}
const populateDeviceId = function (req, res, next) {
const peerCert = req.socket.getPeerCertificate
? req.socket.getPeerCertificate()
: null
let deviceId = peerCert?.raw ? sha256(peerCert.raw) : null
if (!deviceId && IS_STRESS_TESTING) deviceId = req.headers.device_id
if (!deviceId)
return res.status(500).json({ error: 'Unable to find certificate' })
req.deviceId = deviceId
req.deviceTime = req.get('date')
next()
}
module.exports = populateDeviceId

View file

@ -0,0 +1,147 @@
const db = require('../db')
const state = require('./state')
const newSettingsLoader = require('../new-settings-loader')
const logger = require('../logger')
db.connect({ direct: true })
.then(sco => {
sco.client.on('notification', data => {
const parsedData = JSON.parse(data.payload)
return reload(parsedData.operatorId)
})
return sco.none('LISTEN $1:name', 'reload')
})
.catch(console.error)
db.connect({ direct: true })
.then(sco => {
sco.client.on('notification', data => {
const parsedData = JSON.parse(data.payload)
return machineAction(parsedData.action, parsedData.value)
})
return sco.none('LISTEN $1:name', 'machineAction')
})
.catch(console.error)
function machineAction(type, value) {
const deviceId = value.deviceId
const operatorId = value.operatorId
const pid = state.pids?.[operatorId]?.[deviceId]?.pid
switch (type) {
case 'reboot':
logger.debug(
`Rebooting machine '${deviceId}' from operator ${operatorId}`,
)
state.reboots[operatorId] = { [deviceId]: pid }
break
case 'shutdown':
logger.debug(
`Shutting down machine '${deviceId}' from operator ${operatorId}`,
)
state.shutdowns[operatorId] = { [deviceId]: pid }
break
case 'restartServices':
logger.debug(
`Restarting services of machine '${deviceId}' from operator ${operatorId}`,
)
state.restartServicesMap[operatorId] = { [deviceId]: pid }
break
case 'emptyUnit':
logger.debug(
`Emptying units from machine '${deviceId}' from operator ${operatorId}`,
)
state.emptyUnit[operatorId] = { [deviceId]: pid }
break
case 'refillUnit':
logger.debug(
`Refilling recyclers from machine '${deviceId}' from operator ${operatorId}`,
)
state.refillUnit[operatorId] = { [deviceId]: pid }
break
case 'diagnostics':
logger.debug(
`Running diagnostics on machine '${deviceId}' from operator ${operatorId}`,
)
state.diagnostics[operatorId] = { [deviceId]: pid }
break
default:
break
}
}
function reload(operatorId) {
state.needsSettingsReload[operatorId] = true
}
const populateSettings = function (req, res, next) {
const { needsSettingsReload, settingsCache } = state
const operatorId = res.locals.operatorId
const versionId = req.headers['config-version']
if (versionId !== state.oldVersionId) {
state.oldVersionId = versionId
}
try {
// Priority of configs to retrieve
// 1. Machine is in the middle of a transaction and has the config-version header set, fetch that config from cache or database, depending on whether it exists in cache
// 2. The operator settings changed, so we must update the cache
// 3. There's a cached config, send the cached value
// 4. There's no cached config, cache and send the latest config
if (versionId) {
const cachedVersionedSettings = settingsCache.get(
`${operatorId}-v${versionId}`,
)
if (!cachedVersionedSettings) {
logger.debug('Fetching a specific config version cached value')
return newSettingsLoader
.load(versionId)
.then(settings => {
settingsCache.set(`${operatorId}-v${versionId}`, settings)
req.settings = settings
})
.then(() => next())
.catch(next)
}
logger.debug('Fetching a cached specific config version')
req.settings = cachedVersionedSettings
return next()
}
const operatorSettings = settingsCache.get(`${operatorId}-latest`)
if (!!needsSettingsReload[operatorId] || !operatorSettings) {
needsSettingsReload[operatorId]
? logger.debug(
'Fetching and caching a new latest config value, as a reload was requested',
)
: logger.debug(
"Fetching the latest config version because there's no cached value",
)
return newSettingsLoader
.loadLatest()
.then(settings => {
const versionId = settings.version
settingsCache.set(`${operatorId}-latest`, settings)
settingsCache.set(`${operatorId}-v${versionId}`, settings)
if (needsSettingsReload[operatorId])
delete needsSettingsReload[operatorId]
req.settings = settings
})
.then(() => next())
.catch(next)
}
logger.debug('Fetching the latest config value from cache')
req.settings = operatorSettings
return next()
} catch (e) {
logger.error(e)
}
}
module.exports = populateSettings

View file

@ -0,0 +1,7 @@
const plugins = require('../plugins')
module.exports = (req, res, next) =>
plugins(req.settings, req.deviceId)
.recordPing(req.deviceTime, req.query.version, req.query.model)
.then(() => next())
.catch(() => next())

View file

@ -0,0 +1,35 @@
const semver = require('semver')
const version = require('../../package.json').version
const logger = require('../logger')
const rejectIncompatibleMachines = function (req, res, next) {
const machineVersion = req.query.version
const deviceId = req.deviceId
if (!machineVersion) return next()
const serverMajor = semver.major(version)
const machineMajor = semver.major(machineVersion)
if (serverMajor - machineMajor > 1) {
logger.error(
`Machine version too old: ${machineVersion} deviceId: ${deviceId}`,
)
return res.status(400).json({
error: 'Machine version too old',
})
}
if (serverMajor < machineMajor) {
logger.error(
`Machine version too new: ${machineVersion} deviceId: ${deviceId}`,
)
return res.status(400).json({
error: 'Machine version too new',
})
}
next()
}
module.exports = rejectIncompatibleMachines

View file

@ -0,0 +1,23 @@
const NodeCache = require('node-cache')
const SETTINGS_CACHE_REFRESH = 3600
module.exports = (function () {
return {
oldVersionId: 'unset',
needsSettingsReload: {},
settingsCache: new NodeCache({
stdTTL: SETTINGS_CACHE_REFRESH,
checkperiod: SETTINGS_CACHE_REFRESH, // Clear cache every hour
}),
canLogClockSkewMap: {},
canGetLastSeenMap: {},
pids: {},
reboots: {},
shutdowns: {},
restartServicesMap: {},
emptyUnit: {},
refillUnit: {},
diagnostics: {},
mnemonic: null,
}
})()