22using System . Collections . Generic ;
33using System . Collections . Immutable ;
44using System . Linq ;
5+ using System . Net ;
6+ using System . Net . Http ;
7+ using System . Security . Cryptography . X509Certificates ;
58using System . Text ;
69using System . Text . RegularExpressions ;
10+ using System . Threading ;
11+ using System . Threading . Tasks ;
712using Semmle . Util ;
813using Semmle . Util . Logging ;
914
1015namespace 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