Skip to content

Commit c4a6498

Browse files
authored
[NuGet Symbol Server] Gallery upload - Refactoring (#6478)
1 parent 704ee0b commit c4a6498

16 files changed

Lines changed: 747 additions & 507 deletions

src/NuGetGallery.Core/NuGetGallery.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
<Compile Include="Infrastructure\ElmahException.cs" />
169169
<Compile Include="Infrastructure\TableErrorLog.cs" />
170170
<Compile Include="Authentication\NuGetClaims.cs" />
171+
<Compile Include="PackageMetadataExtensions.cs" />
171172
<Compile Include="PackageReaderCoreExtensions.cs" />
172173
<Compile Include="PackageStatus.cs" />
173174
<Compile Include="Packaging\InvalidPackageException.cs" />
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 NuGetGallery.Packaging;
5+
using System.Linq;
6+
using ClientPackageType = NuGet.Packaging.Core.PackageType;
7+
8+
namespace NuGetGallery
9+
{
10+
/// <summary>
11+
/// Utilities extending PackageMetadata
12+
/// </summary>
13+
public static class PackageMetadataExtensions
14+
{
15+
private const string SymbolPackageTypeName = "SymbolsPackage";
16+
private static readonly ClientPackageType SymbolPackageType = new ClientPackageType(SymbolPackageTypeName, ClientPackageType.EmptyVersion);
17+
18+
/// <summary>
19+
/// The package is a symbol package, if and only if it has metadata
20+
/// element of type <see cref="SymbolPackageTypeName"/> and only that element in package types.
21+
/// </summary>
22+
/// <param name="metadata"><see cref="PackageMetadata"/> for package</param>
23+
/// <returns>True if the package is a symbols package</returns>
24+
public static bool IsSymbolPackage(this PackageMetadata metadata)
25+
{
26+
var packageTypes = metadata.GetPackageTypes();
27+
return packageTypes.Any()
28+
&& packageTypes.Count() == 1
29+
&& packageTypes.First() == SymbolPackageType;
30+
}
31+
}
32+
}

src/NuGetGallery.Core/StreamExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System.IO;
5-
using System.Threading.Tasks;
65

76
namespace NuGetGallery
87
{

src/NuGetGallery/Controllers/ApiController.cs

Lines changed: 76 additions & 137 deletions
Large diffs are not rendered by default.

src/NuGetGallery/Controllers/PackagesController.cs

Lines changed: 124 additions & 110 deletions
Large diffs are not rendered by default.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.IO;
6+
using System.IO.Compression;
7+
using System.Linq;
8+
9+
namespace NuGetGallery
10+
{
11+
public static class ZipArchiveHelpers
12+
{
13+
/// <summary>
14+
/// This method checks all the <see cref="ZipArchiveEntry"/> in a given
15+
/// <see cref="Stream"/> if it has the entry in the future. It will return
16+
/// the first entry found in the future.
17+
/// </summary>
18+
/// <param name="stream"><see cref="Stream"/> object to verify</param>
19+
/// <param name="entry"><see cref="ZipArchiveEntry"/> found with future entry.</param>
20+
/// <returns>True if <see cref="Stream"/> contains an entry in future, false otherwise.</returns>
21+
public static bool FoundEntryInFuture(Stream stream, out ZipArchiveEntry entry)
22+
{
23+
entry = null;
24+
25+
using (var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true))
26+
{
27+
var reference = DateTime.UtcNow.AddDays(1); // allow "some" clock skew
28+
29+
var entryInTheFuture = archive.Entries.FirstOrDefault(
30+
e => e.LastWriteTime.UtcDateTime > reference);
31+
32+
if (entryInTheFuture != null)
33+
{
34+
entry = entryInTheFuture;
35+
return true;
36+
}
37+
}
38+
39+
return false;
40+
}
41+
}
42+
}

src/NuGetGallery/NuGetGallery.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@
292292
<Compile Include="GlobalSuppressions.cs" />
293293
<Compile Include="Helpers\GravatarHelper.cs" />
294294
<Compile Include="Helpers\ObfuscationHelper.cs" />
295+
<Compile Include="Helpers\ZipArchiveHelpers.cs" />
295296
<Compile Include="Helpers\PermissionsHelpers.cs" />
296297
<Compile Include="Helpers\RegexEx.cs" />
297298
<Compile Include="Helpers\RouteUrlTemplate.cs" />
@@ -400,6 +401,7 @@
400401
<Compile Include="Services\ITyposquattingConfiguration.cs" />
401402
<Compile Include="Services\ISymbolsConfiguration.cs" />
402403
<Compile Include="Services\ITyposquattingService.cs" />
404+
<Compile Include="Services\SymbolPackageValidationResult.cs" />
403405
<Compile Include="Services\SymbolPackageUploadService.cs" />
404406
<Compile Include="Services\SymbolPackageFileService.cs" />
405407
<Compile Include="Services\SymbolPackageService.cs" />

src/NuGetGallery/Services/ISymbolPackageUploadService.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,19 @@ public interface ISymbolPackageUploadService
2020
/// cases (such as database failures). This method does not dispose the provided stream but does read it.
2121
/// </summary>
2222
/// <param name="package">The package for which the symbols are to be uploaded.</param>
23-
/// <param name="packageStreamMetadata">The metadata object for the uploaded snupkg.</param>
24-
/// <param name="symbolPackageFile">The seekable stream containing the symbol package content (.snupkg).</param>
23+
/// <param name="symbolPackageStream">The Stream object for the uploaded snupkg.</param>
2524
/// <returns>Awaitable task with <see cref="PackageCommitResult"/></returns>
26-
Task<PackageCommitResult> CreateAndUploadSymbolsPackage(Package package, PackageStreamMetadata packageStreamMetadata, Stream symbolPackageFile);
25+
Task<PackageCommitResult> CreateAndUploadSymbolsPackage(Package package, Stream symbolPackageStream);
26+
27+
/// <summary>
28+
/// Validate the uploaded symbols package. This method will perform all required validations for symbols, including
29+
/// synchronous package validations for fail fast scenario. This method will not perform the ownership validations
30+
/// for the symbols package. It is the responsibility of the caller to perform ownership validations. This method
31+
/// does not dispose the provided stream but does read it.
32+
/// </summary>
33+
/// <param name="uploadStream">The <see cref="Stream"/> object for the uploaded snupkg.</param>
34+
/// <param name="currentUser">The user performing the uploads.</param>
35+
/// <returns>Awaitable task with <see cref="SymbolPackageValidationResult"/></returns>
36+
Task<SymbolPackageValidationResult> ValidateUploadedSymbolsPackage(Stream uploadStream, User currentUser);
2737
}
2838
}

src/NuGetGallery/Services/SymbolPackageService.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using NuGet.Packaging;
1212
using NuGet.Packaging.Core;
1313
using NuGetGallery.Packaging;
14-
using ClientPackageType = NuGet.Packaging.Core.PackageType;
1514

1615
namespace NuGetGallery
1716
{
@@ -26,8 +25,6 @@ public class SymbolPackageService : CoreSymbolPackageService, ISymbolPackageServ
2625
".rels",
2726
".p7s"
2827
};
29-
private const string SymbolPackageTypeName = "SymbolsPackage";
30-
private static readonly ClientPackageType SymbolPackageType = new ClientPackageType(SymbolPackageTypeName, ClientPackageType.EmptyVersion);
3128

3229
public SymbolPackageService(
3330
IEntityRepository<SymbolPackage> symbolPackageRepository,
@@ -62,7 +59,7 @@ public async Task EnsureValidAsync(PackageArchiveReader symbolPackageArchiveRead
6259
symbolPackageArchiveReader.GetNuspecReader(),
6360
strict: true);
6461

65-
if (!IsSymbolPackage(packageMetadata))
62+
if (!packageMetadata.IsSymbolPackage())
6663
{
6764
throw new InvalidPackageException(Strings.SymbolsPackage_NotSymbolPackage);
6865
}
@@ -173,14 +170,6 @@ private static bool CheckForAllowedFiles(PackageArchiveReader symbolPackage)
173170
return true;
174171
}
175172

176-
private static bool IsSymbolPackage(PackageMetadata metadata)
177-
{
178-
var packageTypes = metadata.GetPackageTypes();
179-
return packageTypes.Any()
180-
&& packageTypes.Count() == 1
181-
&& packageTypes.First() == SymbolPackageType;
182-
}
183-
184173
private static bool IsPortable(string pdbFile)
185174
{
186175
byte[] currentPDBStamp = new byte[4];

src/NuGetGallery/Services/SymbolPackageUploadService.cs

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Globalization;
56
using System.IO;
7+
using System.IO.Compression;
68
using System.Linq;
79
using System.Threading.Tasks;
10+
using NuGet.Frameworks;
11+
using NuGet.Packaging;
812
using NuGetGallery.Packaging;
913

1014
namespace NuGetGallery
@@ -15,17 +19,109 @@ public class SymbolPackageUploadService : ISymbolPackageUploadService
1519
private readonly IValidationService _validationService;
1620
private readonly ISymbolPackageService _symbolPackageService;
1721
private readonly ISymbolPackageFileService _symbolPackageFileService;
22+
private readonly IPackageService _packageService;
23+
private readonly ITelemetryService _telemetryService;
24+
private readonly IContentObjectService _contentObjectService;
1825

1926
public SymbolPackageUploadService(
2027
ISymbolPackageService symbolPackageService,
2128
ISymbolPackageFileService symbolPackageFileService,
2229
IEntitiesContext entitiesContext,
23-
IValidationService validationService)
30+
IValidationService validationService,
31+
IPackageService packageService,
32+
ITelemetryService telemetryService,
33+
IContentObjectService contentObjectService)
2434
{
2535
_symbolPackageService = symbolPackageService ?? throw new ArgumentNullException(nameof(symbolPackageService));
2636
_symbolPackageFileService = symbolPackageFileService ?? throw new ArgumentNullException(nameof(symbolPackageFileService));
2737
_entitiesContext = entitiesContext ?? throw new ArgumentNullException(nameof(entitiesContext));
2838
_validationService = validationService ?? throw new ArgumentNullException(nameof(validationService));
39+
_packageService = packageService ?? throw new ArgumentNullException(nameof(packageService));
40+
_telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService));
41+
_contentObjectService = contentObjectService ?? throw new ArgumentNullException(nameof(contentObjectService));
42+
}
43+
44+
/// <summary>
45+
/// This method does not perform the ownership validations for the symbols package. It is the responsibility
46+
/// of the caller to do it. Also, this method does not dispose the <see cref="Stream"/> object, the caller
47+
/// should take care of it.
48+
/// </summary>
49+
/// <param name="symbolPackageStream"><see cref="Stream"/> object for the symbols package.</param>
50+
/// <param name="currentUser">The user performing the uploads.</param>
51+
/// <returns>Awaitable task for <see cref="SymbolPackageValidationResult"/></returns>
52+
public async Task<SymbolPackageValidationResult> ValidateUploadedSymbolsPackage(Stream symbolPackageStream, User currentUser)
53+
{
54+
Package package = null;
55+
56+
// Check if symbol package upload is allowed for this user.
57+
if (!_contentObjectService.SymbolsConfiguration.IsSymbolsUploadEnabledForUser(currentUser))
58+
{
59+
return SymbolPackageValidationResult.UserNotAllowedToUpload(Strings.SymbolsPackage_UploadNotAllowed);
60+
}
61+
62+
try
63+
{
64+
if (ZipArchiveHelpers.FoundEntryInFuture(symbolPackageStream, out ZipArchiveEntry entryInTheFuture))
65+
{
66+
return SymbolPackageValidationResult.Invalid(string.Format(
67+
CultureInfo.CurrentCulture,
68+
Strings.PackageEntryFromTheFuture,
69+
entryInTheFuture.Name));
70+
}
71+
72+
using (var packageToPush = new PackageArchiveReader(symbolPackageStream, leaveStreamOpen: true))
73+
{
74+
var nuspec = packageToPush.GetNuspecReader();
75+
var id = nuspec.GetId();
76+
var version = nuspec.GetVersion();
77+
var normalizedVersion = version.ToNormalizedStringSafe();
78+
79+
// Ensure the corresponding package exists before pushing a snupkg.
80+
package = _packageService.FindPackageByIdAndVersionStrict(id, version.ToStringSafe());
81+
if (package == null || package.PackageStatusKey == PackageStatus.Deleted)
82+
{
83+
return SymbolPackageValidationResult.MissingPackage(string.Format(
84+
CultureInfo.CurrentCulture,
85+
Strings.SymbolsPackage_PackageIdAndVersionNotFound,
86+
id,
87+
normalizedVersion));
88+
}
89+
90+
// Do not allow to upload a snupkg to a package which has symbols package pending validations.
91+
if (package.SymbolPackages.Any(sp => sp.StatusKey == PackageStatus.Validating))
92+
{
93+
return SymbolPackageValidationResult.SymbolsPackageExists(Strings.SymbolsPackage_ConflictValidating);
94+
}
95+
96+
try
97+
{
98+
await _symbolPackageService.EnsureValidAsync(packageToPush);
99+
}
100+
catch (Exception ex)
101+
{
102+
ex.Log();
103+
104+
var message = Strings.SymbolsPackage_FailedToReadPackage;
105+
if (ex is InvalidPackageException || ex is InvalidDataException || ex is EntityException)
106+
{
107+
message = ex.Message;
108+
}
109+
110+
_telemetryService.TrackSymbolPackageFailedGalleryValidationEvent(id, normalizedVersion);
111+
return SymbolPackageValidationResult.Invalid(message);
112+
}
113+
}
114+
115+
return SymbolPackageValidationResult.AcceptedForPackage(package);
116+
}
117+
catch (Exception ex) when (ex is InvalidPackageException
118+
|| ex is InvalidDataException
119+
|| ex is EntityException
120+
|| ex is FrameworkException)
121+
{
122+
return SymbolPackageValidationResult.Invalid(
123+
string.Format(CultureInfo.CurrentCulture, Strings.UploadPackage_InvalidPackage, ex.Message));
124+
}
29125
}
30126

31127
/// <summary>
@@ -34,11 +130,21 @@ public SymbolPackageUploadService(
34130
/// based on the result. It will then update the references in the database for persistence with appropriate status.
35131
/// </summary>
36132
/// <param name="package">The package for which symbols package is to be uplloaded</param>
37-
/// <param name="packageStreamMetadata">The package stream metadata for the uploaded symbols package file.</param>
38-
/// <param name="symbolPackageFile">The symbol package file stream.</param>
39-
/// <returns>The <see cref="PackageCommitResult"/> for the symbol package upload flow.</returns>
40-
public async Task<PackageCommitResult> CreateAndUploadSymbolsPackage(Package package, PackageStreamMetadata packageStreamMetadata, Stream symbolPackageFile)
133+
/// <param name="symbolPackageStream">The symbols package stream metadata for the uploaded symbols package file.</param>
134+
/// <returns>The <see cref="PackageCommitResult"/> for the create and upload symbol package flow.</returns>
135+
public async Task<PackageCommitResult> CreateAndUploadSymbolsPackage(Package package, Stream symbolPackageStream)
41136
{
137+
var packageStreamMetadata = new PackageStreamMetadata
138+
{
139+
HashAlgorithm = CoreConstants.Sha512HashAlgorithmId,
140+
Hash = CryptographyService.GenerateHash(
141+
symbolPackageStream.AsSeekableStream(),
142+
CoreConstants.Sha512HashAlgorithmId),
143+
Size = symbolPackageStream.Length
144+
};
145+
146+
Stream symbolPackageFile = symbolPackageStream.AsSeekableStream();
147+
42148
var symbolPackage = _symbolPackageService.CreateSymbolPackage(package, packageStreamMetadata);
43149

44150
await _validationService.StartValidationAsync(symbolPackage);
@@ -66,7 +172,7 @@ public async Task<PackageCommitResult> CreateAndUploadSymbolsPackage(Package pac
66172
// Mark any other associated available symbol packages for deletion.
67173
var availableSymbolPackages = package
68174
.SymbolPackages
69-
.Where(sp => sp.StatusKey == PackageStatus.Available
175+
.Where(sp => sp.StatusKey == PackageStatus.Available
70176
&& sp != symbolPackage);
71177

72178
var overwrite = false;
@@ -120,6 +226,8 @@ await _symbolPackageFileService.DeletePackageFileAsync(
120226
return PackageCommitResult.Conflict;
121227
}
122228

229+
_telemetryService.TrackSymbolPackagePushEvent(package.Id, package.NormalizedVersion);
230+
123231
return PackageCommitResult.Success;
124232
}
125233
}

0 commit comments

Comments
 (0)