mirror of
https://github.com/actions/upload-artifact.git
synced 2026-04-09 08:23:07 +00:00
Add proxy tests
This commit is contained in:
parent
1203fabacc
commit
ba6bdbd897
197
__tests__/proxy-policy.test.ts
Normal file
197
__tests__/proxy-policy.test.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* Tests that the proxyPolicy does NOT leak application-level request headers
|
||||||
|
* into the HTTP CONNECT tunnel handshake.
|
||||||
|
*
|
||||||
|
* Background: HttpsProxyAgent's `headers` constructor option specifies headers
|
||||||
|
* to send in the CONNECT request to the proxy server. When Azure SDK request
|
||||||
|
* headers (Content-Type, x-ms-version, x-ms-blob-type, etc.) are passed here,
|
||||||
|
* strict corporate proxies (Fortinet, Zscaler) reject the tunnel — causing
|
||||||
|
* ECONNRESET. See: https://github.com/actions/upload-artifact/issues/XXX
|
||||||
|
*/
|
||||||
|
import {describe, test, expect, beforeEach, afterEach} from '@jest/globals'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createPipelineRequest,
|
||||||
|
type PipelineRequest,
|
||||||
|
type SendRequest
|
||||||
|
} from '@typespec/ts-http-runtime'
|
||||||
|
import {proxyPolicy} from '@typespec/ts-http-runtime/internal/policies'
|
||||||
|
import {HttpsProxyAgent} from 'https-proxy-agent'
|
||||||
|
import {HttpProxyAgent} from 'http-proxy-agent'
|
||||||
|
|
||||||
|
describe('proxyPolicy', () => {
|
||||||
|
const PROXY_URL = 'http://corporate-proxy.example.com:3128'
|
||||||
|
|
||||||
|
let savedHttpsProxy: string | undefined
|
||||||
|
let savedHttpProxy: string | undefined
|
||||||
|
let savedNoProxy: string | undefined
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Save and set proxy env vars
|
||||||
|
savedHttpsProxy = process.env['HTTPS_PROXY']
|
||||||
|
savedHttpProxy = process.env['HTTP_PROXY']
|
||||||
|
savedNoProxy = process.env['NO_PROXY']
|
||||||
|
|
||||||
|
process.env['HTTPS_PROXY'] = PROXY_URL
|
||||||
|
process.env['HTTP_PROXY'] = PROXY_URL
|
||||||
|
delete process.env['NO_PROXY']
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original env
|
||||||
|
if (savedHttpsProxy !== undefined) {
|
||||||
|
process.env['HTTPS_PROXY'] = savedHttpsProxy
|
||||||
|
} else {
|
||||||
|
delete process.env['HTTPS_PROXY']
|
||||||
|
}
|
||||||
|
if (savedHttpProxy !== undefined) {
|
||||||
|
process.env['HTTP_PROXY'] = savedHttpProxy
|
||||||
|
} else {
|
||||||
|
delete process.env['HTTP_PROXY']
|
||||||
|
}
|
||||||
|
if (savedNoProxy !== undefined) {
|
||||||
|
process.env['NO_PROXY'] = savedNoProxy
|
||||||
|
} else {
|
||||||
|
delete process.env['NO_PROXY']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mock "next" handler that captures the request after the proxy policy
|
||||||
|
* has set the agent, so we can inspect it.
|
||||||
|
*/
|
||||||
|
function createCapturingNext(): SendRequest & {
|
||||||
|
capturedRequest: PipelineRequest | undefined
|
||||||
|
} {
|
||||||
|
const fn = async (request: PipelineRequest) => {
|
||||||
|
fn.capturedRequest = request
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
headers: createPipelineRequest({url: ''}).headers,
|
||||||
|
request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn.capturedRequest = undefined as PipelineRequest | undefined
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
|
||||||
|
test('does not leak application headers into HttpsProxyAgent CONNECT request', async () => {
|
||||||
|
const policy = proxyPolicy()
|
||||||
|
const next = createCapturingNext()
|
||||||
|
|
||||||
|
// Simulate an Azure Blob Storage upload request with typical SDK headers
|
||||||
|
const request = createPipelineRequest({
|
||||||
|
url: 'https://productionresultssa0.blob.core.windows.net/artifacts/upload'
|
||||||
|
})
|
||||||
|
request.headers.set('Content-Type', 'application/octet-stream')
|
||||||
|
request.headers.set('x-ms-version', '2024-11-04')
|
||||||
|
request.headers.set('x-ms-blob-type', 'BlockBlob')
|
||||||
|
request.headers.set(
|
||||||
|
'x-ms-client-request-id',
|
||||||
|
'00000000-0000-0000-0000-000000000000'
|
||||||
|
)
|
||||||
|
|
||||||
|
await policy.sendRequest(request, next)
|
||||||
|
|
||||||
|
// The policy should have assigned an HttpsProxyAgent
|
||||||
|
const agent = next.capturedRequest?.agent
|
||||||
|
expect(agent).toBeDefined()
|
||||||
|
expect(agent).toBeInstanceOf(HttpsProxyAgent)
|
||||||
|
|
||||||
|
// CRITICAL: The agent's proxyHeaders must NOT contain application headers.
|
||||||
|
// If this fails, application headers are being leaked into the CONNECT
|
||||||
|
// request, which breaks strict corporate proxies.
|
||||||
|
const proxyAgent = agent as HttpsProxyAgent<string>
|
||||||
|
const proxyHeaders =
|
||||||
|
typeof proxyAgent.proxyHeaders === 'function'
|
||||||
|
? proxyAgent.proxyHeaders()
|
||||||
|
: proxyAgent.proxyHeaders
|
||||||
|
|
||||||
|
expect(proxyHeaders).toBeDefined()
|
||||||
|
|
||||||
|
// None of the Azure SDK application headers should appear
|
||||||
|
const headerObj = proxyHeaders as Record<string, string>
|
||||||
|
expect(headerObj['content-type']).toBeUndefined()
|
||||||
|
expect(headerObj['Content-Type']).toBeUndefined()
|
||||||
|
expect(headerObj['x-ms-version']).toBeUndefined()
|
||||||
|
expect(headerObj['x-ms-blob-type']).toBeUndefined()
|
||||||
|
expect(headerObj['x-ms-client-request-id']).toBeUndefined()
|
||||||
|
|
||||||
|
// proxyHeaders should be empty (no application headers leaked)
|
||||||
|
expect(Object.keys(headerObj).length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not leak application headers into HttpProxyAgent CONNECT request', async () => {
|
||||||
|
const policy = proxyPolicy()
|
||||||
|
const next = createCapturingNext()
|
||||||
|
|
||||||
|
// Simulate an insecure (HTTP) request with application headers
|
||||||
|
const request = createPipelineRequest({
|
||||||
|
url: 'http://example.com/api/upload',
|
||||||
|
allowInsecureConnection: true
|
||||||
|
})
|
||||||
|
request.headers.set('Content-Type', 'application/json')
|
||||||
|
request.headers.set('Authorization', 'Bearer some-token')
|
||||||
|
|
||||||
|
await policy.sendRequest(request, next)
|
||||||
|
|
||||||
|
const agent = next.capturedRequest?.agent
|
||||||
|
expect(agent).toBeDefined()
|
||||||
|
expect(agent).toBeInstanceOf(HttpProxyAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('still routes HTTPS requests through the proxy', async () => {
|
||||||
|
const policy = proxyPolicy()
|
||||||
|
const next = createCapturingNext()
|
||||||
|
|
||||||
|
const request = createPipelineRequest({
|
||||||
|
url: 'https://results-receiver.actions.githubusercontent.com/twirp/test'
|
||||||
|
})
|
||||||
|
|
||||||
|
await policy.sendRequest(request, next)
|
||||||
|
|
||||||
|
const agent = next.capturedRequest?.agent
|
||||||
|
expect(agent).toBeDefined()
|
||||||
|
expect(agent).toBeInstanceOf(HttpsProxyAgent)
|
||||||
|
|
||||||
|
// Verify the proxy URL is correct
|
||||||
|
const proxyAgent = agent as HttpsProxyAgent<string>
|
||||||
|
expect(proxyAgent.proxy.href).toBe(`${PROXY_URL}/`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bypasses proxy for no_proxy hosts', async () => {
|
||||||
|
// Use customNoProxyList since globalNoProxyList is only loaded once.
|
||||||
|
// Patterns starting with "." match subdomains (e.g. ".example.com"
|
||||||
|
// matches "api.example.com"), bare names match the host exactly.
|
||||||
|
const policy = proxyPolicy(undefined, {
|
||||||
|
customNoProxyList: ['.blob.core.windows.net', 'exact-host.test']
|
||||||
|
})
|
||||||
|
const next = createCapturingNext()
|
||||||
|
|
||||||
|
// This host matches ".blob.core.windows.net" via subdomain matching
|
||||||
|
const request = createPipelineRequest({
|
||||||
|
url: 'https://productionresultssa0.blob.core.windows.net/artifacts/upload'
|
||||||
|
})
|
||||||
|
|
||||||
|
await policy.sendRequest(request, next)
|
||||||
|
|
||||||
|
// Agent should not be set for a bypassed host
|
||||||
|
expect(next.capturedRequest?.agent).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not override a custom agent already set on the request', async () => {
|
||||||
|
const policy = proxyPolicy()
|
||||||
|
const next = createCapturingNext()
|
||||||
|
|
||||||
|
const customAgent = new HttpsProxyAgent('http://custom-proxy:9999')
|
||||||
|
const request = createPipelineRequest({
|
||||||
|
url: 'https://blob.core.windows.net/test'
|
||||||
|
})
|
||||||
|
request.agent = customAgent
|
||||||
|
|
||||||
|
await policy.sendRequest(request, next)
|
||||||
|
|
||||||
|
// The policy should not overwrite the pre-existing agent
|
||||||
|
expect(next.capturedRequest?.agent).toBe(customAgent)
|
||||||
|
})
|
||||||
|
})
|
||||||
13
package-lock.json
generated
13
package-lock.json
generated
@ -188,7 +188,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
|
||||||
"integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
|
"integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.1.2",
|
"@azure/abort-controller": "^2.1.2",
|
||||||
"@azure/core-auth": "^1.10.0",
|
"@azure/core-auth": "^1.10.0",
|
||||||
@ -250,7 +249,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz",
|
||||||
"integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==",
|
"integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.1.2",
|
"@azure/abort-controller": "^2.1.2",
|
||||||
"@azure/core-auth": "^1.10.0",
|
"@azure/core-auth": "^1.10.0",
|
||||||
@ -392,7 +390,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@ -1802,7 +1799,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
|
||||||
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
|
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^6.0.0",
|
"@octokit/auth-token": "^6.0.0",
|
||||||
"@octokit/graphql": "^9.0.3",
|
"@octokit/graphql": "^9.0.3",
|
||||||
@ -2207,7 +2203,6 @@
|
|||||||
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
|
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
@ -2799,7 +2794,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -3393,7 +3387,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@ -4279,7 +4272,6 @@
|
|||||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@ -6293,7 +6285,6 @@
|
|||||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "30.2.0",
|
"@jest/core": "30.2.0",
|
||||||
"@jest/types": "30.2.0",
|
"@jest/types": "30.2.0",
|
||||||
@ -7766,7 +7757,6 @@
|
|||||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@ -8818,7 +8808,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -8950,7 +8939,6 @@
|
|||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
"@tsconfig/node10": "^1.0.7",
|
"@tsconfig/node10": "^1.0.7",
|
||||||
@ -9159,7 +9147,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user