diff --git a/package-lock.json b/package-lock.json index bf3abb7b5fa..a6c5fdddc7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5484,7 +5484,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -5497,7 +5496,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -5510,7 +5508,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -5523,7 +5520,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -5536,7 +5532,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -5549,7 +5544,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -5562,7 +5556,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5575,7 +5568,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5588,7 +5580,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5601,7 +5592,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5614,7 +5604,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5627,7 +5616,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5640,7 +5628,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5653,7 +5640,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5666,7 +5652,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5679,7 +5664,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5692,7 +5676,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -5705,7 +5688,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "openharmony" @@ -5718,7 +5700,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -5731,7 +5712,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -5744,7 +5724,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -5757,7 +5736,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -12268,7 +12246,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 99e1eb360e5..d7d3aff64be 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -17,6 +17,8 @@ import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt' import merge from 'lodash/merge.js' import pick from 'lodash/pick.js' +import type { HttpsProxyAgent } from 'https-proxy-agent' + import { getAgent } from '../lib/http-agent.js' import { NETLIFY_CYAN, @@ -590,7 +592,15 @@ export default class BaseCommand extends Command { httpProxy: flags.httpProxy, certificateFile: flags.httpProxyCertificateFilename, }) - const apiOpts = { ...apiUrlOpts, agent } + const apiOpts: { + userAgent: string + scheme?: string + host?: string + pathPrefix?: string + fetchOptions?: { + agent?: HttpsProxyAgent + } + } = { ...apiUrlOpts, fetchOptions: { agent } } const api = new NetlifyAPI(token ?? '', apiOpts) actionCommand.siteId = flags.siteId || (typeof flags.site === 'string' && flags.site) || state.get('siteId') diff --git a/src/lib/http-agent.ts b/src/lib/http-agent.ts index d9f44df9b65..ff7e5ea9533 100644 --- a/src/lib/http-agent.ts +++ b/src/lib/http-agent.ts @@ -5,118 +5,101 @@ import { HttpsProxyAgent } from 'https-proxy-agent' import { NETLIFYDEVERR, NETLIFYDEVWARN, exit, log } from '../utils/command-helpers.js' import { waitPort } from './wait-port.js' -// https://github.com/TooTallNate/node-https-proxy-agent/issues/89 -// Maybe replace with https://github.com/delvedor/hpagent -// @ts-expect-error TS(2507) FIXME: Type 'typeof createHttpsProxyAgent' is not a const... Remove this comment to see the full error message -class HttpsProxyAgentWithCA extends HttpsProxyAgent { - // @ts-expect-error TS(7006) FIXME: Parameter 'opts' implicitly has an 'any' type. - constructor(opts) { - super(opts) - // @ts-expect-error TS(2339) FIXME: Property 'ca' does not exist on type 'HttpsProxyAg... Remove this comment to see the full error message - this.ca = opts.ca - } - - // @ts-expect-error TS(7006) FIXME: Parameter 'req' implicitly has an 'any' type. - callback(req, opts) { - return super.callback(req, { - ...opts, - // @ts-expect-error TS(2339) FIXME: Property 'ca' does not exist on type 'HttpsProxyAg... Remove this comment to see the full error message - ...(this.ca && { ca: this.ca }), - }) - } -} - const DEFAULT_HTTP_PORT = 80 const DEFAULT_HTTPS_PORT = 443 // 50 seconds const AGENT_PORT_TIMEOUT = 50_000 +type Success = { + agent: HttpsProxyAgent + warning?: { message: string; details?: string } +} + +type Failure = { + error: { message: string; details?: string } +} + export const tryGetAgent = async ({ certificateFile, httpProxy, }: { httpProxy?: string | undefined certificateFile?: string | undefined -}): Promise< - | { - error?: string | undefined - warning?: string | undefined - message?: string | undefined - } - | { - agent: HttpsProxyAgentWithCA - response: unknown - } -> => { +}): Promise => { if (!httpProxy) { - return {} + return } - let proxyUrl + let proxyUrl: URL try { proxyUrl = new URL(httpProxy) } catch { - return { error: `${httpProxy} is not a valid URL` } + return { error: { message: `${httpProxy} is not a valid URL` } } } const scheme = proxyUrl.protocol.slice(0, -1) if (!['http', 'https'].includes(scheme)) { - return { error: `${httpProxy} must have a scheme of http or https` } + return { error: { message: `${httpProxy} must have a scheme of http or https` } } } - let port try { - port = await waitPort( + const port = await waitPort( Number.parseInt(proxyUrl.port) || (scheme === 'http' ? DEFAULT_HTTP_PORT : DEFAULT_HTTPS_PORT), proxyUrl.hostname, AGENT_PORT_TIMEOUT, ) + + if (!port.open) { + // timeout error + return { error: { message: `Could not connect to '${httpProxy}'` } } + } } catch (error) { - // unknown error - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - return { error: `${httpProxy} is not available.`, message: error.message } - } + const details = error instanceof Error ? error.message : String(error) - if (!port.open) { - // timeout error - return { error: `Could not connect to '${httpProxy}'` } + return { error: { message: `${httpProxy} is not available.`, details } } } - let response = {} + let certificate: Buffer | undefined + let warning: { message: string; details?: string } | undefined - let certificate if (certificateFile) { try { certificate = await readFile(certificateFile) } catch (error) { - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - response = { warning: `Could not read certificate file '${certificateFile}'.`, message: error.message } + const details = error instanceof Error ? error.message : String(error) + + warning = { message: `Could not read certificate file '${certificateFile}'.`, details } } } - const opts = { - port: proxyUrl.port, - host: proxyUrl.host, - hostname: proxyUrl.hostname, - protocol: proxyUrl.protocol, - ca: certificate, - } + const agent = new HttpsProxyAgent(httpProxy, { ca: certificate }) - const agent = new HttpsProxyAgentWithCA(opts) - response = { ...response, agent } - return response + return { agent, warning } } -// @ts-expect-error TS(7031) FIXME: Binding element 'certificateFile' implicitly has a... Remove this comment to see the full error message -export const getAgent = async ({ certificateFile, httpProxy }) => { - // @ts-expect-error TS(2339) FIXME: Property 'agent' does not exist on type '{ error?:... Remove this comment to see the full error message - const { agent, error, message, warning } = await tryGetAgent({ httpProxy, certificateFile }) - if (error) { - log(NETLIFYDEVERR, error, message || '') +export const getAgent = async ({ + certificateFile, + httpProxy, +}: { + certificateFile?: string + httpProxy?: string +}): Promise | undefined> => { + const result = await tryGetAgent({ httpProxy, certificateFile }) + + if (result && 'error' in result) { + const { + error: { details, message }, + } = result + log(NETLIFYDEVERR, message, details || '') exit(1) } - if (warning) { - log(NETLIFYDEVWARN, warning, message || '') + + if (result && 'warning' in result && result.warning) { + const { + warning: { details, message }, + } = result + log(NETLIFYDEVWARN, message, details || '') } - return agent + + return result && 'agent' in result ? result.agent : undefined } diff --git a/tests/unit/lib/http-agent.test.ts b/tests/unit/lib/http-agent.test.ts index fd925032672..0c8b8d2e626 100644 --- a/tests/unit/lib/http-agent.test.ts +++ b/tests/unit/lib/http-agent.test.ts @@ -9,28 +9,28 @@ import { tryGetAgent } from '../../../src/lib/http-agent.js' describe('tryGetAgent', () => { test(`should return an empty object when there is no httpProxy`, async () => { - expect(await tryGetAgent({})).toEqual({}) + expect(await tryGetAgent({})).toBeUndefined() }) test(`should return error on invalid url`, async () => { const httpProxy = 'invalid_url' const result = await tryGetAgent({ httpProxy }) - expect(result).toHaveProperty('error', expect.any(String)) + expect(result).toHaveProperty('error') }) test(`should return error when scheme is not http or https`, async () => { const httpProxy = 'file://localhost' const result = await tryGetAgent({ httpProxy }) - expect(result).toHaveProperty('error', expect.any(String)) + expect(result).toHaveProperty('error') }) test(`should return error when proxy is not available`, async () => { const httpProxy = 'https://unknown:7979' const result = await tryGetAgent({ httpProxy }) - expect(result).toHaveProperty('error', expect.any(String)) + expect(result).toHaveProperty('error') }) test(`should return agent for a valid proxy`, async () => { @@ -48,7 +48,7 @@ describe('tryGetAgent', () => { const httpProxyUrl = `http://localhost:${(server.address() as net.AddressInfo).port.toString()}` const result = await tryGetAgent({ httpProxy: httpProxyUrl }) - if (!('agent' in result)) { + if (!result || !('agent' in result)) { throw new Error('expected result to include agent') } expect(result.agent).toBeInstanceOf(HttpsProxyAgent)