Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 132 additions & 3 deletions Tasks/Common/coveragepublisher/coveragepublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ async function publishCoverage(inputFiles: string[], reportDirectory: string, pa
}

try {
// Get comprehensive proxy configuration to fix .NET HttpClient proxy issues
const proxyConfig = getProxyEnvironmentVariables();

const env = {
"SYSTEM_ACCESSTOKEN": taskLib.getEndpointAuthorizationParameter('SystemVssConnection', 'AccessToken', false),
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI": taskLib.getVariable('System.TeamFoundationCollectionUri'),
Expand All @@ -72,9 +75,9 @@ async function publishCoverage(inputFiles: string[], reportDirectory: string, pa
"AGENT_TEMPPATH": taskLib.getVariable('Agent.TempPath'),
"SYSTEM_TEAMPROJECTID": taskLib.getVariable('System.TeamProjectId'),
"PIPELINES_COVERAGEPUBLISHER_DEBUG": taskLib.getVariable('PIPELINES_COVERAGEPUBLISHER_DEBUG'),
"HTTPS_PROXY": process.env['HTTPS_PROXY'],
"NO_PROXY": process.env['NO_PROXY'],
"DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": taskLib.getVariable('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT')
"DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": taskLib.getVariable('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT'),
// Comprehensive proxy configuration for .NET HttpClient
...proxyConfig
};

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


function getProxyEnvironmentVariables(): { [key: string]: string } {
const proxyVars: { [key: string]: string } = {};

// Get Azure Pipelines agent proxy configuration (highest priority)
const agentProxyUrl = taskLib.getVariable("agent.proxyurl");
const agentProxyUsername = taskLib.getVariable("agent.proxyusername");
const agentProxyPassword = taskLib.getVariable("agent.proxypassword");
const agentProxyBypass = taskLib.getVariable("agent.proxybypasslist");

// Input validation and sanitization
if (agentProxyUrl && !isValidProxyUrl(agentProxyUrl)) {
taskLib.warning("Invalid proxy URL format detected, skipping agent proxy configuration");
return proxyVars;
}

// Construct proxy URL with authentication if available
let proxyUrl = "";
if (agentProxyUrl) {
if (agentProxyUsername && agentProxyPassword) {
// Validate credentials contain no malicious characters
if (!isValidCredential(agentProxyUsername) || !isValidCredential(agentProxyPassword)) {
taskLib.warning("Invalid characters detected in proxy credentials, using proxy without authentication");
proxyUrl = agentProxyUrl;
} else {
// Add authentication to proxy URL
try {
const url = new URL(agentProxyUrl);
url.username = encodeURIComponent(agentProxyUsername);
url.password = encodeURIComponent(agentProxyPassword);
proxyUrl = url.toString();
taskLib.debug(`Using agent proxy with authentication: ${getMaskedProxyUrl(proxyUrl)}`);
} catch (err) {
proxyUrl = agentProxyUrl;
taskLib.warning(`Failed to parse agent proxy URL, using without authentication: ${err}`);
}
}
} else {
proxyUrl = agentProxyUrl;
taskLib.debug(`Using agent proxy: ${getMaskedProxyUrl(agentProxyUrl)}`);
}
} else {
// Fall back to environment variables
proxyUrl = process.env['HTTPS_PROXY'] || process.env['https_proxy'] ||
process.env['HTTP_PROXY'] || process.env['http_proxy'] || "";
if (proxyUrl) {
taskLib.debug(`Using environment proxy: ${getMaskedProxyUrl(proxyUrl)}`);
}
}
// Set comprehensive proxy environment variables for .NET
if (proxyUrl) {
// Standard proxy environment variables (case variations for compatibility)
proxyVars["HTTP_PROXY"] = proxyUrl;
proxyVars["HTTPS_PROXY"] = proxyUrl;
proxyVars["http_proxy"] = proxyUrl;
proxyVars["https_proxy"] = proxyUrl;

// .NET specific environment variables to force proxy initialization
// These are critical for resolving the HttpClient.DefaultProxy initialization issue
proxyVars["DOTNET_SYSTEM_NET_HTTP_USEDEFAULTCREDENTIALS"] = "true";
proxyVars["DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER"] = "false"; // Forces WinHttpHandler on Windows
}

// Handle NO_PROXY / bypass list
let noProxyList = "";
if (agentProxyBypass) {
noProxyList = agentProxyBypass;
taskLib.debug(`Using agent proxy bypass list: ${agentProxyBypass}`);
} else {
noProxyList = process.env['NO_PROXY'] || process.env['no_proxy'] || "";
if (noProxyList) {
taskLib.debug(`Using environment proxy bypass list: ${noProxyList}`);
}
}

if (noProxyList) {
proxyVars["NO_PROXY"] = noProxyList;
proxyVars["no_proxy"] = noProxyList;
}

// Log configuration for debugging (with enhanced credential masking)
if (proxyUrl) {
taskLib.debug(`Proxy configuration set for .NET application: ${getMaskedProxyUrl(proxyUrl)}`);
if (noProxyList) {
taskLib.debug(`Proxy bypass list: ${noProxyList}`);
}
} else {
taskLib.debug("No proxy configuration detected");
}

// Security note: Environment variables will contain proxy credentials
// Ensure child process cleans up these variables after use
return proxyVars;
}

// Security helper functions
function isValidProxyUrl(url: string): boolean {
try {
const parsedUrl = new URL(url);
return ['http:', 'https:'].includes(parsedUrl.protocol);
} catch {
return false;
}
}

function isValidCredential(credential: string): boolean {
// Basic validation to prevent injection attacks
// Reject credentials containing potentially dangerous characters
const dangerousChars = /[\r\n\0\x1f<>"'`\\]/;
return !dangerousChars.test(credential) && credential.length <= 256;
}

function getMaskedProxyUrl(url: string): string {
if (!url) return url;
try {
const parsedUrl = new URL(url);
if (parsedUrl.username || parsedUrl.password) {
// Mask both username and password for security
return `${parsedUrl.protocol}//***:***@${parsedUrl.host}${parsedUrl.pathname}${parsedUrl.search}`;
}
return url;
} catch {
// If URL parsing fails, mask any potential credentials pattern
return url.replace(/\/\/[^@]*@/, '//***:***@');
}
}

function isNullOrWhitespace(input: any) {
if (typeof input === 'undefined' || input == null) {
return true;
Expand Down
10 changes: 10 additions & 0 deletions Tasks/PublishCodeCoverageResultsV2/Tests/L0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,14 @@ describe('PublishCodeCoverageResultsV2 Suite', function () {
assert(tr.succeeded, 'task should have succeeded'); // It will give a message of No code coverage for empty inputs
});

// New proxy configuration tests
it('Should handle agent proxy configuration correctly', async function() {
const testPath = path.join(__dirname, 'L0ProxyAgentConfig.ts');
const tr: MockTestRunner = new MockTestRunner(testPath);
await tr.runAsync();

// Verify proxy environment variables are set correctly
assert(tr.succeeded || tr.stdout.indexOf('Using agent proxy') >= 0, 'Should configure agent proxy');
});

});
2 changes: 1 addition & 1 deletion Tasks/PublishCodeCoverageResultsV2/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"version": {
"Major": 2,
"Minor": 264,
"Patch": 0
"Patch": 1
},
"demands": [],
"minimumAgentVersion": "2.144.0",
Expand Down
2 changes: 1 addition & 1 deletion Tasks/PublishCodeCoverageResultsV2/task.loc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"version": {
"Major": 2,
"Minor": 264,
"Patch": 0
"Patch": 1
},
"demands": [],
"minimumAgentVersion": "2.144.0",
Expand Down
2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading