Skip to content

Commit

Permalink
feat(functions): use isolated-function (#591)
Browse files Browse the repository at this point in the history
* chore(functions): use isolated-function

It drops vm2 which is considered unsecured in favour of isolated-function

* refactor: only handle browser errors

* refactor: drop unnecessary logic
  • Loading branch information
Kikobeats authored Sep 12, 2024
1 parent 6e3fb40 commit de8dad0
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 336 deletions.
7 changes: 6 additions & 1 deletion packages/errors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ const debug = require('debug-logfmt')('browserless:error')
const { serializeError } = require('serialize-error')
const whoops = require('whoops')

const createErrorFactory = opts => whoops('BrowserlessError', opts)
const ERROR_NAME = 'BrowserlessError'

const createErrorFactory = opts => whoops(ERROR_NAME, opts)

const markAsProcessed = error => {
Object.defineProperty(error, '__parsed', {
Expand Down Expand Up @@ -80,4 +82,7 @@ browserlessError.ensureError = rawError => {
return require('ensure-error')(error)
}

const isBrowserlessError = error => error.name === ERROR_NAME

module.exports = browserlessError
module.exports.isBrowserlessError = isBrowserlessError
10 changes: 2 additions & 8 deletions packages/function/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,9 @@
"serverless"
],
"dependencies": {
"@browserless/errors": "^10.5.2",
"debug-logfmt": "~1.2.1",
"deepmerge": "~4.3.1",
"p-reflect": "~2.1.0",
"isolated-function": "~0.1.4",
"p-retry": "~4.6.1",
"p-timeout": "~4.1.0",
"require-one-of": "~1.0.19",
"serialize-error": "~8.1.0",
"vm2": "~3.9.19"
"require-one-of": "~1.0.19"
},
"devDependencies": {
"@browserless/test": "^10.5.2",
Expand Down
53 changes: 12 additions & 41 deletions packages/function/src/function.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,22 @@
'use strict'

const path = require('path')

const createVm = require('./vm')

const scriptPath = path.resolve(__dirname, 'function.js')
const isolatedFunction = require('isolated-function')

const createFn = code => `
async ({ url, gotoOpts, browserWSEndpoint, ...opts }) => {
const { serializeError } = require('serialize-error')
const getBrowserless = require('browserless')
const browserless = await getBrowserless({ mode: 'connect', browserWSEndpoint }).createContext()
const fnWrapper = fn => (page, response) => {
if (!response && opts.response) {
const { status, statusText, headers, html } = opts.response
response = {
ok: () => status === 0 || (status >= 200 && opts.status <= 299),
fromCache: () => false,
fromServiceWorker: () => false,
url: () => url,
text: () => html,
statusText: () => statusText,
json: () => JSON.parse(html),
headers: () => headers,
status: () => status
}
}
return fn({ ...opts, page, response, url })
}
const browserFn = browserless.evaluate(fnWrapper(${code}), gotoOpts)
async (url, browserWSEndpoint, opts) => {
const puppeteer = require('@cloudflare/puppeteer')
const browser = await puppeteer.connect({ browserWSEndpoint })
const page = (await browser.pages())[1]
try {
const value = await browserFn(url)
return { isFulfilled: true, isRejected: false, value }
} catch (error) {
return { isFulfilled: false, isRejected: true, reason: serializeError(error) }
return await (${code})({ page, ...opts })
} finally {
await browserless.destroyContext()
await browserless.browser().then(browser => browser.disconnect())
await browser.disconnect()
}
}`

module.exports = ({ url, code, vmOpts, gotoOpts, browserWSEndpoint, ...opts }) => {
const vm = createVm(vmOpts)
const fn = createFn(code)
const run = vm(fn, scriptPath)
return run({ url, gotoOpts, browserWSEndpoint, ...opts })
module.exports = async ({ url, code, vmOpts, browserWSEndpoint, ...opts }) => {
const [fn, teardown] = isolatedFunction(createFn(code), { ...vmOpts, throwError: false })
const result = await fn(url, browserWSEndpoint, opts)
await teardown()
return result
}
85 changes: 33 additions & 52 deletions packages/function/src/index.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,41 @@
'use strict'

const { ensureError, browserTimeout } = require('@browserless/errors')
const debug = require('debug-logfmt')('browserless:function')
const { isBrowserlessError, ensureError } = require('@browserless/errors')
const requireOneOf = require('require-one-of')
const pTimeout = require('p-timeout')
const pRetry = require('p-retry')

const { AbortError } = pRetry

const runFunction = require('./function')

const stringify = fn => fn.toString().trim().replace(/;$/, '')

module.exports = (
fn,
{ getBrowserless = requireOneOf(['browserless']), retry = 2, timeout = 30000, ...opts } = {}
) => {
return async (url, fnOpts = {}) => {
const browserlessPromise = getBrowserless()
let isRejected = false

async function run () {
const browserless = await browserlessPromise
const browser = await browserless.browser()
const browserWSEndpoint = browser.wsEndpoint()

const { value, reason, isFulfilled } = await runFunction({
url,
code: stringify(fn),
browserWSEndpoint,
...opts,
...fnOpts
})

if (isFulfilled) return value
throw ensureError(reason)
module.exports =
(
fn,
{
getBrowserless = requireOneOf(['browserless']),
retry = 2,
timeout = 30000,
gotoOpts,
...opts
} = {}
) =>
async (url, fnOpts = {}) => {
const browserlessPromise = getBrowserless()
const browser = await browserlessPromise
const browserless = await browser.createContext()

return browserless.withPage((page, goto) => async () => {
const { device } = await goto(page, { url, ...gotoOpts })
const result = await runFunction({
url,
code: stringify(fn),
browserWSEndpoint: (await browserless.browser()).wsEndpoint(),
device,
...opts,
...fnOpts
})

if (result.isFulfilled) return result
const error = ensureError(result.value)
if (isBrowserlessError(error)) throw error
return result
})()
}

const task = () =>
pRetry(run, {
retries: retry,
onFailedAttempt: async error => {
if (error.name === 'AbortError') throw error
if (isRejected) throw new AbortError()
await (await browserlessPromise).respawn()
const { message, attemptNumber, retriesLeft } = error
debug('retry', { attemptNumber, retriesLeft, message })
}
})

// main
const result = await pTimeout(task(), timeout, () => {
isRejected = true
throw browserTimeout({ timeout })
})

return result
}
}
34 changes: 0 additions & 34 deletions packages/function/src/vm.js

This file was deleted.

Loading

0 comments on commit de8dad0

Please sign in to comment.