Skip to content

Commit a053203

Browse files
authored
Typosquatting optimization: add caching (#6597)
* Typosquatting optimization: add caching * update * Move locker to cache class * Refactor, Decouple and Update a lot of things * Update * Fix unit test * Update again * Set default cache expire time as 24 hours * Some changes * Use TimeSpan for expire time * Add some tests for cache
1 parent 0146ed3 commit a053203

13 files changed

Lines changed: 443 additions & 94 deletions

src/NuGetGallery/App_Start/DefaultDependenciesModule.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,11 @@ protected override void Load(ContainerBuilder builder)
333333
.As<ITyposquattingService>()
334334
.InstancePerLifetimeScope();
335335

336+
builder.RegisterType<TyposquattingCheckListCacheService>()
337+
.AsSelf()
338+
.As<ITyposquattingCheckListCacheService>()
339+
.SingleInstance();
340+
336341
RegisterMessagingService(builder, configuration);
337342

338343
builder.Register(c => HttpContext.Current.User)

src/NuGetGallery/NuGetGallery.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@
423423
<Compile Include="Infrastructure\Mail\BackgroundMarkdownMessageService.cs" />
424424
<Compile Include="Services\ISymbolPackageUploadService.cs" />
425425
<Compile Include="Services\ISymbolPackageFileService.cs" />
426+
<Compile Include="Services\ITyposquattingCheckListCacheService.cs" />
426427
<Compile Include="Services\ITyposquattingConfiguration.cs" />
427428
<Compile Include="Services\ISymbolsConfiguration.cs" />
428429
<Compile Include="Services\ITyposquattingService.cs" />
@@ -435,6 +436,7 @@
435436
<Compile Include="Services\SymbolPackageUploadService.cs" />
436437
<Compile Include="Services\SymbolPackageFileService.cs" />
437438
<Compile Include="Services\SymbolPackageService.cs" />
439+
<Compile Include="Services\TyposquattingCheckListCacheService.cs" />
438440
<Compile Include="Services\TyposquattingConfiguration.cs" />
439441
<Compile Include="Services\SymbolsConfiguration.cs" />
440442
<Compile Include="Services\CertificatesConfiguration.cs" />

src/NuGetGallery/Services/ITelemetryService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,15 @@ public interface ITelemetryService
203203
/// <param name="totalTime">The total time for the typosquatting check logic</param>
204204
/// <param name="wasUploadBlocked">Whether the uploaded package is blocked because of typosquatting check.</param>
205205
/// <param name="collisionPackageIds">The list of collision package Ids for this uploaded package.</param>
206-
/// <param name="checklistLength">The length of the checklist for typosquatting check</param>
206+
/// <param name="checkListLength">The length of the checklist for typosquatting check</param>
207+
/// <param name="checkListCacheExpireTime">The expire time for checklist caching</param>
207208
void TrackMetricForTyposquattingCheckResultAndTotalTime(
208209
string packageId,
209210
TimeSpan totalTime,
210211
bool wasUploadBlocked,
211212
List<string> collisionPackageIds,
212-
int checklistLength);
213+
int checkListLength,
214+
TimeSpan checkListCacheExpireTime);
213215

214216
/// <summary>
215217
/// The retrieval time to get the checklist for typosquatting check.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace NuGetGallery
8+
{
9+
/// <summary>
10+
/// This interface is used to implement a basic caching for typosquatting checklist.
11+
/// /// </summary>
12+
public interface ITyposquattingCheckListCacheService
13+
{
14+
/// <summary>
15+
/// The function is used to get the checklist from the cache for typosquatting
16+
/// </summary>
17+
/// <param name="checkListLength">The length of the checklist for typosquatting</param>
18+
/// <param name="checkListExpireTime">The expire time for checklist caching</param>
19+
/// <param name="packageService">The package service for checklist retrieval from database</param>
20+
IReadOnlyCollection<string> GetTyposquattingCheckList(int checkListLength, TimeSpan checkListExpireTime, IPackageService packageService);
21+
}
22+
}

src/NuGetGallery/Services/ITyposquattingConfiguration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ public interface ITyposquattingConfiguration
88
int PackageIdChecklistLength { get; }
99
bool IsCheckEnabled { get; }
1010
bool IsBlockUsersEnabled { get; }
11+
double PackageIdChecklistCacheExpireTimeInHours { get; }
1112
}
1213
}

src/NuGetGallery/Services/TelemetryService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ internal class Events
150150
public const string CollisionPackageIdsCount = "CollisionPackageIdsCount";
151151
public const string CheckListLength = "CheckListLength";
152152
public const string HasExtraCollisionPackageIds = "HasExtraCollisionPackageIds";
153+
public const string CheckListCacheExpireTimeInHours = "CheckListCacheExpireTimeInHours";
153154

154155
public TelemetryService(IDiagnosticsService diagnosticsService, ITelemetryClient telemetryClient = null)
155156
{
@@ -722,15 +723,17 @@ public void TrackMetricForTyposquattingCheckResultAndTotalTime(
722723
TimeSpan totalTime,
723724
bool wasUploadBlocked,
724725
List<string> collisionPackageIds,
725-
int checklistLength)
726+
int checkListLength,
727+
TimeSpan checkListCacheExpireTime)
726728
{
727729
TrackMetric(Events.TyposquattingCheckResultAndTotalTimeInMs, totalTime.TotalMilliseconds, properties => {
728730
properties.Add(PackageId, packageId);
729731
properties.Add(WasUploadBlocked, wasUploadBlocked.ToString());
730732
properties.Add(CollisionPackageIds, string.Join(",", collisionPackageIds.Take(TyposquattingCollisionIdsMaxPropertyValue)));
731733
properties.Add(CollisionPackageIdsCount, collisionPackageIds.Count.ToString());
732-
properties.Add(CheckListLength, checklistLength.ToString());
734+
properties.Add(CheckListLength, checkListLength.ToString());
733735
properties.Add(HasExtraCollisionPackageIds, (collisionPackageIds.Count > TyposquattingCollisionIdsMaxPropertyValue).ToString());
736+
properties.Add(CheckListCacheExpireTimeInHours, checkListCacheExpireTime.ToString());
734737
});
735738
}
736739

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
8+
namespace NuGetGallery
9+
{
10+
public class TyposquattingCheckListCacheService : ITyposquattingCheckListCacheService
11+
{
12+
private readonly object Locker = new object();
13+
14+
private List<string> Cache;
15+
private DateTime LastRefreshTime;
16+
17+
private int TyposquattingCheckListConfiguredLength;
18+
19+
public TyposquattingCheckListCacheService()
20+
{
21+
TyposquattingCheckListConfiguredLength = -1;
22+
LastRefreshTime = DateTime.MinValue;
23+
}
24+
25+
public IReadOnlyCollection<string> GetTyposquattingCheckList(int checkListConfiguredLength, TimeSpan checkListExpireTime, IPackageService packageService)
26+
{
27+
if (packageService == null)
28+
{
29+
throw new ArgumentNullException(nameof(packageService));
30+
}
31+
if (checkListConfiguredLength < 0)
32+
{
33+
throw new ArgumentOutOfRangeException(nameof(checkListConfiguredLength), "Negative values are not supported.");
34+
}
35+
if (checkListExpireTime.CompareTo(TimeSpan.Zero) < 0)
36+
{
37+
throw new ArgumentOutOfRangeException(nameof(checkListExpireTime), "Negative values are not supported.");
38+
}
39+
40+
if (ShouldCacheBeUpdated(checkListConfiguredLength, checkListExpireTime))
41+
{
42+
lock (Locker)
43+
{
44+
if (ShouldCacheBeUpdated(checkListConfiguredLength, checkListExpireTime))
45+
{
46+
TyposquattingCheckListConfiguredLength = checkListConfiguredLength;
47+
48+
Cache = packageService.GetAllPackageRegistrations()
49+
.OrderByDescending(pr => pr.IsVerified)
50+
.ThenByDescending(pr => pr.DownloadCount)
51+
.Select(pr => pr.Id)
52+
.Take(TyposquattingCheckListConfiguredLength)
53+
.ToList();
54+
55+
LastRefreshTime = DateTime.UtcNow;
56+
}
57+
}
58+
}
59+
60+
return Cache;
61+
}
62+
63+
private bool ShouldCacheBeUpdated(int checkListConfiguredLength, TimeSpan checkListExpireTime)
64+
{
65+
return Cache == null || checkListConfiguredLength != TyposquattingCheckListConfiguredLength || IsCheckListCacheExpired(checkListExpireTime);
66+
}
67+
68+
private bool IsCheckListCacheExpired(TimeSpan checkListExpireTime)
69+
{
70+
return DateTime.UtcNow >= LastRefreshTime.Add(checkListExpireTime);
71+
}
72+
}
73+
}

src/NuGetGallery/Services/TyposquattingConfiguration.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,31 @@ namespace NuGetGallery.Services
88
public sealed class TyposquattingConfiguration : ITyposquattingConfiguration
99
{
1010
private const int DefaultPackageIdCheckListLength = 20000;
11+
private const double DefaultPackageIdChecklistCacheExpireTimeInHours = 24;
1112
public int PackageIdChecklistLength { get; }
1213
public bool IsCheckEnabled { get; }
1314
public bool IsBlockUsersEnabled { get; }
15+
public double PackageIdChecklistCacheExpireTimeInHours { get; }
16+
1417
public TyposquattingConfiguration()
1518
: this(packageIdChecklistLength: DefaultPackageIdCheckListLength,
1619
isCheckEnabled: false,
17-
isBlockUsersEnabled: false)
20+
isBlockUsersEnabled: false,
21+
packageIdChecklistCacheExpireTimeInHours: DefaultPackageIdChecklistCacheExpireTimeInHours)
1822
{
1923
}
2024

2125
[JsonConstructor]
2226
public TyposquattingConfiguration(
2327
int packageIdChecklistLength,
2428
bool isCheckEnabled,
25-
bool isBlockUsersEnabled)
29+
bool isBlockUsersEnabled,
30+
double packageIdChecklistCacheExpireTimeInHours)
2631
{
2732
PackageIdChecklistLength = packageIdChecklistLength;
2833
IsCheckEnabled = isCheckEnabled;
2934
IsBlockUsersEnabled = isBlockUsersEnabled;
35+
PackageIdChecklistCacheExpireTimeInHours = packageIdChecklistCacheExpireTimeInHours;
3036
}
3137
}
3238
}

src/NuGetGallery/Services/TyposquattingService.cs

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,86 +20,84 @@ public class TyposquattingService : ITyposquattingService
2020
new ThresholdInfo (lowerBound: 50, upperBound: 129, threshold: 2)
2121
};
2222

23-
private static int TyposquattingCheckListLength;
24-
2523
private readonly IContentObjectService _contentObjectService;
2624
private readonly IPackageService _packageService;
2725
private readonly IReservedNamespaceService _reservedNamespaceService;
2826
private readonly ITelemetryService _telemetryService;
27+
private readonly ITyposquattingCheckListCacheService _typosquattingCheckListCacheService;
2928

30-
public TyposquattingService(IContentObjectService contentObjectService, IPackageService packageService, IReservedNamespaceService reservedNamespaceService, ITelemetryService telemetryService)
29+
public TyposquattingService(IContentObjectService contentObjectService,
30+
IPackageService packageService,
31+
IReservedNamespaceService reservedNamespaceService,
32+
ITelemetryService telemetryService,
33+
ITyposquattingCheckListCacheService typosquattingCheckListCacheService)
3134
{
3235
_contentObjectService = contentObjectService ?? throw new ArgumentNullException(nameof(contentObjectService));
3336
_packageService = packageService ?? throw new ArgumentNullException(nameof(packageService));
3437
_reservedNamespaceService = reservedNamespaceService ?? throw new ArgumentNullException(nameof(reservedNamespaceService));
3538
_telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
36-
37-
TyposquattingCheckListLength = _contentObjectService.TyposquattingConfiguration.PackageIdChecklistLength;
39+
_typosquattingCheckListCacheService = typosquattingCheckListCacheService ?? throw new ArgumentNullException(nameof(typosquattingCheckListCacheService));
3840
}
3941

4042
public bool IsUploadedPackageIdTyposquatting(string uploadedPackageId, User uploadedPackageOwner, out List<string> typosquattingCheckCollisionIds)
4143
{
44+
var checkListConfiguredLength = _contentObjectService.TyposquattingConfiguration.PackageIdChecklistLength;
45+
var checkListExpireTimeInHours = TimeSpan.FromHours(_contentObjectService.TyposquattingConfiguration.PackageIdChecklistCacheExpireTimeInHours);
4246
typosquattingCheckCollisionIds = new List<string>();
4347
var wasUploadBlocked = false;
48+
4449
if (!_contentObjectService.TyposquattingConfiguration.IsCheckEnabled || _reservedNamespaceService.GetReservedNamespacesForId(uploadedPackageId).Any())
4550
{
4651
return wasUploadBlocked;
4752
}
48-
4953
if (uploadedPackageId == null)
5054
{
5155
throw new ArgumentNullException(nameof(uploadedPackageId));
5256
}
53-
5457
if (uploadedPackageOwner == null)
5558
{
5659
throw new ArgumentNullException(nameof(uploadedPackageOwner));
5760
}
5861

62+
var totalTimeStopwatch = Stopwatch.StartNew();
5963
var checklistRetrievalStopwatch = Stopwatch.StartNew();
60-
var packageRegistrations = _packageService.GetAllPackageRegistrations();
61-
var packagesCheckList = packageRegistrations
62-
.OrderByDescending(pr => pr.IsVerified)
63-
.ThenByDescending(pr => pr.DownloadCount)
64-
.Select(pr => pr.Id)
65-
.Take(TyposquattingCheckListLength)
66-
.ToList();
64+
var packageIdsCheckList = _typosquattingCheckListCacheService.GetTyposquattingCheckList(checkListConfiguredLength, checkListExpireTimeInHours, _packageService);
6765
checklistRetrievalStopwatch.Stop();
6866

67+
_telemetryService.TrackMetricForTyposquattingChecklistRetrievalTime(uploadedPackageId, checklistRetrievalStopwatch.Elapsed);
68+
6969
var algorithmProcessingStopwatch = Stopwatch.StartNew();
7070
var threshold = GetThreshold(uploadedPackageId);
7171
var normalizedUploadedPackageId = TyposquattingStringNormalization.NormalizeString(uploadedPackageId);
72-
7372
var collisionIds = new ConcurrentBag<string>();
74-
Parallel.ForEach(packagesCheckList, (packageId, loopState) =>
73+
Parallel.ForEach(packageIdsCheckList, (packageId, loopState) =>
7574
{
7675
string normalizedPackageId = TyposquattingStringNormalization.NormalizeString(packageId);
7776
if (TyposquattingDistanceCalculation.IsDistanceLessThanThreshold(normalizedUploadedPackageId, normalizedPackageId, threshold))
7877
{
7978
collisionIds.Add(packageId);
8079
}
8180
});
82-
8381
algorithmProcessingStopwatch.Stop();
8482

85-
var totalTime = checklistRetrievalStopwatch.Elapsed.Add(algorithmProcessingStopwatch.Elapsed);
86-
_telemetryService.TrackMetricForTyposquattingChecklistRetrievalTime(uploadedPackageId, checklistRetrievalStopwatch.Elapsed);
8783
_telemetryService.TrackMetricForTyposquattingAlgorithmProcessingTime(uploadedPackageId, algorithmProcessingStopwatch.Elapsed);
8884

8985
if (collisionIds.Count == 0)
9086
{
87+
totalTimeStopwatch.Stop();
9188
_telemetryService.TrackMetricForTyposquattingCheckResultAndTotalTime(
9289
uploadedPackageId,
93-
totalTime,
90+
totalTimeStopwatch.Elapsed,
9491
wasUploadBlocked,
9592
typosquattingCheckCollisionIds,
96-
TyposquattingCheckListLength);
93+
packageIdsCheckList.Count,
94+
checkListExpireTimeInHours);
9795

9896
return false;
9997
}
10098

10199
var ownersCheckStopwatch = Stopwatch.StartNew();
102-
var collisionPackagesIdAndOwners = packageRegistrations
100+
var collisionPackagesIdAndOwners = _packageService.GetAllPackageRegistrations()
103101
.Where(pr => collisionIds.Contains(pr.Id))
104102
.Select(pr => new { Id = pr.Id, Owners = pr.Owners.Select(x => x.Key).ToList() })
105103
.ToList();
@@ -122,21 +120,21 @@ public bool IsUploadedPackageIdTyposquatting(string uploadedPackageId, User uplo
122120
.Any(pio => pio.Owners.Any(k => k == uploadedPackageOwner.Key));
123121

124122
wasUploadBlocked = _contentObjectService.TyposquattingConfiguration.IsBlockUsersEnabled && !isUserAllowedTyposquatting;
125-
126123
ownersCheckStopwatch.Stop();
127124

128-
totalTime = totalTime.Add(ownersCheckStopwatch.Elapsed);
129125
_telemetryService.TrackMetricForTyposquattingOwnersCheckTime(uploadedPackageId, ownersCheckStopwatch.Elapsed);
126+
127+
totalTimeStopwatch.Stop();
130128
_telemetryService.TrackMetricForTyposquattingCheckResultAndTotalTime(
131129
uploadedPackageId,
132-
totalTime,
130+
totalTimeStopwatch.Elapsed,
133131
wasUploadBlocked,
134132
typosquattingCheckCollisionIds,
135-
TyposquattingCheckListLength);
133+
packageIdsCheckList.Count,
134+
checkListExpireTimeInHours);
136135

137136
return wasUploadBlocked;
138137
}
139-
140138
private static int GetThreshold(string packageId)
141139
{
142140
foreach (var thresholdInfo in ThresholdsList)
@@ -150,7 +148,6 @@ private static int GetThreshold(string packageId)
150148
throw new ArgumentException(String.Format("There is no predefined typo-squatting threshold for this package Id: {0}", packageId));
151149
}
152150
}
153-
154151
public class ThresholdInfo
155152
{
156153
public int LowerBound { get; }

tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
<Compile Include="Services\ActionRequiringEntityPermissionsFacts.cs" />
158158
<Compile Include="Services\CertificatesConfigurationFacts.cs" />
159159
<Compile Include="Services\CuratedFeedServiceFacts.cs" />
160+
<Compile Include="Services\TyposquattingCheckListCacheServiceFacts.cs" />
160161
<Compile Include="Services\TyposquattingServiceFacts.cs" />
161162
<Compile Include="Services\CloudDownloadCountServiceFacts.cs" />
162163
<Compile Include="Services\CertificateServiceFacts.cs" />

0 commit comments

Comments
 (0)