1
0
Fork 0
mirror of synced 2024-10-03 02:27:06 +13:00

Implement per-request JS execution limiting.

This commit is contained in:
Sam Rose 2023-12-18 15:29:56 +00:00
parent 22c0e81308
commit bd324f3225
No known key found for this signature in database
11 changed files with 950 additions and 100 deletions

View file

@ -335,3 +335,11 @@ export function isScim(): boolean {
const scimCall = context?.isScim
return !!scimCall
}
export function getCurrentContext(): ContextMap | undefined {
try {
return Context.get()
} catch (e) {
return undefined
}
}

View file

@ -1,4 +1,5 @@
import { IdentityContext } from "@budibase/types"
import { ExecutionTimeTracker } from "../timers"
// keep this out of Budibase types, don't want to expose context info
export type ContextMap = {
@ -9,4 +10,5 @@ export type ContextMap = {
isScim?: boolean
automationId?: string
isMigrating?: boolean
jsExecutionTracker?: ExecutionTimeTracker
}

View file

@ -20,3 +20,35 @@ export function cleanup() {
}
intervals = []
}
export class ExecutionTimeTracker {
static withLimit(limitMs: number) {
return new ExecutionTimeTracker(limitMs)
}
constructor(private limitMs: number) {}
private totalTimeMs = 0
track<T>(f: () => T): T {
const [startSeconds, startNanoseconds] = process.hrtime()
const startMs = startSeconds * 1000 + startNanoseconds / 1e6
try {
return f()
} finally {
const [endSeconds, endNanoseconds] = process.hrtime()
const endMs = endSeconds * 1000 + endNanoseconds / 1e6
this.increment(endMs - startMs)
}
}
private increment(byMs: number) {
this.totalTimeMs += byMs
if (this.totalTimeMs > this.limitMs) {
throw new Error(
`Execution time limit of ${this.limitMs}ms exceeded: ${this.totalTimeMs}ms`
)
}
}
}

View file

@ -49,4 +49,8 @@ export class Duration {
static fromDays(duration: number) {
return Duration.from(DurationType.DAYS, duration)
}
static fromMiliseconds(duration: number) {
return Duration.from(DurationType.MILLISECONDS, duration)
}
}

View file

@ -70,6 +70,11 @@ const environment = {
SELF_HOSTED: process.env.SELF_HOSTED,
HTTP_MB_LIMIT: process.env.HTTP_MB_LIMIT,
FORKED_PROCESS_NAME: process.env.FORKED_PROCESS_NAME || "main",
JS_PER_EXECUTION_TIME_LIMIT_MS:
parseIntSafe(process.env.JS_PER_EXECUTION_TIME_LIMIT_MS) || 1000,
JS_PER_REQUEST_TIME_LIMIT_MS: parseIntSafe(
process.env.JS_PER_REQUEST_TIME_LIMIT_MS
),
// old
CLIENT_ID: process.env.CLIENT_ID,
_set(key: string, value: any) {

View file

@ -0,0 +1,32 @@
import vm from "vm"
import env from "./environment"
import { setJSRunner } from "@budibase/string-templates"
import { context, timers } from "@budibase/backend-core"
type TrackerFn = <T>(f: () => T) => T
export function init() {
setJSRunner((js: string, ctx: vm.Context) => {
const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS
const bbCtx = context.getCurrentContext()
let track: TrackerFn = f => f()
if (perRequestLimit && bbCtx && !bbCtx.jsExecutionTracker) {
bbCtx.jsExecutionTracker =
timers.ExecutionTimeTracker.withLimit(perRequestLimit)
track = bbCtx.jsExecutionTracker.track
}
ctx = {
...ctx,
alert: undefined,
setInterval: undefined,
setTimeout: undefined,
}
vm.createContext(ctx)
return track(() =>
vm.runInNewContext(js, ctx, {
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
})
)
})
}

View file

@ -23,6 +23,7 @@ import { automationsEnabled, printFeatures } from "./features"
import Koa from "koa"
import { Server } from "http"
import { AddressInfo } from "net"
import * as javascript from "./javascript"
let STARTUP_RAN = false
@ -152,4 +153,6 @@ export async function startup(app?: Koa, server?: Server) {
}
})
}
javascript.init()
}

View file

@ -18,6 +18,7 @@ module.exports.doesContainString = templates.doesContainString
module.exports.disableEscaping = templates.disableEscaping
module.exports.findHBSBlocks = templates.findHBSBlocks
module.exports.convertToJS = templates.convertToJS
module.exports.setJSRunner = templates.setJSRunner
module.exports.FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX
if (!process.env.NO_JS) {

View file

@ -9,6 +9,7 @@ const {
findDoubleHbsInstances,
} = require("./utilities")
const { convertHBSBlock } = require("./conversion")
const javascript = require("./helpers/javascript")
const hbsInstance = handlebars.create()
registerAll(hbsInstance)
@ -362,6 +363,8 @@ module.exports.doesContainString = (template, string) => {
return exports.doesContainStrings(template, [string])
}
module.exports.setJSRunner = javascript.setJSRunner
module.exports.convertToJS = hbs => {
const blocks = exports.findHBSBlocks(hbs)
let js = "return `",

View file

@ -1,6 +1,5 @@
import vm from "vm"
import templates from "./index.js"
import { setJSRunner } from "./helpers/javascript"
/**
* ES6 entrypoint for rollup
@ -20,6 +19,7 @@ export const doesContainString = templates.doesContainString
export const disableEscaping = templates.disableEscaping
export const findHBSBlocks = templates.findHBSBlocks
export const convertToJS = templates.convertToJS
export const setJSRunner = templates.setJSRunner
export const FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX
if (process && !process.env.NO_JS) {

958
yarn.lock

File diff suppressed because it is too large Load diff