Skip to content

Commit 7ebc0fd

Browse files
committed
C#: Re-factor more the feed related methods into the FeedManager.
1 parent 62bfb70 commit 7ebc0fd

3 files changed

Lines changed: 227 additions & 224 deletions

File tree

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
44
using System.Linq;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Security.Cryptography.X509Certificates;
58
using System.Text;
69
using System.Text.RegularExpressions;
10+
using System.Threading;
11+
using System.Threading.Tasks;
712
using Semmle.Util;
813
using Semmle.Util.Logging;
914

1015
namespace Semmle.Extraction.CSharp.DependencyFetching
1116
{
1217
internal sealed partial class FeedManager : IDisposable
1318
{
19+
internal const string PublicNugetOrgFeed = "https://api.nuget.org/v3/index.json";
20+
1421
private readonly ILogger logger;
1522
private readonly IDotNet dotnet;
23+
private readonly DependabotProxy? dependabotProxy;
1624
private readonly DependencyDirectory emptyPackageDirectory;
1725

1826
public ImmutableHashSet<string> PrivateRegistryFeeds { get; }
@@ -23,6 +31,7 @@ public FeedManager(ILogger logger, IDotNet dotnet, DependabotProxy? dependabotPr
2331
{
2432
this.logger = logger;
2533
this.dotnet = dotnet;
34+
this.dependabotProxy = dependabotProxy;
2635
PrivateRegistryFeeds = dependabotProxy?.RegistryURLs.ToImmutableHashSet() ?? [];
2736
HasPrivateRegistryFeeds = PrivateRegistryFeeds.Count > 0;
2837
emptyPackageDirectory = new DependencyDirectory("empty", "empty package", logger);
@@ -114,6 +123,217 @@ private string FeedsToRestoreArgument(IEnumerable<string> feeds)
114123
return FeedsToRestoreArgument(feedsToUse);
115124
}
116125

126+
private (int initialTimeout, int tryCount) GetFeedRequestSettings(bool isFallback)
127+
{
128+
int timeoutMilliSeconds = isFallback && int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariableNames.NugetFeedResponsivenessInitialTimeoutForFallback), out timeoutMilliSeconds)
129+
? timeoutMilliSeconds
130+
: int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariableNames.NugetFeedResponsivenessInitialTimeout), out timeoutMilliSeconds)
131+
? timeoutMilliSeconds
132+
: 1000;
133+
logger.LogDebug($"Initial timeout for NuGet feed reachability check is {timeoutMilliSeconds}ms.");
134+
135+
int tryCount = isFallback && int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariableNames.NugetFeedResponsivenessRequestCountForFallback), out tryCount)
136+
? tryCount
137+
: int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariableNames.NugetFeedResponsivenessRequestCount), out tryCount)
138+
? tryCount
139+
: 4;
140+
logger.LogDebug($"Number of tries for NuGet feed reachability check is {tryCount}.");
141+
142+
return (timeoutMilliSeconds, tryCount);
143+
}
144+
145+
private static async Task<HttpResponseMessage> ExecuteGetRequest(string address, HttpClient httpClient, CancellationToken cancellationToken)
146+
{
147+
return await httpClient.GetAsync(address, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
148+
}
149+
150+
private bool IsFeedReachable(string feed, int timeoutMilliSeconds, int tryCount, out bool isTimeout)
151+
{
152+
logger.LogInfo($"Checking if NuGet feed '{feed}' is reachable...");
153+
154+
// Configure the HttpClient to be aware of the Dependabot Proxy, if used.
155+
HttpClientHandler httpClientHandler = new();
156+
if (dependabotProxy != null)
157+
{
158+
httpClientHandler.Proxy = new WebProxy(dependabotProxy.Address);
159+
160+
if (dependabotProxy.Certificate != null)
161+
{
162+
httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, _) =>
163+
{
164+
if (chain is null || cert is null)
165+
{
166+
var msg = cert is null && chain is null
167+
? "certificate and chain"
168+
: chain is null
169+
? "chain"
170+
: "certificate";
171+
logger.LogWarning($"Dependabot proxy certificate validation failed due to missing {msg}");
172+
return false;
173+
}
174+
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
175+
chain.ChainPolicy.CustomTrustStore.Add(dependabotProxy.Certificate);
176+
return chain.Build(cert);
177+
};
178+
}
179+
}
180+
181+
using HttpClient client = new(httpClientHandler);
182+
183+
isTimeout = false;
184+
185+
for (var i = 0; i < tryCount; i++)
186+
{
187+
using var cts = new CancellationTokenSource();
188+
cts.CancelAfter(timeoutMilliSeconds);
189+
try
190+
{
191+
logger.LogInfo($"Attempt {i + 1}/{tryCount} to reach NuGet feed '{feed}'.");
192+
using var response = ExecuteGetRequest(feed, client, cts.Token).GetAwaiter().GetResult();
193+
response.EnsureSuccessStatusCode();
194+
logger.LogInfo($"Querying NuGet feed '{feed}' succeeded.");
195+
return true;
196+
}
197+
catch (Exception exc)
198+
{
199+
if (exc is TaskCanceledException tce &&
200+
tce.CancellationToken == cts.Token &&
201+
cts.Token.IsCancellationRequested)
202+
{
203+
logger.LogInfo($"Didn't receive answer from NuGet feed '{feed}' in {timeoutMilliSeconds}ms.");
204+
timeoutMilliSeconds *= 2;
205+
continue;
206+
}
207+
208+
logger.LogInfo($"Querying NuGet feed '{feed}' failed. The reason for the failure: {exc.Message}");
209+
return false;
210+
}
211+
}
212+
213+
logger.LogWarning($"Didn't receive answer from NuGet feed '{feed}'. Tried it {tryCount} times.");
214+
isTimeout = true;
215+
return false;
216+
}
217+
218+
/// <summary>
219+
/// Retrieves a list of excluded NuGet feeds from the corresponding environment variable.
220+
/// </summary>
221+
private HashSet<string> GetExcludedFeeds()
222+
{
223+
var excludedFeeds = EnvironmentVariables.GetURLs(EnvironmentVariableNames.ExcludedNugetFeedsFromResponsivenessCheck)
224+
.ToHashSet();
225+
226+
if (excludedFeeds.Count > 0)
227+
{
228+
logger.LogInfo($"Excluded NuGet feeds from responsiveness check: {string.Join(", ", excludedFeeds.OrderBy(f => f))}");
229+
}
230+
231+
return excludedFeeds;
232+
}
233+
234+
/// <summary>
235+
/// Checks that we can connect to the specified NuGet feeds.
236+
/// </summary>
237+
/// <param name="feeds">The set of package feeds to check.</param>
238+
/// <param name="reachableFeeds">The list of feeds that were reachable.</param>
239+
/// <returns>
240+
/// True if there is a timeout when trying to reach the feeds (excluding any feeds that are configured
241+
/// to be excluded from the check) or false otherwise.
242+
/// </returns>
243+
public bool CheckSpecifiedFeeds(HashSet<string> feeds, out HashSet<string> reachableFeeds)
244+
{
245+
// Exclude any feeds from the feed check that are configured by the corresponding environment variable.
246+
// These feeds are always assumed to be reachable.
247+
var excludedFeeds = GetExcludedFeeds();
248+
249+
HashSet<string> feedsToCheck = feeds.Where(feed =>
250+
{
251+
if (excludedFeeds.Contains(feed))
252+
{
253+
logger.LogInfo($"Not checking reachability of NuGet feed '{feed}' as it is in the list of excluded feeds.");
254+
return false;
255+
}
256+
return true;
257+
}).ToHashSet();
258+
259+
reachableFeeds = GetReachableNuGetFeeds(feedsToCheck, isFallback: false, out var isTimeout).ToHashSet();
260+
261+
// Always consider feeds excluded for the reachability check as reachable.
262+
reachableFeeds.UnionWith(feeds.Where(feed => excludedFeeds.Contains(feed)));
263+
264+
return isTimeout;
265+
}
266+
267+
public bool IsDefaultFeedReachable()
268+
{
269+
if (CheckNugetFeedResponsiveness)
270+
{
271+
var (initialTimeout, tryCount) = GetFeedRequestSettings(isFallback: false);
272+
return IsFeedReachable(PublicNugetOrgFeed, initialTimeout, tryCount, out var _);
273+
}
274+
275+
return true;
276+
}
277+
278+
/// <summary>
279+
/// Tests which of the feeds given by <paramref name="feedsToCheck"/> are reachable.
280+
/// </summary>
281+
/// <param name="feedsToCheck">The feeds to check.</param>
282+
/// <param name="isFallback">Whether the feeds are fallback feeds or not.</param>
283+
/// <param name="isTimeout">Whether a timeout occurred while checking the feeds.</param>
284+
/// <returns>The list of feeds that could be reached.</returns>
285+
public List<string> GetReachableNuGetFeeds(HashSet<string> feedsToCheck, bool isFallback, out bool isTimeout)
286+
{
287+
var fallbackStr = isFallback ? "fallback " : "";
288+
logger.LogInfo($"Checking {fallbackStr}NuGet feed reachability on feeds: {string.Join(", ", feedsToCheck.OrderBy(f => f))}");
289+
290+
var (initialTimeout, tryCount) = GetFeedRequestSettings(isFallback);
291+
var timeout = false;
292+
var reachableFeeds = feedsToCheck
293+
.Where(feed =>
294+
{
295+
var reachable = IsFeedReachable(feed, initialTimeout, tryCount, out var feedTimeout);
296+
timeout |= feedTimeout;
297+
return reachable;
298+
})
299+
.ToList();
300+
301+
if (reachableFeeds.Count == 0)
302+
{
303+
logger.LogWarning($"No {fallbackStr}NuGet feeds are reachable.");
304+
}
305+
else
306+
{
307+
logger.LogInfo($"Reachable {fallbackStr}NuGet feeds: {string.Join(", ", reachableFeeds.OrderBy(f => f))}");
308+
}
309+
310+
isTimeout = timeout;
311+
return reachableFeeds;
312+
}
313+
314+
public List<string> GetReachableFallbackNugetFeeds(HashSet<string>? feedsFromNugetConfigs)
315+
{
316+
var fallbackFeeds = EnvironmentVariables.GetURLs(EnvironmentVariableNames.FallbackNugetFeeds).ToHashSet();
317+
if (fallbackFeeds.Count == 0)
318+
{
319+
fallbackFeeds.Add(PublicNugetOrgFeed);
320+
logger.LogInfo($"No fallback NuGet feeds specified. Adding default feed: {PublicNugetOrgFeed}");
321+
322+
var shouldAddNugetConfigFeeds = EnvironmentVariables.GetBooleanOptOut(EnvironmentVariableNames.AddNugetConfigFeedsToFallback);
323+
logger.LogInfo($"Adding feeds from nuget.config to fallback restore: {shouldAddNugetConfigFeeds}");
324+
325+
if (shouldAddNugetConfigFeeds && feedsFromNugetConfigs?.Count > 0)
326+
{
327+
// There are some feeds in `feedsFromNugetConfigs` that have already been checked for reachability, we could skip those.
328+
// But we might use different responsiveness testing settings when we try them in the fallback logic, so checking them again is safer.
329+
fallbackFeeds.UnionWith(feedsFromNugetConfigs);
330+
logger.LogInfo($"Using NuGet feeds from nuget.config files as fallback feeds: {string.Join(", ", feedsFromNugetConfigs.OrderBy(f => f))}");
331+
}
332+
}
333+
334+
return GetReachableNuGetFeeds(fallbackFeeds, isFallback: true, out var _);
335+
}
336+
117337
[GeneratedRegex(@"^E\s(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
118338
private static partial Regex EnabledNugetFeed();
119339

0 commit comments

Comments
 (0)