ioroutes: Implement better spam detection

http: Added new POST /update/:name support
other: Bunch of refactoring
This commit is contained in:
Jonatan Nilsson 2022-03-30 08:09:48 +00:00
parent 24bcea69a2
commit a68a73438c
6 changed files with 118 additions and 54 deletions

3
.gitignore vendored
View file

@ -104,6 +104,5 @@ dist
.tern-port .tern-port
db.json db.json
app/* hello-world
manage/*
public/main.js public/main.js

View file

@ -1,8 +1,6 @@
import defaults from '../defaults.mjs' import defaults from '../defaults.mjs'
import { formatLog } from './loghelper.mjs' import { formatLog } from './loghelper.mjs'
import { getStatus } from '../util.mjs' import { getApp, stopSpam } from '../util.mjs'
const stopSpam = {}
/* /*
* Event: 'core.restart' * Event: 'core.restart'
@ -27,12 +25,8 @@ export async function getlastlogs(ctx, data, cb) {
if (data.name === 'service-core') { if (data.name === 'service-core') {
return cb(ctx.core.log.ringbuffer.records.map(formatLog)) return cb(ctx.core.log.ringbuffer.records.map(formatLog))
} }
let app = ctx.core.applicationMap.get(data.name) let app = getApp(ctx, data.name, 'getlastlogs')
if (!app) { if (!app) return
ctx.log.warn('Invalid getlastlogs command for app ' + data.name)
ctx.log.event.warn('Invalid getlastlogs command for app ' + data.name)
return
}
cb(app.ctx.log.ringbuffer.records.map(formatLog)) cb(app.ctx.log.ringbuffer.records.map(formatLog))
} }
@ -61,26 +55,16 @@ export async function unlistenlogs(ctx, data) {
* Update specific software * Update specific software
*/ */
export async function update(ctx, data, cb) { export async function update(ctx, data, cb) {
let app = ctx.core.applicationMap.get(data.name) let app = getApp(ctx, data.name, 'update')
if (!app) { if (!app) return
ctx.log.warn('Invalid update command for app ' + data.name)
ctx.log.event.warn('Invalid update command for app ' + data.name)
return
}
let d = new Date() if (stopSpam(ctx, 'update', data.name)) return
if (stopSpam[app.name] && d - stopSpam[app.name] < 1000 * 60 * 5) {
ctx.log.warn('Update called too fast for app ' + data.name)
ctx.log.event.warn('Update called too fast for app ' + data.name)
return
}
stopSpam[app.name] = d
ctx.log.info('Checking for updates on app ' + data.name) ctx.log.info('Checking for updates on app ' + data.name)
app.update().then(function(res) { app.update().then(function(res) {
ctx.log.info(res, 'Update completed on app ' + data.name) ctx.log.info(res, 'Update completed on app ' + data.name)
}, function(err) { }, function(err) {
ctx.log.err(err, 'Error checking for updates on app ' + data.name) ctx.log.error(err, 'Error checking for updates on app ' + data.name)
}) })
} }
@ -90,27 +74,16 @@ export async function update(ctx, data, cb) {
* Start specific software * Start specific software
*/ */
export async function start(ctx, data, cb) { export async function start(ctx, data, cb) {
let app = ctx.core.applicationMap.get(data.name) let app = getApp(ctx, data.name, 'start')
if (!app) return
if (!app || (!app.config.scAllowStop && app.running)) {
ctx.log.warn('Invalid start command for app ' + data.name) if (stopSpam(ctx, 'start', data.name)) return
ctx.log.event.warn('Invalid start command for app ' + data.name)
return
}
let d = new Date()
if (app.running && stopSpam[app.name] && d - stopSpam[app.name] < 1000 * 60 * 5) {
ctx.log.warn('Update called too fast for app ' + data.name)
ctx.log.event.warn('Update called too fast for app ' + data.name)
return
}
stopSpam[app.name] = d
ctx.log.info('Checking for updates on app ' + data.name) ctx.log.info('Checking for updates on app ' + data.name)
ctx.core.runApplication(app).then(function(res) { ctx.core.runApplication(app).then(function(res) {
ctx.log.info('Successfully started ' + data.name + ' running ' + app.running) ctx.log.info('Successfully started ' + data.name + ' running ' + app.running)
}, function(err) { }, function(err) {
ctx.log.err(err, 'Error starting app ' + data.name) ctx.log.error(err, 'Error starting app ' + data.name)
}) })
} }
@ -125,12 +98,9 @@ export async function listentoapp(ctx, data) {
return return
} }
let app = ctx.core.applicationMap.get(data.name) let app = getApp(ctx, data.name, 'listentoapp')
if (!app) return
if (!app) {
ctx.log.warn(`listento called on non-existing app ${data.name}`)
return
}
ctx.socket.join('app.' + data.name) ctx.socket.join('app.' + data.name)
let version = ctx.db.get(ctx.db.data.core[app.name].versions, ctx.db.data.core[app.name].latestInstalled) let version = ctx.db.get(ctx.db.data.core[app.name].versions, ctx.db.data.core[app.name].latestInstalled)
ctx.socket.emit('app.updatelog', { ctx.socket.emit('app.updatelog', {
@ -149,13 +119,9 @@ export async function unlistentoapp(ctx, data) {
ctx.log.warn(`unlistento called with missing name`) ctx.log.warn(`unlistento called with missing name`)
return return
} }
let app = ctx.core.applicationMap.get(data.name) let app = getApp(ctx, data.name, 'unlistentoapp')
if (!app) return
if (!app) {
ctx.log.warn(`unlistento called on non-existing app ${data.name}`)
return
}
ctx.socket.leave('app.' + data.name) ctx.socket.leave('app.' + data.name)
} }

50
api/core/routes.mjs Normal file
View file

@ -0,0 +1,50 @@
import { HttpError } from 'flaska'
import { getApp, stopSpam } from '../util.mjs'
/*
* POST: /update/:name
*
*/
export async function updateApp(ctx) {
let app = getApp(ctx, ctx.params.name, 'POST.update')
if (!app) {
ctx.status = 404
return ctx.body = {
status: 404,
message: `Application ${ctx.params.name} was not found`
}
}
let t = stopSpam(ctx, 'update', ctx.params.name)
if (t) {
let d2 = new Date()
let timeLeft =
Math.round((new Date(t).setMinutes(t.getMinutes() + 1) - d2) / 1000)
ctx.headers['Retry-After'] = timeLeft
ctx.status = 503
return ctx.body = {
status: 503,
message: `Update request too fast for ${ctx.params.name}. Try again in ${timeLeft} seconds.`
}
}
return app.update()
.then(function(res) {
if (res) {
ctx.body = res
} else {
ctx.body = {
message: 'No update was found',
}
}
}, function(err) {
ctx.log.error(err)
ctx.status = 500
return ctx.body = {
status: 500,
message: `Application ${ctx.params.name} failed to update: ${err.message}`,
}
})
}

View file

@ -3,6 +3,7 @@ import fs from 'fs/promises'
import socket from 'socket.io-serveronly' import socket from 'socket.io-serveronly'
import { Flaska, FileResponse } from 'flaska' import { Flaska, FileResponse } from 'flaska'
import coremonitor from './core/coremonitor.mjs' import coremonitor from './core/coremonitor.mjs'
import * as routes from './core/routes.mjs'
import onConnection from './routerio.mjs' import onConnection from './routerio.mjs'
@ -15,6 +16,7 @@ export function run(http, port, orgCtx) {
}, http) }, http)
flaska.before(function(ctx) { flaska.before(function(ctx) {
ctx.core = orgCtx.core
ctx.state.started = new Date().getTime() ctx.state.started = new Date().getTime()
}) })
@ -47,6 +49,15 @@ export function run(http, port, orgCtx) {
}) })
flaska.on404(function(ctx) { flaska.on404(function(ctx) {
if (ctx.method !== 'GET' && ctx.method !== 'HEAD') {
ctx.status = 404
ctx.body = {
status: 404,
message: 'Not Found',
}
return
}
let file = path.resolve(path.join(staticRoot, ctx.url === '/' ? 'index.html' : ctx.url)) let file = path.resolve(path.join(staticRoot, ctx.url === '/' ? 'index.html' : ctx.url))
if (!file.startsWith(staticRoot)) { if (!file.startsWith(staticRoot)) {
ctx.status = 404 ctx.status = 404
@ -64,6 +75,8 @@ export function run(http, port, orgCtx) {
}) })
}) })
flaska.post('/update/:name', routes.updateApp)
return flaska.listenAsync(port).then(function() { return flaska.listenAsync(port).then(function() {
orgCtx.log.info('Server is listening on port ' + port) orgCtx.log.info('Server is listening on port ' + port)

View file

@ -28,6 +28,42 @@ export function getConfig(ctx) {
return defaults(ctx.db.config, merge) return defaults(ctx.db.config, merge)
} }
export function getApp(ctx, name, action) {
let app = ctx.core.applicationMap.get(name)
if (!app) {
ctx.log.warn(`Invalid action ${action} on non-existing app ${name}`)
ctx.log.event.warn(`Invalid action ${action} on non-existing app ${name}`)
return null
}
if (action === 'start' && !app.config.scAllowStop && app.running) {
ctx.log.warn(`Invalid action ${action} on existing app ${name}`)
ctx.log.event.warn(`Invalid action ${action} on existing app ${name}`)
return null
}
return app
}
const lastAction = {
}
export function stopSpam(ctx, action, name) {
let key = name + '.' + action
var d = new Date()
if (!lastAction[key]) {
lastAction[key] = d
return false
}
if (d - lastAction[key] < 1000 * 60 * 1) {
ctx.log.warn(`${action} called too fast on ${name}`)
ctx.log.event.warn(`${action} called too fast on ${name}`)
return lastAction[key]
}
lastAction[key] = d
return false
}
export function getStatus(ctx) { export function getStatus(ctx) {
let status = {} let status = {}
for (let app of ctx.core.applications) { for (let app of ctx.core.applications) {

View file

@ -1,6 +1,6 @@
{ {
"name": "sc-manager", "name": "sc-manager",
"version": "1.0.0", "version": "2.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {