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 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.0.33",
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.0.33",
40
- "@browserless/pdf": "14.0.33",
41
- "@browserless/screenshot": "14.0.33",
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": "c1373eae5ddf96b49452076e4e184d4f2d2e08f3"
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
- let _contextPromise = createBrowserContext(contextOpts)
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 = createBrowserContext(contextOpts)
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
- const [browserProcess, browserContext] = await Promise.all([
243
- getBrowser(),
244
- getBrowserContext()
245
- ])
246
- await pReflect(browserContext.close())
247
- debug('destroyContext', { pid: driver.pid(browserProcess), force, id: browserContext.id })
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 }