ioroutes: Implement better spam detection
http: Added new POST /update/:name support other: Bunch of refactoring
This commit is contained in:
parent
24bcea69a2
commit
a68a73438c
6 changed files with 118 additions and 54 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -104,6 +104,5 @@ dist
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
db.json
|
db.json
|
||||||
app/*
|
hello-world
|
||||||
manage/*
|
|
||||||
public/main.js
|
public/main.js
|
||||||
|
|
|
@ -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
50
api/core/routes.mjs
Normal 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}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
36
api/util.mjs
36
api/util.mjs
|
@ -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) {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
Loading…
Reference in a new issue