@applitools/execution-grid-tunnel
Version:
Allows user to run tests with exection-grid and navigate to private hosts and ips
145 lines (134 loc) • 4.27 kB
JavaScript
const fs = require('fs').promises
const fetch = require('node-fetch')
const {PacError} = require('./pac-errors')
/**
* Load and compile a PAC source ONCE at startup.
*
* Resolution priority: pacFile > pacScript > pacUrl > null (no PAC loaded).
*
* When all three are unset the function returns `null` synchronously
* (well, async-returns null) without booting QuickJS. That keeps the
* fast path for existing customers zero-cost.
*
* Returns a compiled `findProxyForURL(url, host)` function on success.
* Throws `PacError` on any startup failure (load, fetch, compile).
*
* The compiled function is bytecode — each call just executes it,
* no re-parsing. See AD-14128 comments for the contract.
*/
const PAC_URL_TIMEOUT_MS = 10000
async function loadPacResolver({pacFile, pacScript, pacUrl, logger}) {
// Source resolution (priority order). `resolveSource` throws a typed
// PacError on file-read / URL-fetch failure; we log + re-throw so callers
// see the same shape regardless of which phase failed.
let sourceSpec
try {
sourceSpec = await resolveSource({pacFile, pacScript, pacUrl})
} catch (err) {
logger?.error({
action: 'pac-load-failed',
code: err.code,
sourceKind: err.sourceKind,
source: err.source,
error: err.cause,
})
throw err
}
if (!sourceSpec) return null
const {sourceKind, source, content} = sourceSpec
const sourceLength = content.length
// Boot QuickJS + compile via pac-resolver. Lazy-imported so the all-null
// fast path doesn't pay the WASM-init cost. Dynamic `import()` is required
// because pac-resolver and quickjs-wasi are ESM-only (`"type": "module"`)
// and this package is CommonJS.
const compileStart = Date.now()
let findProxyForURL
try {
const {QuickJS} = await import('quickjs-wasi')
const {createPacResolver} = await import('pac-resolver')
const qjs = await QuickJS.create()
findProxyForURL = createPacResolver(qjs, content)
} catch (cause) {
const err = new PacError({
code: 'EG_TUNNEL_PAC_COMPILE_FAILED',
sourceKind,
source,
cause,
})
logger?.error({
action: 'pac-load-failed',
code: err.code,
sourceKind,
source,
error: cause,
})
throw err
}
const compileTimeMs = Date.now() - compileStart
logger?.info({
action: 'pac-loaded',
sourceKind,
sourceLength,
compileTimeMs,
})
return findProxyForURL
}
async function resolveSource({pacFile, pacScript, pacUrl}) {
if (pacFile) {
let content
try {
content = await fs.readFile(pacFile, 'utf8')
} catch (cause) {
throw new PacError({
code: 'EG_TUNNEL_PAC_FILE_LOAD_FAILED',
sourceKind: 'file',
source: pacFile,
cause,
})
}
return {sourceKind: 'file', source: pacFile, content: stripBom(content)}
}
if (pacScript) {
return {sourceKind: 'script', source: '<inline>', content: stripBom(pacScript)}
}
if (pacUrl) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), PAC_URL_TIMEOUT_MS)
let res
try {
res = await fetch(pacUrl, {signal: controller.signal})
} catch (cause) {
clearTimeout(timeout)
throw new PacError({
code: 'EG_TUNNEL_PAC_URL_LOAD_FAILED',
sourceKind: 'url',
source: pacUrl,
cause,
})
}
clearTimeout(timeout)
if (!res.ok) {
throw new PacError({
code: 'EG_TUNNEL_PAC_URL_LOAD_FAILED',
sourceKind: 'url',
source: pacUrl,
cause: new Error(`HTTP ${res.status} ${res.statusText}`),
})
}
const content = await res.text()
return {sourceKind: 'url', source: pacUrl, content: stripBom(content)}
}
return null
}
/**
* Strip a leading UTF-8 BOM (``) if present. PAC files saved by some
* editors (notably Windows ones) include a BOM; pac-resolver's parser
* choked on it in early tests with at least one customer in the wild.
* Safe no-op for sources without a BOM. Runs once per service-lifetime —
* cost is negligible.
*/
function stripBom(s) {
return typeof s === 'string' && s.charCodeAt(0) === 0xfeff ? s.slice(1) : s
}
module.exports = {loadPacResolver}