Skip to content

Commit 12bb2a6

Browse files
authored
Fixing the Proxy issue with the PCCRV2 (#21384)
* Fixing the Proxy issue with the PCCRV2 * Addressing the comments * Upgarding the PCCRV2 version * upgrading the pccrv2 version
1 parent 16c4e56 commit 12bb2a6

File tree

5 files changed

+144
-7
lines changed

5 files changed

+144
-7
lines changed

Tasks/Common/coveragepublisher/coveragepublisher.ts

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ async function publishCoverage(inputFiles: string[], reportDirectory: string, pa
6464
}
6565

6666
try {
67+
// Get comprehensive proxy configuration to fix .NET HttpClient proxy issues
68+
const proxyConfig = getProxyEnvironmentVariables();
69+
6770
const env = {
6871
"SYSTEM_ACCESSTOKEN": taskLib.getEndpointAuthorizationParameter('SystemVssConnection', 'AccessToken', false),
6972
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI": taskLib.getVariable('System.TeamFoundationCollectionUri'),
@@ -72,9 +75,9 @@ async function publishCoverage(inputFiles: string[], reportDirectory: string, pa
7275
"AGENT_TEMPPATH": taskLib.getVariable('Agent.TempPath'),
7376
"SYSTEM_TEAMPROJECTID": taskLib.getVariable('System.TeamProjectId'),
7477
"PIPELINES_COVERAGEPUBLISHER_DEBUG": taskLib.getVariable('PIPELINES_COVERAGEPUBLISHER_DEBUG'),
75-
"HTTPS_PROXY": process.env['HTTPS_PROXY'],
76-
"NO_PROXY": process.env['NO_PROXY'],
77-
"DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": taskLib.getVariable('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT')
78+
"DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": taskLib.getVariable('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT'),
79+
// Comprehensive proxy configuration for .NET HttpClient
80+
...proxyConfig
7881
};
7982

8083
await dotnet.exec({
@@ -96,6 +99,132 @@ async function publishCoverage(inputFiles: string[], reportDirectory: string, pa
9699
}
97100

98101

102+
function getProxyEnvironmentVariables(): { [key: string]: string } {
103+
const proxyVars: { [key: string]: string } = {};
104+
105+
// Get Azure Pipelines agent proxy configuration (highest priority)
106+
const agentProxyUrl = taskLib.getVariable("agent.proxyurl");
107+
const agentProxyUsername = taskLib.getVariable("agent.proxyusername");
108+
const agentProxyPassword = taskLib.getVariable("agent.proxypassword");
109+
const agentProxyBypass = taskLib.getVariable("agent.proxybypasslist");
110+
111+
// Input validation and sanitization
112+
if (agentProxyUrl && !isValidProxyUrl(agentProxyUrl)) {
113+
taskLib.warning("Invalid proxy URL format detected, skipping agent proxy configuration");
114+
return proxyVars;
115+
}
116+
117+
// Construct proxy URL with authentication if available
118+
let proxyUrl = "";
119+
if (agentProxyUrl) {
120+
if (agentProxyUsername && agentProxyPassword) {
121+
// Validate credentials contain no malicious characters
122+
if (!isValidCredential(agentProxyUsername) || !isValidCredential(agentProxyPassword)) {
123+
taskLib.warning("Invalid characters detected in proxy credentials, using proxy without authentication");
124+
proxyUrl = agentProxyUrl;
125+
} else {
126+
// Add authentication to proxy URL
127+
try {
128+
const url = new URL(agentProxyUrl);
129+
url.username = encodeURIComponent(agentProxyUsername);
130+
url.password = encodeURIComponent(agentProxyPassword);
131+
proxyUrl = url.toString();
132+
taskLib.debug(`Using agent proxy with authentication: ${getMaskedProxyUrl(proxyUrl)}`);
133+
} catch (err) {
134+
proxyUrl = agentProxyUrl;
135+
taskLib.warning(`Failed to parse agent proxy URL, using without authentication: ${err}`);
136+
}
137+
}
138+
} else {
139+
proxyUrl = agentProxyUrl;
140+
taskLib.debug(`Using agent proxy: ${getMaskedProxyUrl(agentProxyUrl)}`);
141+
}
142+
} else {
143+
// Fall back to environment variables
144+
proxyUrl = process.env['HTTPS_PROXY'] || process.env['https_proxy'] ||
145+
process.env['HTTP_PROXY'] || process.env['http_proxy'] || "";
146+
if (proxyUrl) {
147+
taskLib.debug(`Using environment proxy: ${getMaskedProxyUrl(proxyUrl)}`);
148+
}
149+
}
150+
// Set comprehensive proxy environment variables for .NET
151+
if (proxyUrl) {
152+
// Standard proxy environment variables (case variations for compatibility)
153+
proxyVars["HTTP_PROXY"] = proxyUrl;
154+
proxyVars["HTTPS_PROXY"] = proxyUrl;
155+
proxyVars["http_proxy"] = proxyUrl;
156+
proxyVars["https_proxy"] = proxyUrl;
157+
158+
// .NET specific environment variables to force proxy initialization
159+
// These are critical for resolving the HttpClient.DefaultProxy initialization issue
160+
proxyVars["DOTNET_SYSTEM_NET_HTTP_USEDEFAULTCREDENTIALS"] = "true";
161+
proxyVars["DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER"] = "false"; // Forces WinHttpHandler on Windows
162+
}
163+
164+
// Handle NO_PROXY / bypass list
165+
let noProxyList = "";
166+
if (agentProxyBypass) {
167+
noProxyList = agentProxyBypass;
168+
taskLib.debug(`Using agent proxy bypass list: ${agentProxyBypass}`);
169+
} else {
170+
noProxyList = process.env['NO_PROXY'] || process.env['no_proxy'] || "";
171+
if (noProxyList) {
172+
taskLib.debug(`Using environment proxy bypass list: ${noProxyList}`);
173+
}
174+
}
175+
176+
if (noProxyList) {
177+
proxyVars["NO_PROXY"] = noProxyList;
178+
proxyVars["no_proxy"] = noProxyList;
179+
}
180+
181+
// Log configuration for debugging (with enhanced credential masking)
182+
if (proxyUrl) {
183+
taskLib.debug(`Proxy configuration set for .NET application: ${getMaskedProxyUrl(proxyUrl)}`);
184+
if (noProxyList) {
185+
taskLib.debug(`Proxy bypass list: ${noProxyList}`);
186+
}
187+
} else {
188+
taskLib.debug("No proxy configuration detected");
189+
}
190+
191+
// Security note: Environment variables will contain proxy credentials
192+
// Ensure child process cleans up these variables after use
193+
return proxyVars;
194+
}
195+
196+
// Security helper functions
197+
function isValidProxyUrl(url: string): boolean {
198+
try {
199+
const parsedUrl = new URL(url);
200+
return ['http:', 'https:'].includes(parsedUrl.protocol);
201+
} catch {
202+
return false;
203+
}
204+
}
205+
206+
function isValidCredential(credential: string): boolean {
207+
// Basic validation to prevent injection attacks
208+
// Reject credentials containing potentially dangerous characters
209+
const dangerousChars = /[\r\n\0\x1f<>"'`\\]/;
210+
return !dangerousChars.test(credential) && credential.length <= 256;
211+
}
212+
213+
function getMaskedProxyUrl(url: string): string {
214+
if (!url) return url;
215+
try {
216+
const parsedUrl = new URL(url);
217+
if (parsedUrl.username || parsedUrl.password) {
218+
// Mask both username and password for security
219+
return `${parsedUrl.protocol}//***:***@${parsedUrl.host}${parsedUrl.pathname}${parsedUrl.search}`;
220+
}
221+
return url;
222+
} catch {
223+
// If URL parsing fails, mask any potential credentials pattern
224+
return url.replace(/\/\/[^@]*@/, '//***:***@');
225+
}
226+
}
227+
99228
function isNullOrWhitespace(input: any) {
100229
if (typeof input === 'undefined' || input == null) {
101230
return true;

Tasks/PublishCodeCoverageResultsV2/Tests/L0.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,14 @@ describe('PublishCodeCoverageResultsV2 Suite', function () {
2626
assert(tr.succeeded, 'task should have succeeded'); // It will give a message of No code coverage for empty inputs
2727
});
2828

29+
// New proxy configuration tests
30+
it('Should handle agent proxy configuration correctly', async function() {
31+
const testPath = path.join(__dirname, 'L0ProxyAgentConfig.ts');
32+
const tr: MockTestRunner = new MockTestRunner(testPath);
33+
await tr.runAsync();
34+
35+
// Verify proxy environment variables are set correctly
36+
assert(tr.succeeded || tr.stdout.indexOf('Using agent proxy') >= 0, 'Should configure agent proxy');
37+
});
38+
2939
});

Tasks/PublishCodeCoverageResultsV2/task.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"author": "Microsoft Corporation",
1717
"version": {
1818
"Major": 2,
19-
"Minor": 264,
19+
"Minor": 265,
2020
"Patch": 0
2121
},
2222
"demands": [],

Tasks/PublishCodeCoverageResultsV2/task.loc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"author": "Microsoft Corporation",
1717
"version": {
1818
"Major": 2,
19-
"Minor": 264,
19+
"Minor": 265,
2020
"Patch": 0
2121
},
2222
"demands": [],

package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)