From a68a73438c9c677e4813dcd0a38643179f169754 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Wed, 30 Mar 2022 08:09:48 +0000 Subject: [PATCH] ioroutes: Implement better spam detection http: Added new POST /update/:name support other: Bunch of refactoring --- .gitignore | 3 +- api/core/ioroutes.mjs | 68 +++++++++++-------------------------------- api/core/routes.mjs | 50 +++++++++++++++++++++++++++++++ api/server.mjs | 13 +++++++++ api/util.mjs | 36 +++++++++++++++++++++++ package.json | 2 +- 6 files changed, 118 insertions(+), 54 deletions(-) create mode 100644 api/core/routes.mjs diff --git a/.gitignore b/.gitignore index 1d20b2f..4dfcf31 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,5 @@ dist .tern-port db.json -app/* -manage/* +hello-world public/main.js diff --git a/api/core/ioroutes.mjs b/api/core/ioroutes.mjs index 0e01ddf..5b8eb69 100644 --- a/api/core/ioroutes.mjs +++ b/api/core/ioroutes.mjs @@ -1,8 +1,6 @@ import defaults from '../defaults.mjs' import { formatLog } from './loghelper.mjs' -import { getStatus } from '../util.mjs' - -const stopSpam = {} +import { getApp, stopSpam } from '../util.mjs' /* * Event: 'core.restart' @@ -27,12 +25,8 @@ export async function getlastlogs(ctx, data, cb) { if (data.name === 'service-core') { return cb(ctx.core.log.ringbuffer.records.map(formatLog)) } - let app = ctx.core.applicationMap.get(data.name) - if (!app) { - ctx.log.warn('Invalid getlastlogs command for app ' + data.name) - ctx.log.event.warn('Invalid getlastlogs command for app ' + data.name) - return - } + let app = getApp(ctx, data.name, 'getlastlogs') + if (!app) return cb(app.ctx.log.ringbuffer.records.map(formatLog)) } @@ -61,26 +55,16 @@ export async function unlistenlogs(ctx, data) { * Update specific software */ export async function update(ctx, data, cb) { - let app = ctx.core.applicationMap.get(data.name) - if (!app) { - ctx.log.warn('Invalid update command for app ' + data.name) - ctx.log.event.warn('Invalid update command for app ' + data.name) - return - } + let app = getApp(ctx, data.name, 'update') + if (!app) return - let d = new Date() - 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 + if (stopSpam(ctx, 'update', data.name)) return ctx.log.info('Checking for updates on app ' + data.name) app.update().then(function(res) { ctx.log.info(res, 'Update completed on app ' + data.name) }, 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 */ export async function start(ctx, data, cb) { - let app = ctx.core.applicationMap.get(data.name) - - if (!app || (!app.config.scAllowStop && app.running)) { - ctx.log.warn('Invalid start command for app ' + data.name) - 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 + let app = getApp(ctx, data.name, 'start') + if (!app) return + + if (stopSpam(ctx, 'start', data.name)) return ctx.log.info('Checking for updates on app ' + data.name) ctx.core.runApplication(app).then(function(res) { ctx.log.info('Successfully started ' + data.name + ' running ' + app.running) }, 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 } - 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) let version = ctx.db.get(ctx.db.data.core[app.name].versions, ctx.db.data.core[app.name].latestInstalled) ctx.socket.emit('app.updatelog', { @@ -149,13 +119,9 @@ export async function unlistentoapp(ctx, data) { ctx.log.warn(`unlistento called with missing name`) return } - - let app = ctx.core.applicationMap.get(data.name) - - if (!app) { - ctx.log.warn(`unlistento called on non-existing app ${data.name}`) - return - } + + let app = getApp(ctx, data.name, 'unlistentoapp') + if (!app) return ctx.socket.leave('app.' + data.name) } diff --git a/api/core/routes.mjs b/api/core/routes.mjs new file mode 100644 index 0000000..7c65c31 --- /dev/null +++ b/api/core/routes.mjs @@ -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}`, + } + }) +} \ No newline at end of file diff --git a/api/server.mjs b/api/server.mjs index 1eb8449..dafc747 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -3,6 +3,7 @@ import fs from 'fs/promises' import socket from 'socket.io-serveronly' import { Flaska, FileResponse } from 'flaska' import coremonitor from './core/coremonitor.mjs' +import * as routes from './core/routes.mjs' import onConnection from './routerio.mjs' @@ -15,6 +16,7 @@ export function run(http, port, orgCtx) { }, http) flaska.before(function(ctx) { + ctx.core = orgCtx.core ctx.state.started = new Date().getTime() }) @@ -47,6 +49,15 @@ export function run(http, port, orgCtx) { }) 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)) if (!file.startsWith(staticRoot)) { 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() { orgCtx.log.info('Server is listening on port ' + port) diff --git a/api/util.mjs b/api/util.mjs index 72d3bdf..2f55f35 100644 --- a/api/util.mjs +++ b/api/util.mjs @@ -28,6 +28,42 @@ export function getConfig(ctx) { 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) { let status = {} for (let app of ctx.core.applications) { diff --git a/package.json b/package.json index f502384..38aa4da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sc-manager", - "version": "1.0.0", + "version": "2.0.0", "description": "", "main": "index.js", "scripts": {