browserless 14.0.33 → 14.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -5
- package/src/index.js +26 -9
- package/src/proxy-forwarder.js +50 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "browserless",
|
|
3
3
|
"description": "The headless Chrome/Chromium driver on top of Puppeteer. Take screenshots, generate PDFs, extract text and HTML with a production-ready API.",
|
|
4
4
|
"homepage": "https://browserless.js.org",
|
|
5
|
-
"version": "14.
|
|
5
|
+
"version": "14.1.2",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"author": {
|
|
@@ -36,14 +36,15 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@browserless/errors": "14.0.22",
|
|
39
|
-
"@browserless/goto": "14.
|
|
40
|
-
"@browserless/pdf": "14.
|
|
41
|
-
"@browserless/screenshot": "14.
|
|
39
|
+
"@browserless/goto": "14.1.2",
|
|
40
|
+
"@browserless/pdf": "14.1.2",
|
|
41
|
+
"@browserless/screenshot": "14.1.2",
|
|
42
42
|
"debug-logfmt": "~1.4.15",
|
|
43
43
|
"kill-process-group": "~1.0.16",
|
|
44
44
|
"p-reflect": "~2.1.0",
|
|
45
45
|
"p-retry": "~4.6.1",
|
|
46
46
|
"p-timeout": "~4.1.0",
|
|
47
|
+
"proxy-chain": "~3.0.1",
|
|
47
48
|
"require-one-of": "~1.0.25",
|
|
48
49
|
"superlock": "~1.4.2"
|
|
49
50
|
},
|
|
@@ -69,5 +70,5 @@
|
|
|
69
70
|
"timeout": "2m",
|
|
70
71
|
"workerThreads": false
|
|
71
72
|
},
|
|
72
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "700bbabadea0a16facf30d76ecf60ff9602e06f8"
|
|
73
74
|
}
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const pRetry = require('p-retry')
|
|
|
12
12
|
|
|
13
13
|
const { AbortError } = pRetry
|
|
14
14
|
|
|
15
|
+
const { forwardProxy, closeForwarder } = require('./proxy-forwarder')
|
|
15
16
|
const driver = require('./driver')
|
|
16
17
|
|
|
17
18
|
// Chrome can stop answering CDP while `connected` stays true, so the
|
|
@@ -33,9 +34,11 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
|
|
|
33
34
|
const { defaultViewport } = goto
|
|
34
35
|
|
|
35
36
|
let isClosed = false
|
|
37
|
+
const forwarders = new Set()
|
|
36
38
|
|
|
37
|
-
const close = opts => {
|
|
39
|
+
const close = async opts => {
|
|
38
40
|
isClosed = true
|
|
41
|
+
await Promise.all([...forwarders].map(closeForwarder))
|
|
39
42
|
return browserProcessPromise.then(browserProcess => driver.close(browserProcess, opts))
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -108,8 +111,15 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
|
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
const createContext = async ({ retry = 2, timeout: contextTimeout, ...contextOpts } = {}) => {
|
|
111
|
-
|
|
114
|
+
const forwardedContextOpts = isClosed
|
|
115
|
+
? Promise.resolve(contextOpts)
|
|
116
|
+
: forwardProxy(contextOpts, { mode: launchOpts.mode })
|
|
117
|
+
forwarders.add(forwardedContextOpts)
|
|
118
|
+
const newBrowserContext = () => forwardedContextOpts.then(createBrowserContext)
|
|
119
|
+
const usesForwarder = () => forwardedContextOpts.then(opts => opts !== contextOpts)
|
|
120
|
+
let _contextPromise = newBrowserContext()
|
|
112
121
|
let isDestroyedForced = false
|
|
122
|
+
let isDestroyed = false
|
|
113
123
|
const pageMetadata = new WeakMap()
|
|
114
124
|
|
|
115
125
|
const getBrowserContext = () => _contextPromise
|
|
@@ -194,13 +204,14 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
|
|
|
194
204
|
const isRetryable =
|
|
195
205
|
error.code === 'EBRWSRCONTEXTCONNRESET' || error.code === 'EPROTOCOL'
|
|
196
206
|
if (!isRetryable) throw error
|
|
207
|
+
if (isDestroyed && (await usesForwarder())) throw new AbortError()
|
|
197
208
|
const { message, attemptNumber, retriesLeft } = error
|
|
198
209
|
// Every attempt builds its own page, so both codes are retryable
|
|
199
210
|
// inside the existing context: `EBRWSRCONTEXTCONNRESET` covers a
|
|
200
211
|
// closed page, a detached frame and a destroyed execution context,
|
|
201
212
|
// which a fresh page recovers from.
|
|
202
213
|
const previousContextPromise = _contextPromise
|
|
203
|
-
_contextPromise =
|
|
214
|
+
_contextPromise = newBrowserContext()
|
|
204
215
|
await pReflect(previousContextPromise.then(ctx => ctx.close()))
|
|
205
216
|
debug('retry', { attemptNumber, retriesLeft, message })
|
|
206
217
|
}
|
|
@@ -237,14 +248,20 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
|
|
|
237
248
|
}
|
|
238
249
|
|
|
239
250
|
const destroyContext = async ({ force = false } = {}) => {
|
|
251
|
+
isDestroyed = true
|
|
240
252
|
if (force) isDestroyedForced = true
|
|
241
253
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
try {
|
|
255
|
+
const [browserProcess, browserContext] = await Promise.all([
|
|
256
|
+
getBrowser(),
|
|
257
|
+
getBrowserContext()
|
|
258
|
+
])
|
|
259
|
+
await pReflect(browserContext.close())
|
|
260
|
+
debug('destroyContext', { pid: driver.pid(browserProcess), force, id: browserContext.id })
|
|
261
|
+
} finally {
|
|
262
|
+
forwarders.delete(forwardedContextOpts)
|
|
263
|
+
await closeForwarder(forwardedContextOpts)
|
|
264
|
+
}
|
|
248
265
|
}
|
|
249
266
|
|
|
250
267
|
return {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { anonymizeProxy, closeAnonymizedProxy } = require('proxy-chain')
|
|
4
|
+
|
|
5
|
+
const REDACTED = '***'
|
|
6
|
+
const DEFAULT_SCHEME = 'http://'
|
|
7
|
+
const CONNECT_MODE_ERROR =
|
|
8
|
+
"A proxyServer with credentials needs `mode: 'launch'`: a connected browser cannot reach the local forwarder"
|
|
9
|
+
|
|
10
|
+
const forwardedProxyServers = new Set()
|
|
11
|
+
|
|
12
|
+
const parseProxyServer = proxyServer =>
|
|
13
|
+
URL.parse(proxyServer.includes('://') ? proxyServer : `${DEFAULT_SCHEME}${proxyServer}`)
|
|
14
|
+
|
|
15
|
+
const hasCredentials = proxyUrl => Boolean(proxyUrl?.username || proxyUrl?.password)
|
|
16
|
+
|
|
17
|
+
const redactSecrets = (text, secrets) =>
|
|
18
|
+
secrets.reduce((redacted, secret) => redacted.replaceAll(secret, REDACTED), text)
|
|
19
|
+
|
|
20
|
+
const withoutCredentials = (error, proxyUrl) => {
|
|
21
|
+
const redactedUrl = new URL(proxyUrl)
|
|
22
|
+
if (proxyUrl.username) redactedUrl.username = REDACTED
|
|
23
|
+
if (proxyUrl.password) redactedUrl.password = REDACTED
|
|
24
|
+
const secrets = [proxyUrl.username, proxyUrl.password].filter(Boolean)
|
|
25
|
+
error.message = error.message
|
|
26
|
+
.split(proxyUrl.href)
|
|
27
|
+
.map(part => redactSecrets(part, secrets))
|
|
28
|
+
.join(redactedUrl.href)
|
|
29
|
+
return error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const forwardProxy = async (contextOpts, { mode } = {}) => {
|
|
33
|
+
const proxyUrl = contextOpts.proxyServer && parseProxyServer(contextOpts.proxyServer)
|
|
34
|
+
if (!hasCredentials(proxyUrl)) return contextOpts
|
|
35
|
+
if (mode === 'connect') throw new TypeError(CONNECT_MODE_ERROR)
|
|
36
|
+
try {
|
|
37
|
+
const proxyServer = await anonymizeProxy(proxyUrl.href)
|
|
38
|
+
forwardedProxyServers.add(proxyServer)
|
|
39
|
+
return { ...contextOpts, proxyServer }
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw withoutCredentials(error, proxyUrl)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const closeForwarder = async contextOptsPromise => {
|
|
46
|
+
const { proxyServer } = await contextOptsPromise.catch(() => ({}))
|
|
47
|
+
if (forwardedProxyServers.delete(proxyServer)) await closeAnonymizedProxy(proxyServer, true)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { forwardProxy, closeForwarder }
|