├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── Feature_request.md │ └── Bug_report.md ├── src ├── NugetProxy.Core │ ├── Entities │ │ ├── TargetFramework.cs │ │ ├── SemVerLevel.cs │ │ ├── PackageType.cs │ │ ├── PackageDependency.cs │ │ └── Package.cs │ ├── Mirror │ │ ├── IPackageDownloadsSource.cs │ │ ├── IMirrorService.cs │ │ └── PackageDownloadsJsonSource.cs │ ├── readme.md │ ├── Metadata │ │ ├── BaGetRegistrationLeafResponse.cs │ │ ├── BaGetRegistrationIndexResponse.cs │ │ └── BaGetPackageMetadata.cs │ ├── Extensions │ │ ├── ServiceCollectionExtensions.cs │ │ └── StreamExtensions.cs │ ├── Search │ │ ├── DependentsResponse.cs │ │ ├── NullSearchService.cs │ │ └── ISearchService.cs │ ├── ServiceIndex │ │ ├── IServiceIndexService.cs │ │ └── BaGetServiceIndex.cs │ ├── NugetProxy.Core.csproj │ ├── Configuration │ │ └── MirrorOptions.cs │ ├── Validation │ │ ├── ValidatePostConfigureOptions.cs │ │ └── RequiredIfAttribute.cs │ ├── Content │ │ ├── DatabasePackageContentService.cs │ │ └── IPackageContentService.cs │ └── IUrlGenerator.cs ├── NugetProxy.Protocol │ ├── Models │ │ ├── RegistrationPageResponse.cs │ │ ├── AutocompleteContext.cs │ │ ├── AutocompleteType.cs │ │ ├── SearchContext.cs │ │ ├── PackageDeleteCatalogLeaf.cs │ │ ├── PackageVersionsResponse.cs │ │ ├── ServiceIndexResponse.cs │ │ ├── DependencyItem.cs │ │ ├── CatalogLeafType.cs │ │ ├── SearchResponse.cs │ │ ├── ServiceIndexItem.cs │ │ ├── DependencyGroupItem.cs │ │ ├── AutocompleteResponse.cs │ │ ├── RegistrationIndexPageItem.cs │ │ ├── SearchResultVersion.cs │ │ ├── ICatalogLeafItem.cs │ │ ├── CatalogPageItem.cs │ │ ├── CatalogIndex.cs │ │ ├── CatalogPage.cs │ │ ├── RegistrationIndexResponse.cs │ │ ├── CatalogLeafItem.cs │ │ ├── RegistrationIndexPage.cs │ │ ├── RegistrationLeafResponse.cs │ │ ├── CatalogLeaf.cs │ │ ├── SearchResult.cs │ │ ├── PackageMetadata.cs │ │ └── PackageDetailsCatalogLeaf.cs │ ├── NugetProxy.Protocol.csproj │ ├── ServiceIndex │ │ ├── IServiceIndexClient.cs │ │ └── ServiceIndexClient.cs │ ├── Catalog │ │ ├── NullCursor.cs │ │ ├── CatalogProcessorOptions.cs │ │ ├── ICursor.cs │ │ ├── CatalogClient.cs │ │ ├── FileCursor.cs │ │ ├── ICatalogLeafProcessor.cs │ │ ├── RawCatalogClient.cs │ │ └── ICatalogClient.cs │ ├── Extensions │ │ ├── PackageContentModelExtensions.cs │ │ ├── SearchModelExtensions.cs │ │ ├── NuGetClientFactoryExtensions.cs │ │ ├── HttpClientExtensions.cs │ │ ├── PackageMetadataModelExtensions.cs │ │ └── ServiceIndexModelExtensions.cs │ ├── Search │ │ ├── SearchClient.cs │ │ ├── ISearchClient.cs │ │ └── RawSearchClient.cs │ ├── Converters │ │ ├── PackageDependencyRangeConverter.cs │ │ ├── SingleOrListConverter.cs │ │ ├── BaseCatalogLeafConverter.cs │ │ ├── CatalogLeafItemTypeConverter.cs │ │ └── CatalogLeafTypeConverter.cs │ ├── PackageNotFoundException.cs │ ├── ResponseAndResult.cs │ ├── PackageMetadata │ │ ├── PackageMetadataClient.cs │ │ ├── IPackageMetadataClient.cs │ │ └── RawPackageMetadataClient.cs │ ├── PackageContent │ │ ├── PackageContentClient.cs │ │ ├── IPackageContentClient.cs │ │ └── RawPackageContentClient.cs │ └── ProtocolException.cs ├── NugetProxy │ ├── appsettings.json │ ├── NugetProxy.csproj │ ├── Properties │ │ └── launchSettings.json │ ├── Dockerfile │ ├── Program.cs │ ├── Startup.cs │ └── Extensions │ │ ├── IHostBuilderExtensions.cs │ │ └── IServiceCollectionExtensions.cs ├── NugetProxy.Core.Server │ ├── NugetProxy.Core.Server.csproj │ ├── Configuration │ │ └── ConfigureForwardedHeadersOptions.cs │ ├── Controllers │ │ ├── ServiceIndexController.cs │ │ ├── SearchController.cs │ │ └── PackageContentController.cs │ ├── Routes.cs │ ├── Extensions │ │ ├── IServiceCollectionExtensions.cs │ │ ├── HttpRequestExtensions.cs │ │ └── IRouteBuilderExtensions.cs │ └── BaGetUrlGenerator.cs └── Directory.Build.props ├── .vscode ├── tasks.json └── launch.json ├── .dockerignore ├── .azure └── pipelines │ └── ci-official.yml ├── LICENSE ├── .gitattributes ├── readme.md ├── .editorconfig ├── NugetProxy.sln └── .gitignore /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Summary of the changes (in less than 80 chars) 2 | 3 | * Detail 1 4 | * Detail 2 5 | 6 | Addresses https://github.com/loic-sharma/NugetProxy/issues/123 7 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Entities/TargetFramework.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy.Core 2 | { 3 | public class TargetFramework 4 | { 5 | public int Key { get; set; } 6 | 7 | public string Moniker { get; set; } 8 | 9 | public Package Package { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Mirror/IPackageDownloadsSource.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace NugetProxy.Core 5 | { 6 | public interface IPackageDownloadsSource 7 | { 8 | Task>> GetPackageDownloadsAsync(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/readme.md: -------------------------------------------------------------------------------- 1 | # NugetProxy.Core 2 | 3 | Contains NugetProxy's core logic. The important folders are: 4 | 5 | * Indexing - Logic to upload packages and symbols 6 | * Metadata - Used to track packages' metadata, usually in a database 7 | * Storage - Stores the content of packages and symbols 8 | * Search - Used to search for packages 9 | * Mirror - Interactions with an upstream NuGet feed 10 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/RegistrationPageResponse.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy.Protocol.Models 2 | { 3 | /// 4 | /// A page of package metadata entries. 5 | /// 6 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page 7 | /// 8 | public class RegistrationPageResponse : RegistrationIndexPage 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "taskName": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/NugetProxy/NugetProxy.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/NugetProxy/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Mirror": { 3 | "Enabled": true, 4 | "PackageSource": "", 5 | "AccessToken": "" 6 | }, 7 | 8 | "Logging": { 9 | "IncludeScopes": false, 10 | "Debug": { 11 | "LogLevel": { 12 | "Default": "Warning" 13 | } 14 | }, 15 | "Console": { 16 | "LogLevel": { 17 | "Default": "Warning" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Entities/SemVerLevel.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy.Core 2 | { 3 | public enum SemVerLevel 4 | { 5 | /// 6 | /// Either an invalid semantic version or a semantic version v1.0.0 7 | /// 8 | Unknown = 0, 9 | 10 | /// 11 | /// A valid semantic version v2.0.0 12 | /// 13 | SemVer2 = 2 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /src/NugetProxy.Core/Entities/PackageType.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy.Core 2 | { 3 | // See NuGetGallery.Core's: https://github.com/NuGet/NuGetGallery/blob/master/src/NuGetGallery.Core/Entities/PackageType.cs 4 | public class PackageType 5 | { 6 | public int Key { get; set; } 7 | 8 | public string Name { get; set; } 9 | public string Version { get; set; } 10 | 11 | public Package Package { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/AutocompleteContext.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace NugetProxy.Protocol.Models 4 | { 5 | public class AutocompleteContext 6 | { 7 | public static readonly AutocompleteContext Default = new AutocompleteContext 8 | { 9 | Vocab = "http://schema.nuget.org/schema#" 10 | }; 11 | 12 | [JsonProperty("@vocab")] 13 | public string Vocab { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Entities/PackageDependency.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy.Core 2 | { 3 | // See NuGetGallery.Core's: https://github.com/NuGet/NuGetGallery/blob/master/src/NuGetGallery.Core/Entities/PackageDependency.cs 4 | public class PackageDependency 5 | { 6 | public int Key { get; set; } 7 | 8 | public string Id { get; set; } 9 | public string VersionRange { get; set; } 10 | public string TargetFramework { get; set; } 11 | 12 | public Package Package { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Metadata/BaGetRegistrationLeafResponse.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Protocol.Models; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Core 5 | { 6 | /// 7 | /// NugetProxy's extensions to a registration leaf response. These additions 8 | /// are not part of the official protocol. 9 | /// 10 | public class NugetProxyRegistrationLeafResponse : RegistrationLeafResponse 11 | { 12 | [JsonProperty("downloads")] 13 | public long Downloads { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/AutocompleteType.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy.Protocol.Models 2 | { 3 | /// 4 | /// The autocomplete request type. 5 | /// 6 | /// See https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource 7 | /// 8 | public enum AutocompleteType 9 | { 10 | /// 11 | /// The response's data should list matching package IDs. 12 | /// 13 | PackageIds, 14 | 15 | /// 16 | /// The response should list the package's versions. 17 | /// 18 | PackageVersions, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/SearchContext.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace NugetProxy.Protocol.Models 4 | { 5 | public class SearchContext 6 | { 7 | public static SearchContext Default(string registrationBaseUrl) 8 | { 9 | return new SearchContext 10 | { 11 | Vocab = "http://schema.nuget.org/schema#", 12 | Base = registrationBaseUrl 13 | }; 14 | } 15 | 16 | [JsonProperty("@vocab")] 17 | public string Vocab { get; set; } 18 | 19 | [JsonProperty("@base")] 20 | public string Base { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ### Is your feature request related to a problem? Please describe. 7 | A clear and concise description of what the problem is. 8 | Example. I'm am trying to do [...] but [...] 9 | 10 | ### Describe the solution you'd like 11 | A clear and concise description of what you want to happen. 12 | 13 | ### Describe alternatives you've considered 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | ### Additional context 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report about something that is not working 4 | --- 5 | 6 | ### Describe the bug 7 | A clear and concise description of what the bug is. 8 | 9 | ### To Reproduce 10 | Steps to reproduce the behavior: 11 | 1. Using this version of NugetProxy '...' 12 | 2. Run this code '....' 13 | 3. With these arguments '....' 14 | 4. See error 15 | 16 | ### Expected behavior 17 | A clear and concise description of what you expected to happen. 18 | 19 | ### Screenshots 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | ### Additional context 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Metadata/BaGetRegistrationIndexResponse.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Protocol.Models; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Core 5 | { 6 | /// 7 | /// NugetProxy's extensions to a registration index response. These additions 8 | /// are not part of the official protocol. 9 | /// 10 | public class NugetProxyRegistrationIndexResponse : RegistrationIndexResponse 11 | { 12 | /// 13 | /// How many times all versions of this package have been downloaded. 14 | /// 15 | [JsonProperty("totalDownloads")] 16 | public long TotalDownloads { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/PackageDeleteCatalogLeaf.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy.Protocol.Models 2 | { 3 | // This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/PackageDeleteCatalogLeaf.cs 4 | 5 | /// 6 | /// A "package delete" catalog leaf. Represents a single package deletion event. 7 | /// Leafs can be discovered from a . 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-leaf 10 | /// 11 | public class PackageDeleteCatalogLeaf : CatalogLeaf 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/PackageVersionsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// The full list of versions for a single package. 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions 10 | /// 11 | public class PackageVersionsResponse 12 | { 13 | /// 14 | /// The versions, lowercased and normalized. 15 | /// 16 | [JsonProperty("versions")] 17 | public IReadOnlyList Versions { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/NugetProxy.Core.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | BaGet's NuGet server implementation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/Configuration/ConfigureForwardedHeadersOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.HttpOverrides; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace NugetProxy.Configuration 6 | { 7 | public class ConfigureForwardedHeadersOptions : IConfigureOptions 8 | { 9 | public void Configure(ForwardedHeadersOptions options) 10 | { 11 | options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; 12 | 13 | // Do not restrict to local network/proxy 14 | options.KnownNetworks.Clear(); 15 | options.KnownProxies.Clear(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace NugetProxy.Core 6 | { 7 | public static class ServiceCollectionExtensions 8 | { 9 | public static IServiceCollection ConfigureAndValidate( 10 | this IServiceCollection services, 11 | IConfiguration config) 12 | where TOptions : class 13 | { 14 | services.Configure(config); 15 | services.AddSingleton, ValidatePostConfigureOptions>(); 16 | 17 | return services; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/NugetProxy.Protocol.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | NuGet;Protocol 7 | Libraries to interact with NuGet server APIs. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Search/DependentsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Core 5 | { 6 | /// 7 | /// The package ids that depend on the queried package. 8 | /// This is an unofficial API that isn't part of the NuGet protocol. 9 | /// 10 | public class DependentsResponse 11 | { 12 | /// 13 | /// The total number of matches, disregarding skip and take. 14 | /// 15 | [JsonProperty("totalHits")] 16 | public long TotalHits { get; set; } 17 | 18 | /// 19 | /// The package IDs matched by the dependent query. 20 | /// 21 | [JsonProperty("data")] 22 | public IReadOnlyList Data { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/ServiceIndex/IServiceIndexService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NugetProxy.Protocol.Models; 4 | 5 | namespace NugetProxy.Core 6 | { 7 | /// 8 | /// The NuGet Service Index service, used to discover other resources. 9 | /// 10 | /// See https://docs.microsoft.com/en-us/nuget/api/service-index 11 | /// 12 | public interface IServiceIndexService 13 | { 14 | /// 15 | /// Get the resources available on this package feed. 16 | /// See: https://docs.microsoft.com/en-us/nuget/api/service-index#resources 17 | /// 18 | /// The resources available on this package feed. 19 | Task GetAsync(CancellationToken cancellationToken = default); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/ServiceIndex/IServiceIndexClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NugetProxy.Protocol.Models; 4 | 5 | namespace NugetProxy.Protocol 6 | { 7 | /// 8 | /// The NuGet Service Index client, used to discover other resources. 9 | /// 10 | /// See https://docs.microsoft.com/en-us/nuget/api/service-index 11 | /// 12 | public interface IServiceIndexClient 13 | { 14 | /// 15 | /// Get the resources available on this package feed. 16 | /// See: https://docs.microsoft.com/en-us/nuget/api/service-index#resources 17 | /// 18 | /// The resources available on this package feed. 19 | Task GetAsync(CancellationToken cancellationToken = default); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/ServiceIndexResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// The entry point for a NuGet package source used by the client to discover NuGet APIs. 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/overview 10 | /// 11 | public class ServiceIndexResponse 12 | { 13 | /// 14 | /// The service index's version. 15 | /// 16 | [JsonProperty("version")] 17 | public string Version { get; set; } 18 | 19 | /// 20 | /// The resources declared by this service index. 21 | /// 22 | [JsonProperty("resources")] 23 | public IReadOnlyList Resources { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Catalog/NullCursor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace NugetProxy.Protocol.Catalog 6 | { 7 | /// 8 | /// A cursor that does not persist any state. Use this with a 9 | /// to process all leafs each time 10 | /// is called. 11 | /// 12 | public class NullCursor : ICursor 13 | { 14 | public Task GetAsync(CancellationToken cancellationToken = default) 15 | { 16 | return Task.FromResult(null); 17 | } 18 | 19 | public Task SetAsync(DateTimeOffset value, CancellationToken cancellationToken = default) 20 | { 21 | return Task.CompletedTask; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/DependencyItem.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Protocol.Internal; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// Represents a package dependency. 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency 10 | /// 11 | public class DependencyItem 12 | { 13 | /// 14 | /// The ID of the package dependency. 15 | /// 16 | [JsonProperty("id")] 17 | public string Id { get; set; } 18 | 19 | /// 20 | /// The allowed version range of the dependency. 21 | /// 22 | [JsonProperty("range")] 23 | [JsonConverter(typeof(PackageDependencyRangeConverter))] 24 | public string Range { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/CatalogLeafType.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy.Protocol.Models 2 | { 3 | // This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/CatalogLeafType.cs 4 | 5 | /// 6 | /// The type of a . 7 | /// 8 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#item-types 9 | /// 10 | public enum CatalogLeafType 11 | { 12 | /// 13 | /// The represents the snapshot of a package's metadata. 14 | /// 15 | PackageDetails = 1, 16 | 17 | /// 18 | /// The represents a package that was deleted. 19 | /// 20 | PackageDelete = 2, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/SearchResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// The response to a search query. 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response 10 | /// 11 | public class SearchResponse 12 | { 13 | [JsonProperty("@context")] 14 | public SearchContext Context { get; set; } 15 | 16 | /// 17 | /// The total number of matches, disregarding skip and take. 18 | /// 19 | [JsonProperty("totalHits")] 20 | public long TotalHits { get; set; } 21 | 22 | /// 23 | /// The packages that matched the search query. 24 | /// 25 | [JsonProperty("data")] 26 | public IReadOnlyList Data { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/ServiceIndexItem.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace NugetProxy.Protocol.Models 4 | { 5 | /// 6 | /// A resource in the . 7 | /// 8 | /// See https://docs.microsoft.com/en-us/nuget/api/service-index#resources 9 | /// 10 | public class ServiceIndexItem 11 | { 12 | /// 13 | /// The resource's base URL. 14 | /// 15 | [JsonProperty("@id")] 16 | public string ResourceUrl { get; set; } 17 | 18 | /// 19 | /// The resource's type. 20 | /// 21 | [JsonProperty("@type")] 22 | public string Type { get; set; } 23 | 24 | /// 25 | /// Human readable comments about the resource. 26 | /// 27 | [JsonProperty("comment")] 28 | public string Comment { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/NugetProxy/NugetProxy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | Linux 7 | ..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Metadata/BaGetPackageMetadata.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NugetProxy.Protocol.Models; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Core 6 | { 7 | /// 8 | /// NugetProxy's extensions to the package metadata model. These additions 9 | /// are not part of the official protocol. 10 | /// 11 | public class NugetProxyPackageMetadata : PackageMetadata 12 | { 13 | [JsonProperty("downloads")] 14 | public long Downloads { get; set; } 15 | 16 | [JsonProperty("hasReadme")] 17 | public bool HasReadme { get; set; } 18 | 19 | [JsonProperty("packageTypes")] 20 | public IReadOnlyList PackageTypes { get; set; } 21 | 22 | [JsonProperty("repositoryUrl")] 23 | public string RepositoryUrl { get; set; } 24 | 25 | [JsonProperty("repositoryType")] 26 | public string RepositoryType { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/DependencyGroupItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// The dependencies of the package for a specific target framework. 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group 10 | /// 11 | public class DependencyGroupItem 12 | { 13 | /// 14 | /// The target framework that these dependencies are applicable to. 15 | /// 16 | [JsonProperty("targetFramework")] 17 | public string TargetFramework { get; set; } 18 | 19 | /// 20 | /// A list of dependencies. 21 | /// 22 | [JsonProperty("dependencies", DefaultValueHandling = DefaultValueHandling.Ignore)] 23 | public List Dependencies { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/AutocompleteResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// The package ids that matched the autocomplete query. 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource#search-for-package-ids 10 | /// 11 | public class AutocompleteResponse 12 | { 13 | public AutocompleteContext Context { get; set; } 14 | 15 | /// 16 | /// The total number of matches, disregarding skip and take. 17 | /// 18 | [JsonProperty("totalHits")] 19 | public long TotalHits { get; set; } 20 | 21 | /// 22 | /// The package IDs matched by the autocomplete query. 23 | /// 24 | [JsonProperty("data")] 25 | public IReadOnlyList Data { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Extensions/PackageContentModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using NugetProxy.Protocol.Models; 4 | using NuGet.Versioning; 5 | 6 | namespace NugetProxy.Protocol 7 | { 8 | /// 9 | /// These are documented interpretations of values returned by the Package Content resource. 10 | /// 11 | public static class PackageContentModelExtensions 12 | { 13 | /// 14 | /// Parse the package versions as s. 15 | /// 16 | /// The package versions response. 17 | /// The package versions. 18 | public static IReadOnlyList ParseVersions(this PackageVersionsResponse response) 19 | { 20 | return response 21 | .Versions 22 | .Select(NuGetVersion.Parse) 23 | .ToList(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/NugetProxy/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:50557/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "NugetProxy": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:50561/" 25 | }, 26 | "Docker": { 27 | "commandName": "Docker", 28 | "launchBrowser": true, 29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 30 | "environmentVariables": {}, 31 | "httpPort": 50558 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/NugetProxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-alpine AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | 5 | FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS build 6 | WORKDIR /src 7 | 8 | COPY ["src/NugetProxy/NugetProxy.csproj", "src/NugetProxy/"] 9 | COPY ["src/NugetProxy.Core/NugetProxy.Core.csproj", "src/NugetProxy.Core/"] 10 | COPY ["src/NugetProxy.Protocol/NugetProxy.Protocol.csproj", "src/NugetProxy.Protocol/"] 11 | COPY ["src/NugetProxy.Core.Server/NugetProxy.Core.Server.csproj", "src/NugetProxy.Core.Server/"] 12 | COPY ["src/Directory.Build.props", "src/Directory.Build.props"] 13 | 14 | RUN dotnet restore "src/NugetProxy/NugetProxy.csproj" 15 | COPY . . 16 | WORKDIR "/src/src/NugetProxy" 17 | RUN dotnet build "NugetProxy.csproj" -c Release -o /app/build 18 | 19 | FROM build AS publish 20 | RUN dotnet publish "NugetProxy.csproj" -c Release -o /app/publish 21 | 22 | FROM base AS final 23 | WORKDIR /app 24 | COPY --from=publish /app/publish . 25 | ENTRYPOINT ["dotnet", "NugetProxy.dll"] 26 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/Controllers/ServiceIndexController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Core; 5 | using NugetProxy.Protocol.Models; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace NugetProxy.Controllers 9 | { 10 | /// 11 | /// The NuGet Service Index. This aids NuGet client to discover this server's services. 12 | /// 13 | public class ServiceIndexController : Controller 14 | { 15 | private readonly IServiceIndexService _serviceIndex; 16 | 17 | public ServiceIndexController(IServiceIndexService serviceIndex) 18 | { 19 | _serviceIndex = serviceIndex ?? throw new ArgumentNullException(nameof(serviceIndex)); 20 | } 21 | 22 | // GET v3/index 23 | [HttpGet] 24 | public async Task GetAsync(CancellationToken cancellationToken) 25 | { 26 | return await _serviceIndex.GetAsync(cancellationToken); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.azure/pipelines/ci-official.yml: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 2 | # Build and test ASP.NET Core projects targeting .NET Core. 3 | # Add steps that run tests, create a NuGet package, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core 5 | 6 | variables: 7 | BuildConfiguration: Release 8 | 9 | jobs: 10 | - job: Docker 11 | pool: 12 | vmImage: ubuntu-16.04 13 | steps: 14 | - task: Docker@2 15 | inputs: 16 | containerRegistry: 'Docker' 17 | repository: 'cwoolum/ThinNugetProxy' 18 | command: 'build' 19 | Dockerfile: 'src/NugetProxy/Dockerfile' 20 | buildContext: 21 | tags: | 22 | $(Build.SourceVersion) 23 | latest 24 | 25 | - task: Docker@2 26 | displayName: Push docker image 27 | condition: eq(variables['build.sourcebranch'], 'refs/heads/master') 28 | inputs: 29 | containerRegistry: 'Docker' 30 | repository: 'cwoolum/ThinNugetProxy' 31 | command: 'push' 32 | tags: | 33 | $(Build.SourceVersion) 34 | latest 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Loic Sharma 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | *.tsx text=auto 46 | 47 | *.csproj text=auto 48 | *.vbproj text=auto 49 | *.fsproj text=auto 50 | *.dbproj text=auto 51 | *.sln text=auto eol=crlf 52 | 53 | *.sh eol=lf -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/RegistrationIndexPageItem.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace NugetProxy.Protocol.Models 4 | { 5 | /// 6 | /// An item in the that references a . 7 | /// 8 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf-object-in-a-page 9 | /// 10 | public class RegistrationIndexPageItem 11 | { 12 | /// 13 | /// The URL to the registration leaf. 14 | /// 15 | [JsonProperty("@id")] 16 | public string RegistrationLeafUrl { get; set; } 17 | 18 | /// 19 | /// The catalog entry containing the package metadata. 20 | /// 21 | [JsonProperty("catalogEntry")] 22 | public PackageMetadata PackageMetadata { get; set; } 23 | 24 | /// 25 | /// The URL to the package content (.nupkg) 26 | /// 27 | [JsonProperty("packageContent")] 28 | public string PackageContentUrl { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/SearchResultVersion.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace NugetProxy.Protocol.Models 4 | { 5 | /// 6 | /// A single version from a . 7 | /// 8 | /// See https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result 9 | /// 10 | public class SearchResultVersion 11 | { 12 | /// 13 | /// The registration leaf URL for this single version of the matched package. 14 | /// 15 | [JsonProperty("@id")] 16 | public string RegistrationLeafUrl { get; set; } 17 | 18 | /// 19 | /// The package's full NuGet version after normalization, including any SemVer 2.0.0 build metadata. 20 | /// 21 | [JsonProperty("version")] 22 | public string Version { get; set; } 23 | 24 | /// 25 | /// The downloads for this single version of the matched package. 26 | /// 27 | [JsonProperty("downloads")] 28 | public long Downloads { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/NugetProxy.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | The core libraries that power BaGet. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/Routes.cs: -------------------------------------------------------------------------------- 1 | namespace NugetProxy 2 | { 3 | public class Routes 4 | { 5 | public const string IndexRouteName = "index"; 6 | public const string UploadPackageRouteName = "upload-package"; 7 | public const string UploadSymbolRouteName = "upload-symbol"; 8 | public const string DeleteRouteName = "delete"; 9 | public const string RelistRouteName = "relist"; 10 | public const string SearchRouteName = "search"; 11 | public const string AutocompleteRouteName = "autocomplete"; 12 | public const string DependentsRouteName = "dependents"; 13 | public const string RegistrationIndexRouteName = "registration-index"; 14 | public const string RegistrationLeafRouteName = "registration-leaf"; 15 | public const string PackageVersionsRouteName = "package-versions"; 16 | public const string PackageDownloadRouteName = "package-download"; 17 | public const string PackageDownloadManifestRouteName = "package-download-manifest"; 18 | public const string PackageDownloadReadmeRouteName = "package-download-readme"; 19 | public const string SymbolDownloadRouteName = "symbol-download"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Search/SearchClient.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Protocol.Models; 2 | 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace NugetProxy.Protocol.Internal 8 | { 9 | public class SearchClient : ISearchClient 10 | { 11 | private readonly NuGetClientFactory _clientfactory; 12 | 13 | public SearchClient(NuGetClientFactory clientFactory) 14 | { 15 | _clientfactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); 16 | } 17 | 18 | public async Task SearchAsync( 19 | string query = null, 20 | int skip = 0, 21 | int take = 20, 22 | bool includePrerelease = true, 23 | bool includeSemVer2 = true, 24 | CancellationToken cancellationToken = default) 25 | { 26 | // TODO: Support search failover. 27 | // See: https://github.com/loic-sharma/NugetProxy/issues/314 28 | var client = await _clientfactory.CreateSearchClientAsync(cancellationToken); 29 | 30 | return await client.SearchAsync(query, skip, take, includePrerelease, includeSemVer2); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Extensions/SearchModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Protocol.Models; 2 | using NuGet.Versioning; 3 | 4 | namespace NugetProxy.Protocol 5 | { 6 | /// 7 | /// These are documented interpretations of values returned by the Search resource. 8 | /// 9 | public static class SearchModelExtensions 10 | { 11 | /// 12 | /// Parse the search result's version as a . 13 | /// 14 | /// The search result. 15 | /// The search result's version. 16 | public static NuGetVersion ParseVersion(this SearchResult result) 17 | { 18 | return NuGetVersion.Parse(result.Version); 19 | } 20 | 21 | /// 22 | /// Parse the search result's version as a . 23 | /// 24 | /// The search result. 25 | /// The search result's version. 26 | public static NuGetVersion ParseVersion(this SearchResultVersion result) 27 | { 28 | return NuGetVersion.Parse(result.Version); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/ICatalogLeafItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NugetProxy.Protocol.Models 4 | { 5 | // This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/ICatalogLeafItem.cs 6 | 7 | /// 8 | /// A catalog leaf. Represents a single package event. 9 | /// Leafs can be discovered from a . 10 | /// 11 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-leaf 12 | /// 13 | public interface ICatalogLeafItem 14 | { 15 | /// 16 | /// The commit timestamp of this catalog item. 17 | /// 18 | DateTimeOffset CommitTimestamp { get; } 19 | 20 | /// 21 | /// The package ID of the catalog item. 22 | /// 23 | string PackageId { get; } 24 | 25 | /// 26 | /// The package version of the catalog item. 27 | /// 28 | string PackageVersion { get; } 29 | 30 | /// 31 | /// The type of the current catalog leaf. 32 | /// 33 | CatalogLeafType Type { get; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/CatalogPageItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | // This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/CatalogPageItem.cs 7 | 8 | /// 9 | /// An item in the that references a . 10 | /// 11 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-page-object-in-the-index 12 | /// 13 | public class CatalogPageItem 14 | { 15 | /// 16 | /// The URL to this item's corresponding . 17 | /// 18 | [JsonProperty("@id")] 19 | public string CatalogPageUrl { get; set; } 20 | 21 | /// 22 | /// A timestamp of the most recent commit in this page. 23 | /// 24 | [JsonProperty("commitTimeStamp")] 25 | public DateTimeOffset CommitTimestamp { get; set; } 26 | 27 | /// 28 | /// The number of items in the page. 29 | /// 30 | [JsonProperty("count")] 31 | public int Count { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Converters/PackageDependencyRangeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Internal 6 | { 7 | internal class PackageDependencyRangeConverter : JsonConverter 8 | { 9 | public override bool CanConvert(Type objectType) 10 | { 11 | return objectType == typeof(string); 12 | } 13 | 14 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 15 | { 16 | if (reader.TokenType == JsonToken.StartArray) 17 | { 18 | // There are some quirky packages with arrays of dependency version ranges. In this case, we take the 19 | // first element. 20 | // Example: https://api.nuget.org/v3/catalog0/data/2016.02.21.11.06.01/dingu.generic.repo.ef7.1.0.0-beta2.json 21 | var array = serializer.Deserialize(reader); 22 | return array.FirstOrDefault(); 23 | } 24 | 25 | return serializer.Deserialize(reader); 26 | } 27 | 28 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 29 | { 30 | serializer.Serialize(writer, value); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Converters/SingleOrListConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Internal 6 | { 7 | /// 8 | /// Converts a single value or a list of values into the desired type or . 9 | /// 10 | /// The desired type. 11 | internal class SingleOrListConverter : JsonConverter 12 | { 13 | /// 14 | public override bool CanConvert(Type objectType) 15 | { 16 | return objectType == typeof(IReadOnlyList); 17 | } 18 | 19 | /// 20 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 21 | { 22 | if (reader.TokenType == JsonToken.StartArray) 23 | { 24 | return serializer.Deserialize>(reader); 25 | } 26 | else 27 | { 28 | return serializer.Deserialize(reader); 29 | } 30 | } 31 | 32 | /// 33 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 34 | { 35 | serializer.Serialize(writer, value); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Converters/BaseCatalogLeafConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NugetProxy.Protocol.Models; 4 | using Newtonsoft.Json; 5 | 6 | namespace NugetProxy.Protocol.Internal 7 | { 8 | /// 9 | /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/BaseCatalogLeafConverter.cs 10 | /// 11 | internal abstract class BaseCatalogLeafConverter : JsonConverter 12 | { 13 | private readonly IReadOnlyDictionary _fromType; 14 | 15 | public BaseCatalogLeafConverter(IReadOnlyDictionary fromType) 16 | { 17 | _fromType = fromType; 18 | } 19 | 20 | public override bool CanConvert(Type objectType) 21 | { 22 | return objectType == typeof(CatalogLeafType); 23 | } 24 | 25 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 26 | { 27 | if (!_fromType.TryGetValue((CatalogLeafType)value, out var output)) 28 | { 29 | throw new NotSupportedException($"The catalog leaf type '{value}' is not supported."); 30 | } 31 | 32 | writer.WriteValue(output); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Configuration/MirrorOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace NugetProxy.Core 6 | { 7 | public class MirrorOptions : IValidatableObject 8 | { 9 | /// 10 | /// If true, packages that aren't found locally will be indexed 11 | /// using the upstream source. 12 | /// 13 | public bool Enabled { get; set; } 14 | 15 | /// 16 | /// The v3 index that will be mirrored. 17 | /// 18 | public Uri PackageSource { get; set; } 19 | 20 | /// 21 | /// The time before a download from the package source times out. 22 | /// 23 | [Range(0, int.MaxValue)] 24 | public int PackageDownloadTimeoutSeconds { get; set; } = 600; 25 | 26 | public string AccessToken { get; set; } 27 | 28 | public IEnumerable Validate(ValidationContext validationContext) 29 | { 30 | if (Enabled && PackageSource == null) 31 | { 32 | yield return new ValidationResult( 33 | $"The {nameof(PackageSource)} configuration is required if mirroring is enabled", 34 | new[] { nameof(PackageSource) }); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/CatalogIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | // This class is based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/CatalogIndex.cs 8 | 9 | /// 10 | /// The catalog index is the entry point for the catalog resource. 11 | /// Use this to discover catalog pages, which in turn can be used to discover catalog leafs. 12 | /// 13 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-index 14 | /// 15 | public class CatalogIndex 16 | { 17 | /// 18 | /// A timestamp of the most recent commit. 19 | /// 20 | [JsonProperty("commitTimeStamp")] 21 | public DateTimeOffset CommitTimestamp { get; set; } 22 | 23 | /// 24 | /// The number of catalog pages in the catalog index. 25 | /// 26 | [JsonProperty("count")] 27 | public int Count { get; set; } 28 | 29 | /// 30 | /// The items used to discover s. 31 | /// 32 | [JsonProperty("items")] 33 | public List Items { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/NugetProxy/Program.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Extensions; 2 | 3 | using McMaster.Extensions.CommandLineUtils; 4 | 5 | using Microsoft.AspNetCore; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.Hosting; 9 | 10 | using System; 11 | 12 | namespace NugetProxy 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | var app = new CommandLineApplication 19 | { 20 | Name = "NugetProxy", 21 | Description = "A light-weight NuGet service", 22 | }; 23 | 24 | app.HelpOption(inherited: true); 25 | 26 | app.OnExecute(() => 27 | { 28 | CreateWebHostBuilder(args).Build().Run(); 29 | }); 30 | 31 | app.Execute(args); 32 | } 33 | 34 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 35 | WebHost.CreateDefaultBuilder(args) 36 | .UseStartup(); 37 | 38 | public static IHostBuilder CreateHostBuilder(string[] args) 39 | { 40 | return new HostBuilder() 41 | .ConfigureNugetProxyConfiguration(args) 42 | .ConfigureNugetProxyServices() 43 | .ConfigureNugetProxyLogging(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Catalog/CatalogProcessorOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NugetProxy.Protocol.Catalog 4 | { 5 | /// 6 | /// The options to configure . 7 | /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/3a468fe534a03dcced897eb5992209fdd3c4b6c9/src/NuGet.Protocol.Catalog/CatalogProcessorSettings.cs 8 | /// 9 | public class CatalogProcessorOptions 10 | { 11 | /// 12 | /// The minimum commit timestamp to use when no cursor value has been saved. 13 | /// 14 | public DateTimeOffset? DefaultMinCommitTimestamp { get; set; } 15 | 16 | /// 17 | /// The absolute minimum (exclusive) commit timestamp to process in the catalog. 18 | /// 19 | public DateTimeOffset MinCommitTimestamp { get; set; } 20 | 21 | /// 22 | /// The absolute maximum (inclusive) commit timestamp to process in the catalog. 23 | /// 24 | public DateTimeOffset MaxCommitTimestamp { get; set; } 25 | 26 | /// 27 | /// If multiple catalog leaves are found in a page concerning the same package ID and version, only the latest 28 | /// is processed. 29 | /// 30 | public bool ExcludeRedundantLeaves { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/PackageNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NuGet.Versioning; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// An exception thrown when a package could not be found on the NuGet server. 8 | /// 9 | public class PackageNotFoundException : Exception 10 | { 11 | /// 12 | /// Create a new instance of the . 13 | /// 14 | /// The ID of the package that could not be found. 15 | /// The version of the package that could not be found. 16 | public PackageNotFoundException(string packageId, NuGetVersion packageVersion) 17 | : base($"Could not find package {packageId} {packageVersion}") 18 | { 19 | PackageId = packageId ?? throw new ArgumentNullException(nameof(packageId)); 20 | PackageVersion = packageVersion ?? throw new ArgumentNullException(nameof(packageVersion)); 21 | } 22 | 23 | /// 24 | /// The package ID that could not be found. 25 | /// 26 | public string PackageId { get; } 27 | 28 | /// 29 | /// The package version that could not be found. 30 | /// 31 | public NuGetVersion PackageVersion { get; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Catalog/ICursor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace NugetProxy.Protocol.Catalog 6 | { 7 | /// 8 | /// The NuGet Catalog resource is an append-only data structure indexed by time. 9 | /// The tracks up to what point in the catalog has been successfully 10 | /// processed. The value is a catalog commit timestamp. 11 | /// See: https://docs.microsoft.com/en-us/nuget/api/catalog-resource#cursor 12 | /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/3a468fe534a03dcced897eb5992209fdd3c4b6c9/src/NuGet.Protocol.Catalog/ICursor.cs 13 | /// 14 | public interface ICursor 15 | { 16 | /// 17 | /// Get the value of the cursor. 18 | /// 19 | /// A token to cancel the task. 20 | /// The cursor value. Null if the cursor has no value yet. 21 | Task GetAsync(CancellationToken cancellationToken = default); 22 | 23 | /// 24 | /// Set the value of the cursor. 25 | /// 26 | /// The new cursor value. 27 | /// A token to cancel the task. 28 | Task SetAsync(DateTimeOffset value, CancellationToken cancellationToken = default); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/Extensions/IServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Configuration; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Cors.Infrastructure; 4 | using Microsoft.AspNetCore.Http.Features; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Options; 8 | using Newtonsoft.Json; 9 | 10 | namespace NugetProxy.Core.Server.Extensions 11 | { 12 | public static class IServiceCollectionExtensions 13 | { 14 | public static IServiceCollection ConfigureHttpServices(this IServiceCollection services) 15 | { 16 | services 17 | .AddMvc() 18 | .AddApplicationPart(typeof(NugetProxy.Controllers.PackageContentController).Assembly) 19 | .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) 20 | .AddJsonOptions(options => 21 | { 22 | options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; 23 | }); 24 | 25 | 26 | services.AddCors(); 27 | services.AddHttpContextAccessor(); 28 | services.AddSingleton, ConfigureForwardedHeadersOptions>(); 29 | services.Configure(options => 30 | { 31 | options.MultipartBodyLengthLimit = int.MaxValue; 32 | }); 33 | 34 | return services; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Search/ISearchClient.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Protocol.Models; 2 | 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NugetProxy.Protocol 7 | { 8 | /// 9 | /// The client used to search for packages. 10 | /// 11 | /// See https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource 12 | /// 13 | public interface ISearchClient 14 | { 15 | /// 16 | /// Perform a search query. 17 | /// See: https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages 18 | /// 19 | /// The search query. 20 | /// How many results to skip. 21 | /// How many results to return. 22 | /// Whether pre-release packages should be returned. 23 | /// Whether packages that require SemVer 2.0.0 compatibility should be returned. 24 | /// A token to cancel the task. 25 | /// The search response. 26 | Task SearchAsync( 27 | string query = null, 28 | int skip = 0, 29 | int take = 20, 30 | bool includePrerelease = true, 31 | bool includeSemVer2 = true, 32 | CancellationToken cancellationToken = default); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/CatalogPage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | // This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/CatalogPage.cs 8 | 9 | /// 10 | /// A catalog page, used to discover catalog leafs. 11 | /// Pages can be discovered from a . 12 | /// 13 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-page 14 | /// 15 | public class CatalogPage 16 | { 17 | /// 18 | /// A unique ID associated with the most recent commit in this page. 19 | /// 20 | [JsonProperty("commitTimeStamp")] 21 | public DateTimeOffset CommitTimestamp { get; set; } 22 | 23 | /// 24 | /// The number of items in the page. 25 | /// 26 | [JsonProperty("count")] 27 | public int Count { get; set; } 28 | 29 | /// 30 | /// The items used to discover s. 31 | /// 32 | [JsonProperty("items")] 33 | public List Items { get; set; } 34 | 35 | /// 36 | /// The URL to the Catalog Index. 37 | /// 38 | [JsonProperty("parent")] 39 | public string CatalogIndexUrl { get; set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ThinNugetProxy 5 | 6 | 0.1.0-prerelease 7 | MIT 8 | https://raw.githubusercontent.com/NuGet/Media/master/Images/MainLogo/256x256/nuget_256.png 9 | 10 | https://github.com/cwoolum/ThinNugetProxy 11 | git 12 | true 13 | 14 | true 15 | true 16 | snupkg 17 | 18 | 7.2 19 | 1591 20 | 21 | 22 | 23 | portable 24 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 25 | 26 | 27 | 28 | 29 | 2.2.0 30 | 2.2.2 31 | 2.2.0 32 | 5.3.0 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/NugetProxy/Startup.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Extensions; 2 | 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | using System; 9 | 10 | namespace NugetProxy 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 22 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 23 | { 24 | if (env.IsDevelopment()) 25 | { 26 | app.UseDeveloperExceptionPage(); 27 | app.UseStatusCodePages(); 28 | } 29 | 30 | app.UseForwardedHeaders(); 31 | 32 | app.UseMvc(routes => 33 | { 34 | routes 35 | .MapServiceIndexRoutes() 36 | .MapSymbolRoutes() 37 | .MapSearchRoutes() 38 | .MapPackageMetadataRoutes() 39 | .MapPackageContentRoutes(); 40 | }); 41 | } 42 | 43 | public void ConfigureServices(IServiceCollection services) 44 | { 45 | services.ConfigureNugetProxy(Configuration, httpServices: true); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/RegistrationIndexResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// The metadata for a package and all of its versions. 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index 10 | /// 11 | public class RegistrationIndexResponse 12 | { 13 | public static readonly IReadOnlyList DefaultType = new List 14 | { 15 | "catalog:CatalogRoot", 16 | "PackageRegistration", 17 | "catalog:Permalink" 18 | }; 19 | 20 | /// 21 | /// The URL to the registration index. 22 | /// 23 | [JsonProperty("@id")] 24 | public string RegistrationIndexUrl { get; set; } 25 | 26 | /// 27 | /// The registration index's type. 28 | /// 29 | [JsonProperty("@type")] 30 | public IReadOnlyList Type { get; set; } 31 | 32 | /// 33 | /// The number of registration pages. See . 34 | /// 35 | [JsonProperty("count")] 36 | public int Count { get; set; } 37 | 38 | /// 39 | /// The pages that contain all of the versions of the package, ordered 40 | /// by the package's version. 41 | /// 42 | [JsonProperty("items")] 43 | public IReadOnlyList Pages { get; set; } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/Extensions/HttpRequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Core; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace NugetProxy.Extensions 8 | { 9 | public static class HttpRequestExtensions 10 | { 11 | public const string ApiKeyHeader = "X-NuGet-ApiKey"; 12 | 13 | public static async Task GetUploadStreamOrNullAsync(this HttpRequest request, CancellationToken cancellationToken) 14 | { 15 | // Try to get the nupkg from the multipart/form-data. If that's empty, 16 | // fallback to the request's body. 17 | Stream rawUploadStream = null; 18 | try 19 | { 20 | if (request.HasFormContentType && request.Form.Files.Count > 0) 21 | { 22 | rawUploadStream = request.Form.Files[0].OpenReadStream(); 23 | } 24 | else 25 | { 26 | rawUploadStream = request.Body; 27 | } 28 | 29 | // Convert the upload stream into a temporary file stream to 30 | // minimize memory usage. 31 | return await rawUploadStream?.AsTemporaryFileStreamAsync(cancellationToken); 32 | } 33 | finally 34 | { 35 | rawUploadStream?.Dispose(); 36 | } 37 | } 38 | 39 | public static string GetApiKey(this HttpRequest request) 40 | { 41 | return request.Headers[ApiKeyHeader]; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/ServiceIndex/ServiceIndexClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using NugetProxy.Protocol.Models; 6 | 7 | namespace NugetProxy.Protocol.Internal 8 | { 9 | /// 10 | /// The NuGet Service Index client, used to discover other resources. 11 | /// 12 | /// See https://docs.microsoft.com/en-us/nuget/api/service-index 13 | /// 14 | public class ServiceIndexClient : IServiceIndexClient 15 | { 16 | private readonly HttpClient _httpClient; 17 | private readonly string _serviceIndexUrl; 18 | 19 | /// 20 | /// Create a service index for the upstream source. 21 | /// 22 | /// The HTTP client used to send requests. 23 | /// The NuGet server's service index URL. 24 | public ServiceIndexClient(HttpClient httpClient, string serviceIndexUrl) 25 | { 26 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 27 | _serviceIndexUrl = serviceIndexUrl ?? throw new ArgumentNullException(nameof(serviceIndexUrl)); 28 | } 29 | 30 | /// 31 | public async Task GetAsync(CancellationToken cancellationToken = default) 32 | { 33 | var response = await _httpClient.DeserializeUrlAsync( 34 | _serviceIndexUrl, 35 | cancellationToken); 36 | 37 | return response.GetResultOrThrow(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Validation/ValidatePostConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace NugetProxy.Core 8 | { 9 | /// 10 | /// A configuration that validates options using data annotations. 11 | /// 12 | /// The type of options to validate. 13 | public class ValidatePostConfigureOptions : IPostConfigureOptions where TOptions : class 14 | { 15 | public void PostConfigure(string name, TOptions options) 16 | { 17 | var context = new ValidationContext(options); 18 | var validationResults = new List(); 19 | if (!Validator.TryValidateObject(options, context, validationResults, validateAllProperties: true)) 20 | { 21 | var optionName = OptionNameOrNull(options); 22 | var message = (optionName == null) 23 | ? $"Invalid options" 24 | : $"Invalid '{optionName}' options"; 25 | 26 | throw new InvalidOperationException( 27 | $"{message}: {string.Join("\n", validationResults)}"); 28 | } 29 | } 30 | 31 | private string OptionNameOrNull(TOptions options) 32 | { 33 | // Trim the "Options" suffix. 34 | var optionsName = typeof(TOptions).Name; 35 | if (optionsName.EndsWith("Options")) 36 | { 37 | return optionsName.Substring(0, optionsName.Length - "Options".Length); 38 | } 39 | 40 | return optionsName; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Converters/CatalogLeafItemTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NugetProxy.Protocol.Models; 5 | using Newtonsoft.Json; 6 | 7 | namespace NugetProxy.Protocol.Internal 8 | { 9 | /// 10 | /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/CatalogLeafItemTypeConverter.cs 11 | /// 12 | internal class CatalogLeafItemTypeConverter : BaseCatalogLeafConverter 13 | { 14 | private static readonly Dictionary FromType = new Dictionary 15 | { 16 | { CatalogLeafType.PackageDelete, "nuget:PackageDelete" }, 17 | { CatalogLeafType.PackageDetails, "nuget:PackageDetails" }, 18 | }; 19 | 20 | private static readonly Dictionary FromString = FromType 21 | .ToDictionary(x => x.Value, x => x.Key); 22 | 23 | public CatalogLeafItemTypeConverter() : base(FromType) 24 | { 25 | } 26 | 27 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 28 | { 29 | var stringValue = reader.Value as string; 30 | if (stringValue != null) 31 | { 32 | CatalogLeafType output; 33 | if (FromString.TryGetValue(stringValue, out output)) 34 | { 35 | return output; 36 | } 37 | } 38 | 39 | throw new JsonSerializationException($"Unexpected value for a {nameof(CatalogLeafType)}."); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/NugetProxy/Extensions/IHostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace NugetProxy.Extensions 7 | { 8 | // See https://github.com/aspnet/MetaPackages/blob/master/src/Microsoft.AspNetCore/WebHost.cs 9 | public static class IHostBuilderExtensions 10 | { 11 | public static IHostBuilder ConfigureNugetProxyConfiguration(this IHostBuilder builder, string[] args) 12 | { 13 | return builder.ConfigureAppConfiguration((context, config) => 14 | { 15 | config.AddEnvironmentVariables(); 16 | 17 | config 18 | .SetBasePath(Environment.CurrentDirectory) 19 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); 20 | 21 | if (args != null) 22 | { 23 | config.AddCommandLine(args); 24 | } 25 | }); 26 | } 27 | 28 | public static IHostBuilder ConfigureNugetProxyLogging(this IHostBuilder builder) 29 | { 30 | return builder 31 | .ConfigureLogging((context, logging) => 32 | { 33 | logging.AddConfiguration(context.Configuration.GetSection("Logging")); 34 | logging.AddConsole(); 35 | logging.AddDebug(); 36 | }); 37 | } 38 | 39 | public static IHostBuilder ConfigureNugetProxyServices(this IHostBuilder builder) 40 | { 41 | return builder 42 | .ConfigureServices((context, services) => services.ConfigureNugetProxy(context.Configuration)); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/ResponseAndResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using NugetProxy.Protocol.Models; 5 | 6 | namespace NugetProxy.Protocol 7 | { 8 | internal class ResponseAndResult 9 | { 10 | public ResponseAndResult( 11 | HttpMethod method, 12 | string requestUri, 13 | HttpStatusCode statusCode, 14 | string reasonPhrase, 15 | bool hasResult, 16 | T result) 17 | { 18 | Method = method ?? throw new ArgumentNullException(nameof(method)); 19 | RequestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri)); 20 | StatusCode = statusCode; 21 | ReasonPhrase = reasonPhrase ?? throw new ArgumentNullException(nameof(reasonPhrase)); 22 | HasResult = hasResult; 23 | Result = result; 24 | } 25 | 26 | public HttpMethod Method { get; } 27 | public string RequestUri { get; } 28 | public HttpStatusCode StatusCode { get; } 29 | public string ReasonPhrase { get; } 30 | public bool HasResult { get; } 31 | public T Result { get; } 32 | 33 | public T GetResultOrThrow() 34 | { 35 | if (!HasResult) 36 | { 37 | throw new ProtocolException( 38 | $"The HTTP request failed.{Environment.NewLine}" + 39 | $"Request: {Method} {RequestUri}{Environment.NewLine}" + 40 | $"Response: {(int)StatusCode} {ReasonPhrase}", 41 | Method, 42 | RequestUri, 43 | StatusCode, 44 | ReasonPhrase); 45 | } 46 | 47 | return Result; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/CatalogLeafItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NugetProxy.Protocol.Internal; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | // This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/CatalogLeafItem.cs 8 | 9 | /// 10 | /// An item in a that references a . 11 | /// 12 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-item-object-in-a-page 13 | /// 14 | public class CatalogLeafItem : ICatalogLeafItem 15 | { 16 | /// 17 | /// The URL to the current catalog leaf. 18 | /// 19 | [JsonProperty("@id")] 20 | public string CatalogLeafUrl { get; set; } 21 | 22 | /// 23 | /// The type of the current catalog leaf. 24 | /// 25 | [JsonProperty("@type")] 26 | [JsonConverter(typeof(CatalogLeafItemTypeConverter))] 27 | public CatalogLeafType Type { get; set; } 28 | 29 | /// 30 | /// The commit timestamp of this catalog item. 31 | /// 32 | [JsonProperty("commitTimeStamp")] 33 | public DateTimeOffset CommitTimestamp { get; set; } 34 | 35 | /// 36 | /// The package ID of the catalog item. 37 | /// 38 | [JsonProperty("nuget:id")] 39 | public string PackageId { get; set; } 40 | 41 | /// 42 | /// The package version of the catalog item. 43 | /// 44 | [JsonProperty("nuget:version")] 45 | public string PackageVersion { get; set; } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/NugetProxy/bin/Debug/netcoreapp2.2/NugetProxy.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/NugetProxy", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start ${auto-detect-url}" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceFolder}/Views" 37 | } 38 | }, 39 | { 40 | "name": ".NET Core Attach", 41 | "type": "coreclr", 42 | "request": "attach", 43 | "processId": "${command:pickProcess}" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/RegistrationIndexPage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace NugetProxy.Protocol.Models 5 | { 6 | /// 7 | /// The registration page object found in the registration index. 8 | /// 9 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page-object 10 | /// 11 | public class RegistrationIndexPage 12 | { 13 | /// 14 | /// The URL to the registration page. 15 | /// 16 | [JsonProperty("@id")] 17 | public string RegistrationPageUrl { get; set; } 18 | 19 | /// 20 | /// The number of registration leafs in the page. 21 | /// 22 | [JsonProperty("count")] 23 | public int Count { get; set; } 24 | 25 | /// 26 | /// if this package's registration is paged. The items can be found 27 | /// by following the page's . 28 | /// 29 | [JsonProperty("items")] 30 | public IReadOnlyList ItemsOrNull { get; set; } 31 | 32 | /// 33 | /// This page's lowest package version. The version should be lowercased, normalized, 34 | /// and the SemVer 2.0.0 build metadata removed, if any. 35 | /// 36 | [JsonProperty("lower")] 37 | public string Lower { get; set; } 38 | 39 | /// 40 | /// This page's highest package version. The version should be lowercased, normalized, 41 | /// and the SemVer 2.0.0 build metadata removed, if any. 42 | /// 43 | [JsonProperty("upper")] 44 | public string Upper { get; set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Extensions/NuGetClientFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NugetProxy.Protocol.Catalog; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace NugetProxy.Protocol 7 | { 8 | public static class NuGetClientFactoryExtensions 9 | { 10 | /// 11 | /// Create a new to discover and download catalog leafs. 12 | /// Leafs are processed by the . 13 | /// 14 | /// The factory used to create NuGet clients. 15 | /// Cursor to track succesfully processed leafs. Leafs before the cursor are skipped. 16 | /// The leaf processor. 17 | /// The options to configure catalog processing. 18 | /// The logger used for telemetry. 19 | /// A token to cancel the task. 20 | /// The catalog processor. 21 | public static async Task CreateCatalogProcessorAsync( 22 | this NuGetClientFactory clientFactory, 23 | ICursor cursor, 24 | ICatalogLeafProcessor leafProcessor, 25 | CatalogProcessorOptions options, 26 | ILogger logger, 27 | CancellationToken cancellationToken = default) 28 | { 29 | var catalogClient = await clientFactory.CreateCatalogClientAsync(cancellationToken); 30 | 31 | return new CatalogProcessor( 32 | cursor, 33 | catalogClient, 34 | leafProcessor, 35 | options, 36 | logger); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Catalog/CatalogClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Protocol.Models; 5 | 6 | namespace NugetProxy.Protocol.Internal 7 | { 8 | public class CatalogClient : ICatalogClient 9 | { 10 | private readonly NuGetClientFactory _clientfactory; 11 | 12 | public CatalogClient(NuGetClientFactory clientFactory) 13 | { 14 | _clientfactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); 15 | } 16 | 17 | public async Task GetIndexAsync(CancellationToken cancellationToken = default) 18 | { 19 | var client = await _clientfactory.CreateCatalogClientAsync(cancellationToken); 20 | 21 | return await client.GetIndexAsync(cancellationToken); 22 | } 23 | 24 | public async Task GetPageAsync(string pageUrl, CancellationToken cancellationToken = default) 25 | { 26 | var client = await _clientfactory.CreateCatalogClientAsync(cancellationToken); 27 | 28 | return await client.GetPageAsync(pageUrl, cancellationToken); 29 | } 30 | 31 | public async Task GetPackageDetailsLeafAsync(string leafUrl, CancellationToken cancellationToken = default) 32 | { 33 | var client = await _clientfactory.CreateCatalogClientAsync(cancellationToken); 34 | 35 | return await client.GetPackageDetailsLeafAsync(leafUrl, cancellationToken); 36 | } 37 | 38 | public async Task GetPackageDeleteLeafAsync(string leafUrl, CancellationToken cancellationToken = default) 39 | { 40 | var client = await _clientfactory.CreateCatalogClientAsync(cancellationToken); 41 | 42 | return await client.GetPackageDeleteLeafAsync(leafUrl, cancellationToken); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/PackageMetadata/PackageMetadataClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Protocol.Models; 5 | using NuGet.Versioning; 6 | 7 | namespace NugetProxy.Protocol.Internal 8 | { 9 | public class PackageMetadataClient : IPackageMetadataClient 10 | { 11 | private readonly NuGetClientFactory _clientfactory; 12 | 13 | public PackageMetadataClient(NuGetClientFactory clientFactory) 14 | { 15 | _clientfactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); 16 | } 17 | 18 | public async Task GetRegistrationIndexOrNullAsync( 19 | string packageId, 20 | CancellationToken cancellationToken = default) 21 | { 22 | var client = await _clientfactory.CreatePackageMetadataClientAsync(cancellationToken); 23 | 24 | return await client.GetRegistrationIndexOrNullAsync(packageId, cancellationToken); 25 | } 26 | 27 | public async Task GetRegistrationPageAsync( 28 | string pageUrl, 29 | CancellationToken cancellationToken = default) 30 | { 31 | var client = await _clientfactory.CreatePackageMetadataClientAsync(cancellationToken); 32 | 33 | return await client.GetRegistrationPageAsync(pageUrl, cancellationToken); 34 | } 35 | 36 | public async Task GetRegistrationLeafOrNullAsync( 37 | string packageId, 38 | NuGetVersion packageVersion, 39 | CancellationToken cancellationToken = default) 40 | { 41 | var client = await _clientfactory.CreatePackageMetadataClientAsync(cancellationToken); 42 | 43 | return await client.GetRegistrationLeafOrNullAsync(packageId, packageVersion, cancellationToken); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/PackageContent/PackageContentClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using NugetProxy.Protocol.Models; 6 | using NuGet.Versioning; 7 | 8 | namespace NugetProxy.Protocol.Internal 9 | { 10 | public class PackageContentClient : IPackageContentClient 11 | { 12 | private readonly NuGetClientFactory _clientfactory; 13 | 14 | public PackageContentClient(NuGetClientFactory clientFactory) 15 | { 16 | _clientfactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); 17 | } 18 | 19 | public async Task GetPackageContentStreamOrNullAsync( 20 | string packageId, 21 | NuGetVersion packageVersion, 22 | CancellationToken cancellationToken = default) 23 | { 24 | var client = await _clientfactory.CreatePackageContentClientAsync(cancellationToken); 25 | 26 | return await client.GetPackageContentStreamOrNullAsync(packageId, packageVersion, cancellationToken); 27 | } 28 | 29 | public async Task GetPackageManifestStreamOrNullAsync( 30 | string packageId, 31 | NuGetVersion packageVersion, 32 | CancellationToken cancellationToken = default) 33 | { 34 | var client = await _clientfactory.CreatePackageContentClientAsync(cancellationToken); 35 | 36 | return await client.GetPackageManifestStreamOrNullAsync(packageId, packageVersion, cancellationToken); 37 | } 38 | 39 | public async Task GetPackageVersionsOrNullAsync( 40 | string packageId, 41 | CancellationToken cancellationToken = default) 42 | { 43 | var client = await _clientfactory.CreatePackageContentClientAsync(cancellationToken); 44 | 45 | return await client.GetPackageVersionsOrNullAsync(packageId, cancellationToken); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Converters/CatalogLeafTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NugetProxy.Protocol.Models; 5 | using Newtonsoft.Json; 6 | 7 | namespace NugetProxy.Protocol.Internal 8 | { 9 | /// 10 | /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/CatalogLeafTypeConverter.cs 11 | /// 12 | internal class CatalogLeafTypeConverter : BaseCatalogLeafConverter 13 | { 14 | private static readonly Dictionary FromType = new Dictionary 15 | { 16 | { CatalogLeafType.PackageDelete, "PackageDelete" }, 17 | { CatalogLeafType.PackageDetails, "PackageDetails" }, 18 | }; 19 | 20 | private static readonly Dictionary FromString = FromType 21 | .ToDictionary(x => x.Value, x => x.Key); 22 | 23 | public CatalogLeafTypeConverter() : base(FromType) 24 | { 25 | } 26 | 27 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 28 | { 29 | List types; 30 | if (reader.TokenType == JsonToken.StartArray) 31 | { 32 | types = serializer.Deserialize>(reader); 33 | } 34 | else 35 | { 36 | types = new List { reader.Value }; 37 | } 38 | 39 | foreach (var type in types.OfType()) 40 | { 41 | CatalogLeafType output; 42 | if (FromString.TryGetValue(type, out output)) 43 | { 44 | return output; 45 | } 46 | } 47 | 48 | throw new JsonSerializationException($"Unexpected value for a {nameof(CatalogLeafType)}."); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/RegistrationLeafResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | /// 8 | /// The metadata for a single version of a package. 9 | /// 10 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf 11 | /// 12 | public class RegistrationLeafResponse 13 | { 14 | public static readonly IReadOnlyList DefaultType = new List 15 | { 16 | "Package", 17 | "http://schema.nuget.org/catalog#Permalink" 18 | }; 19 | 20 | /// 21 | /// The URL to the registration leaf. 22 | /// 23 | [JsonProperty("@id")] 24 | public string RegistrationLeafUrl { get; set; } 25 | 26 | /// 27 | /// The registration leaf's type. 28 | /// 29 | [JsonProperty("@type")] 30 | public IReadOnlyList Type { get; set; } 31 | 32 | /// 33 | /// Whether the package is listed. 34 | /// 35 | [JsonProperty("listed")] 36 | public bool Listed { get; set; } 37 | 38 | /// 39 | /// The URL to the package content (.nupkg). 40 | /// 41 | [JsonProperty("packageContent")] 42 | public string PackageContentUrl { get; set; } 43 | 44 | /// 45 | /// The date the package was published. On NuGet.org, 46 | /// is set to the year 1900 if the package is unlisted. 47 | /// 48 | [JsonProperty("published")] 49 | public DateTimeOffset Published { get; set; } 50 | 51 | /// 52 | /// The URL to the package's registration index. 53 | /// 54 | [JsonProperty("registration")] 55 | public string RegistrationIndexUrl { get; set; } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/ProtocolException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | /// 8 | /// An exception that is thrown when an API has returned an unexpected result. 9 | /// 10 | public class ProtocolException : Exception 11 | { 12 | /// 13 | /// Create a new . 14 | /// 15 | /// The HTTP response message. 16 | /// The HTTP request method. 17 | /// The URI that was requested. 18 | /// The response status code. 19 | /// The HTTP reason phrase. 20 | public ProtocolException( 21 | string message, 22 | HttpMethod method, 23 | string requestUri, 24 | HttpStatusCode statusCode, 25 | string reasonPhrase) : base(message) 26 | { 27 | Method = method ?? throw new ArgumentNullException(nameof(method)); 28 | RequestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri)); 29 | StatusCode = statusCode; 30 | ReasonPhrase = reasonPhrase ?? throw new ArgumentNullException(nameof(reasonPhrase)); 31 | } 32 | 33 | /// 34 | /// The HTTP response message. 35 | /// 36 | public HttpMethod Method { get; } 37 | 38 | /// 39 | /// The URI that was requested. 40 | /// 41 | public string RequestUri { get; } 42 | 43 | /// 44 | /// The response status code. 45 | /// 46 | public HttpStatusCode StatusCode { get; } 47 | 48 | /// 49 | /// The response reason phrase. 50 | /// 51 | public string ReasonPhrase { get; } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Extensions/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace NugetProxy.Core 9 | { 10 | public static class StreamExtensions 11 | { 12 | // See: https://github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/System/IO/Stream.cs#L35 13 | private const int DefaultCopyBufferSize = 81920; 14 | 15 | /// 16 | /// Copies a stream to a file, and returns that file as a stream. The underlying file will be 17 | /// deleted when the resulting stream is disposed. 18 | /// 19 | /// The stream to be copied, at its current position. 20 | /// 21 | /// The copied stream, with its position reset to the beginning. 22 | public static async Task AsTemporaryFileStreamAsync( 23 | this Stream original, 24 | CancellationToken cancellationToken = default) 25 | { 26 | var result = new FileStream( 27 | Path.GetTempFileName(), 28 | FileMode.Create, 29 | FileAccess.ReadWrite, 30 | FileShare.None, 31 | DefaultCopyBufferSize, 32 | FileOptions.DeleteOnClose); 33 | 34 | try 35 | { 36 | await original.CopyToAsync(result, DefaultCopyBufferSize, cancellationToken); 37 | result.Position = 0; 38 | } 39 | catch (Exception) 40 | { 41 | result.Dispose(); 42 | throw; 43 | } 44 | 45 | return result; 46 | } 47 | 48 | public static bool Matches(this Stream content, Stream target) 49 | { 50 | using (var sha256 = SHA256.Create()) 51 | { 52 | var contentHash = sha256.ComputeHash(content); 53 | var targetHash = sha256.ComputeHash(target); 54 | 55 | return contentHash.SequenceEqual(targetHash); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/ServiceIndex/BaGetServiceIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using NugetProxy.Protocol.Models; 6 | 7 | namespace NugetProxy.Core 8 | { 9 | public class NugetProxyServiceIndex : IServiceIndexService 10 | { 11 | private readonly IUrlGenerator _url; 12 | 13 | public NugetProxyServiceIndex(IUrlGenerator url) 14 | { 15 | _url = url ?? throw new ArgumentNullException(nameof(url)); 16 | } 17 | 18 | private IEnumerable BuildResource(string name, string url, params string[] versions) 19 | { 20 | foreach (var version in versions) 21 | { 22 | var type = string.IsNullOrEmpty(version) ? name : $"{name}/{version}"; 23 | 24 | yield return new ServiceIndexItem 25 | { 26 | ResourceUrl = url, 27 | Type = type, 28 | }; 29 | } 30 | } 31 | 32 | public Task GetAsync(CancellationToken cancellationToken = default) 33 | { 34 | var resources = new List(); 35 | 36 | resources.AddRange(BuildResource("PackagePublish", _url.GetPackagePublishResourceUrl(), "2.0.0")); 37 | resources.AddRange(BuildResource("SymbolPackagePublish", _url.GetSymbolPublishResourceUrl(), "4.9.0")); 38 | resources.AddRange(BuildResource("SearchQueryService", _url.GetSearchResourceUrl(), "", "3.0.0-beta", "3.0.0-rc")); 39 | resources.AddRange(BuildResource("RegistrationsBaseUrl", _url.GetPackageMetadataResourceUrl(), "", "3.0.0-rc", "3.0.0-beta")); 40 | resources.AddRange(BuildResource("PackageBaseAddress", _url.GetPackageContentResourceUrl(), "3.0.0")); 41 | resources.AddRange(BuildResource("SearchAutocompleteService", _url.GetAutocompleteResourceUrl(), "", "3.0.0-rc", "3.0.0-beta")); 42 | 43 | var result = new ServiceIndexResponse 44 | { 45 | Version = "3.0.0", 46 | Resources = resources, 47 | }; 48 | 49 | return Task.FromResult(result); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/CatalogLeaf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NugetProxy.Protocol.Internal; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | // This classed is based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/CatalogLeaf.cs 8 | 9 | /// 10 | /// A catalog leaf. Represents a single package event. 11 | /// Leafs can be discovered from a . 12 | /// 13 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-leaf 14 | /// 15 | public class CatalogLeaf : ICatalogLeafItem 16 | { 17 | /// 18 | /// The URL to the current catalog leaf. 19 | /// 20 | [JsonProperty("@id")] 21 | public string CatalogLeafUrl { get; set; } 22 | 23 | /// 24 | /// The type of the current catalog leaf. 25 | /// 26 | [JsonProperty("@type")] 27 | [JsonConverter(typeof(CatalogLeafTypeConverter))] 28 | public CatalogLeafType Type { get; set; } 29 | 30 | /// 31 | /// The catalog commit ID associated with this catalog item. 32 | /// 33 | [JsonProperty("catalog:commitId")] 34 | public string CommitId { get; set; } 35 | 36 | /// 37 | /// The commit timestamp of this catalog item. 38 | /// 39 | [JsonProperty("catalog:commitTimeStamp")] 40 | public DateTimeOffset CommitTimestamp { get; set; } 41 | 42 | /// 43 | /// The package ID of the catalog item. 44 | /// 45 | [JsonProperty("id")] 46 | public string PackageId { get; set; } 47 | 48 | /// 49 | /// The published date of the package catalog item. 50 | /// 51 | [JsonProperty("published")] 52 | public DateTimeOffset Published { get; set; } 53 | 54 | /// 55 | /// The package version of the catalog item. 56 | /// 57 | [JsonProperty("version")] 58 | public string PackageVersion { get; set; } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Extensions/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | 8 | namespace NugetProxy.Protocol 9 | { 10 | internal static class HttpClientExtensions 11 | { 12 | internal static readonly JsonSerializer Serializer = JsonSerializer.Create(JsonSettings); 13 | 14 | internal static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings 15 | { 16 | DateTimeZoneHandling = DateTimeZoneHandling.Utc, 17 | DateParseHandling = DateParseHandling.DateTimeOffset, 18 | NullValueHandling = NullValueHandling.Ignore, 19 | }; 20 | 21 | public static async Task> DeserializeUrlAsync( 22 | this HttpClient httpClient, 23 | string documentUrl, 24 | CancellationToken cancellationToken = default) 25 | { 26 | using (var response = await httpClient.GetAsync( 27 | documentUrl, 28 | HttpCompletionOption.ResponseHeadersRead, 29 | cancellationToken)) 30 | { 31 | if (response.StatusCode != HttpStatusCode.OK) 32 | { 33 | return new ResponseAndResult( 34 | HttpMethod.Get, 35 | documentUrl, 36 | response.StatusCode, 37 | response.ReasonPhrase, 38 | hasResult: false, 39 | result: default); 40 | } 41 | 42 | using (var stream = await response.Content.ReadAsStreamAsync()) 43 | using (var textReader = new StreamReader(stream)) 44 | using (var jsonReader = new JsonTextReader(textReader)) 45 | { 46 | return new ResponseAndResult( 47 | HttpMethod.Get, 48 | documentUrl, 49 | response.StatusCode, 50 | response.ReasonPhrase, 51 | hasResult: true, 52 | result: Serializer.Deserialize(jsonReader)); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Mirror/IMirrorService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using NuGet.Versioning; 6 | 7 | namespace NugetProxy.Core 8 | { 9 | /// 10 | /// Indexes packages from an external source. 11 | /// 12 | public interface IMirrorService 13 | { 14 | /// 15 | /// Attempt to find a package's versions using mirroring. This will merge 16 | /// results from the configured upstream source with the locally indexed packages. 17 | /// 18 | /// The package's id to lookup 19 | /// The token to cancel the lookup 20 | /// 21 | /// The package's versions, or null if the package cannot be found on the 22 | /// configured upstream source. This includes unlisted versions. 23 | /// 24 | Task> FindPackageVersionsOrNullAsync(string id, CancellationToken cancellationToken); 25 | 26 | /// 27 | /// Attempt to find a package's metadata using mirroring. This will merge 28 | /// results from the configured upstream source with the locally indexed packages. 29 | /// 30 | /// The package's id to lookup 31 | /// The token to cancel the lookup 32 | /// 33 | /// The package's metadata, or null if the package cannot be found on the configured 34 | /// upstream source. 35 | /// 36 | Task> FindPackagesOrNullAsync(string id, CancellationToken cancellationToken); 37 | 38 | /// 39 | /// If the package is unknown, attempt to index it from an upstream source. 40 | /// 41 | /// The package's id 42 | /// The package's version 43 | /// The token to cancel the mirroring 44 | /// A task that completes when the package has been mirrored. 45 | Task MirrorAsync(string id, NuGetVersion version, CancellationToken cancellationToken); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Thin Nuget Proxy 2 | 3 | [![Build Status](https://chriswoolum.visualstudio.com/Thin%20Nuget%20Proxy/_apis/build/status/cwoolum.ThinNugetProxy?branchName=master)](https://chriswoolum.visualstudio.com/Thin%20Nuget%20Proxy/_build/latest?definitionId=8&branchName=master) 4 | 5 | This is a super thin Nuget Proxy that is used to proxy connections to an Azure Artifacts Nuget Repository. 6 | 7 | It is a super stripped down version of [BaGet](https://github.com/loic-sharma/Baget) 8 | 9 | ## Usage 10 | 11 | It is meant to be used as a [Service Container](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/service-containers?view=azure-devops&tabs=yaml) in Azure Pipelines so that you can eliminate passing a PAT to your Docker builds allowing you to cache your image layers correctly. 12 | 13 | In your Azure Pipleines YAML definition you need to add a service container. 14 | 15 | ``` yaml 16 | resources: 17 | containers: 18 | # This mounts the proxy server as a container. You specify the feed Url for your Azure Artifacts Repo. 19 | # You must make sure that the build agent has access to the feed. If it's in the same account, it should already be configured that way. 20 | - container: nuget 21 | image: cwoolum/thin-nuget-proxy 22 | ports: 23 | - 8080:80 24 | env: 25 | MIRROR__ACCESSTOKEN: $(System.AccessToken) 26 | MIRROR__PACKAGESOURCE: "Feed Url for your Azure Artifacts Repo" 27 | 28 | pool: 29 | vmImage: "ubuntu-16.04" 30 | 31 | services: 32 | nuget: nuget 33 | 34 | steps: 35 | - bash: | 36 | echo -e "\e[34mBuilding Service\e[0m" 37 | docker build -f Dockerfile -t nugettest:latest . 38 | 39 | displayName: "Build, tag and push image nugettest" 40 | failOnStderr: true 41 | 42 | ``` 43 | 44 | In your Dockerfile, you'll want to make sure that you add the host IP as a source. 45 | 46 | ``` dockerfile 47 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS base 48 | WORKDIR /src 49 | 50 | # 172.17.0.1 seems to always be the host IP when used in Azure Pipelines but we don't want to rely on that. 51 | # TODO: Make this work ->> RUN ip -4 route list match 0/0 | cut -d' ' -f3 | awk '{print $1" host.docker.internal"}' >> /etc/hosts 52 | 53 | COPY NugetTest.csproj . 54 | RUN dotnet restore NugetTest.csproj -s http://172.17.0.1:8080/v3/index.json -s https://api.nuget.org/v3/index.json 55 | COPY . . 56 | 57 | RUN dotnet build NugetTest.csproj -c Release -o /app 58 | ``` 59 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Catalog/FileCursor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using Newtonsoft.Json; 7 | 8 | namespace NugetProxy.Protocol.Catalog 9 | { 10 | /// 11 | /// A cursor implementation which stores the cursor in local file. The cursor value is written to the file as 12 | /// a JSON object. 13 | /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/3a468fe534a03dcced897eb5992209fdd3c4b6c9/src/NuGet.Protocol.Catalog/FileCursor.cs 14 | /// 15 | public class FileCursor : ICursor 16 | { 17 | private static readonly JsonSerializerSettings Settings = HttpClientExtensions.JsonSettings; 18 | 19 | private readonly string _path; 20 | private readonly ILogger _logger; 21 | 22 | public FileCursor(string path, ILogger logger) 23 | { 24 | _path = path ?? throw new ArgumentNullException(nameof(path)); 25 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 26 | } 27 | 28 | public Task GetAsync(CancellationToken cancellationToken) 29 | { 30 | try 31 | { 32 | var jsonString = File.ReadAllText(_path); 33 | var data = JsonConvert.DeserializeObject(jsonString, Settings); 34 | _logger.LogDebug("Read cursor value {cursor:O} from {path}.", data.Value, _path); 35 | return Task.FromResult(data.Value); 36 | } 37 | catch (Exception e) when (e is FileNotFoundException || e is JsonException) 38 | { 39 | return Task.FromResult(null); 40 | } 41 | } 42 | 43 | public Task SetAsync(DateTimeOffset value, CancellationToken cancellationToken) 44 | { 45 | var data = new Data { Value = value }; 46 | var jsonString = JsonConvert.SerializeObject(data); 47 | File.WriteAllText(_path, jsonString); 48 | _logger.LogDebug("Wrote cursor value {cursor:O} to {path}.", data.Value, _path); 49 | return Task.CompletedTask; 50 | } 51 | 52 | private class Data 53 | { 54 | [JsonProperty("value")] 55 | public DateTimeOffset Value { get; set; } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Extensions/PackageMetadataModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Protocol.Models; 2 | using NuGet.Versioning; 3 | 4 | namespace NugetProxy.Protocol 5 | { 6 | /// 7 | /// These are documented interpretations of values returned by the 8 | /// Package Metadata resource API. 9 | /// 10 | public static class RegistrationModelExtensions 11 | { 12 | /// 13 | /// Parse the package version as a . 14 | /// 15 | /// The package metadata. 16 | /// The package version. 17 | public static NuGetVersion ParseVersion(this PackageMetadata package) 18 | { 19 | return NuGetVersion.Parse(package.Version); 20 | } 21 | 22 | /// 23 | /// Determines if the provided package metadata represents a listed package. 24 | /// 25 | /// The package metadata. 26 | /// True if the package is listed. 27 | public static bool IsListed(this PackageMetadata package) 28 | { 29 | if (package.Listed.HasValue) 30 | { 31 | return package.Listed.Value; 32 | } 33 | 34 | // A published year of 1900 indicates that this package is unlisted, when the listed property itself is 35 | // not present (legacy behavior). 36 | return package.Published.Year != 1900; 37 | } 38 | 39 | /// 40 | /// Parse the registration page's lower version as a . 41 | /// 42 | /// The registration page. 43 | /// The page's lower version. 44 | public static NuGetVersion ParseLower(this RegistrationIndexPage page) 45 | { 46 | return NuGetVersion.Parse(page.Lower); 47 | } 48 | 49 | /// 50 | /// Parse the registration page's upper version as a . 51 | /// 52 | /// The registration page. 53 | /// The page's upper version. 54 | public static NuGetVersion ParseUpper(this RegistrationIndexPage page) 55 | { 56 | return NuGetVersion.Parse(page.Upper); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/PackageMetadata/IPackageMetadataClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NugetProxy.Protocol.Models; 4 | using NuGet.Versioning; 5 | 6 | namespace NugetProxy.Protocol 7 | { 8 | /// 9 | /// The Package Metadata client, used to fetch packages' metadata. 10 | /// 11 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource 12 | /// 13 | public interface IPackageMetadataClient 14 | { 15 | /// 16 | /// Attempt to get a package's registration index, if it exists. 17 | /// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page 18 | /// 19 | /// The package's ID. 20 | /// A token to cancel the task. 21 | /// The package's registration index, or null if the package does not exist 22 | Task GetRegistrationIndexOrNullAsync(string packageId, CancellationToken cancellationToken = default); 23 | 24 | /// 25 | /// Get a page that was linked from the package's registration index. 26 | /// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page 27 | /// 28 | /// The URL of the page, from the . 29 | /// A token to cancel the task. 30 | /// The registration index page, or null if the page does not exist. 31 | Task GetRegistrationPageAsync( 32 | string pageUrl, 33 | CancellationToken cancellationToken = default); 34 | 35 | /// 36 | /// Get the metadata for a single package version, if the package exists. 37 | /// 38 | /// The package's id. 39 | /// The package's version. 40 | /// A token to cancel the task. 41 | /// The registration leaf, or null if the package does not exist. 42 | Task GetRegistrationLeafOrNullAsync( 43 | string packageId, 44 | NuGetVersion packageVersion, 45 | CancellationToken cancellationToken = default); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Catalog/ICatalogLeafProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NugetProxy.Protocol.Models; 4 | 5 | namespace NugetProxy.Protocol.Catalog 6 | { 7 | /// 8 | /// An interface which allows custom processing of catalog leaves. This interface should be implemented when the 9 | /// catalog leaf documents need to be downloaded and processed in chronological order. 10 | /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/master/src/NuGet.Protocol.Catalog/ICatalogLeafProcessor.cs 11 | /// 12 | public interface ICatalogLeafProcessor 13 | { 14 | /// 15 | /// Process a catalog leaf containing package details. This method should return false or throw an exception 16 | /// if the catalog leaf cannot be processed. In this case, the will stop 17 | /// processing items. Note that the same package ID/version combination can be passed to this multiple times, 18 | /// for example due to an edit in the package metadata or due to a transient error and retry on the part of the 19 | /// . 20 | /// 21 | /// The leaf document. 22 | /// A token to cancel the task. 23 | /// True, if the leaf was successfully processed. False, otherwise. 24 | Task ProcessPackageDetailsAsync(PackageDetailsCatalogLeaf leaf, CancellationToken cancellationToken = default); 25 | 26 | /// 27 | /// Process a catalog leaf containing a package delete. This method should return false or throw an exception 28 | /// if the catalog leaf cannot be processed. In this case, the will stop 29 | /// processing items. Note that the same package ID/version combination can be passed to this multiple times, 30 | /// for example due to a package being deleted again due to a transient error and retry on the part of the 31 | /// . 32 | /// 33 | /// The leaf document. 34 | /// A token to cancel the task. 35 | /// True, if the leaf was successfully processed. False, otherwise. 36 | Task ProcessPackageDeleteAsync(PackageDeleteCatalogLeaf leaf, CancellationToken cancellationToken = default); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/Controllers/SearchController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Core; 5 | using NugetProxy.Protocol.Models; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace NugetProxy.Controllers 9 | { 10 | public class SearchController : Controller 11 | { 12 | private readonly ISearchService _searchService; 13 | 14 | public SearchController(ISearchService searchService) 15 | { 16 | _searchService = searchService ?? throw new ArgumentNullException(nameof(searchService)); 17 | } 18 | 19 | public async Task> SearchAsync( 20 | [FromQuery(Name = "q")] string query = null, 21 | [FromQuery]int skip = 0, 22 | [FromQuery]int take = 20, 23 | [FromQuery]bool prerelease = false, 24 | [FromQuery]string semVerLevel = null, 25 | 26 | // These are unofficial parameters 27 | [FromQuery]string packageType = null, 28 | [FromQuery]string framework = null, 29 | CancellationToken cancellationToken = default) 30 | { 31 | 32 | var includeSemVer2 = semVerLevel == "2.0.0"; 33 | 34 | return await _searchService.SearchAsync( 35 | query ?? string.Empty, 36 | skip, 37 | take, 38 | prerelease, 39 | includeSemVer2, 40 | packageType, 41 | framework, 42 | cancellationToken); 43 | } 44 | 45 | public async Task> AutocompleteAsync( 46 | [FromQuery(Name = "q")] string query = null, 47 | CancellationToken cancellationToken = default) 48 | { 49 | // TODO: Add other autocomplete parameters 50 | // TODO: Support versions autocomplete. 51 | // See: https://github.com/loic-sharma/NugetProxy/issues/291 52 | return await _searchService.AutocompleteAsync( 53 | query, 54 | cancellationToken: cancellationToken); 55 | } 56 | 57 | public async Task> DependentsAsync( 58 | [FromQuery] string packageId, 59 | CancellationToken cancellationToken = default) 60 | { 61 | // TODO: Add other dependents parameters. 62 | return await _searchService.FindDependentsAsync( 63 | packageId, 64 | cancellationToken: cancellationToken); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Content/DatabasePackageContentService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using NugetProxy.Protocol.Models; 7 | using NuGet.Versioning; 8 | 9 | namespace NugetProxy.Core.Content 10 | { 11 | /// 12 | /// Implements the NuGet Package Content resource. Supports read-through caching. 13 | /// Tracks state in a database () and stores packages 14 | /// using . 15 | /// 16 | public class DatabasePackageContentService : IPackageContentService 17 | { 18 | private readonly IMirrorService _mirror; 19 | 20 | 21 | public DatabasePackageContentService( 22 | IMirrorService mirror) 23 | { 24 | _mirror = mirror ?? throw new ArgumentNullException(nameof(mirror)); 25 | 26 | } 27 | 28 | public async Task GetPackageVersionsOrNullAsync( 29 | string id, 30 | CancellationToken cancellationToken = default) 31 | { 32 | // First, attempt to find all package versions using the upstream source. 33 | var versions = await _mirror.FindPackageVersionsOrNullAsync(id, cancellationToken); 34 | 35 | 36 | return new PackageVersionsResponse 37 | { 38 | Versions = versions 39 | .Select(v => v.ToNormalizedString()) 40 | .Select(v => v.ToLowerInvariant()) 41 | .ToList() 42 | }; 43 | } 44 | 45 | public async Task GetPackageContentStreamOrNullAsync( 46 | string id, 47 | NuGetVersion version, 48 | CancellationToken cancellationToken = default) 49 | { 50 | // Allow read-through caching if it is configured. 51 | return await _mirror.MirrorAsync(id, version, cancellationToken); 52 | } 53 | 54 | public async Task GetPackageManifestStreamOrNullAsync(string id, NuGetVersion version, CancellationToken cancellationToken = default) 55 | { 56 | // Allow read-through caching if it is configured. 57 | return await _mirror.MirrorAsync(id, version, cancellationToken); 58 | 59 | 60 | } 61 | 62 | public async Task GetPackageReadmeStreamOrNullAsync(string id, NuGetVersion version, CancellationToken cancellationToken = default) 63 | { 64 | // Allow read-through caching if it is configured. 65 | return await _mirror.MirrorAsync(id, version, cancellationToken); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Catalog/RawCatalogClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using NugetProxy.Protocol.Models; 6 | 7 | namespace NugetProxy.Protocol.Internal 8 | { 9 | public class RawCatalogClient : ICatalogClient 10 | { 11 | private readonly HttpClient _httpClient; 12 | private readonly string _catalogUrl; 13 | 14 | public RawCatalogClient(HttpClient httpClient, string catalogUrl) 15 | { 16 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 17 | _catalogUrl = catalogUrl;// ?? throw new ArgumentNullException(nameof(catalogUrl)); 18 | } 19 | 20 | public async Task GetIndexAsync(CancellationToken cancellationToken = default) 21 | { 22 | var response = await _httpClient.DeserializeUrlAsync(_catalogUrl, cancellationToken); 23 | 24 | return response.GetResultOrThrow(); 25 | } 26 | 27 | public async Task GetPageAsync(string pageUrl, CancellationToken cancellationToken = default) 28 | { 29 | var response = await _httpClient.DeserializeUrlAsync(pageUrl, cancellationToken); 30 | 31 | return response.GetResultOrThrow(); 32 | } 33 | 34 | public async Task GetPackageDeleteLeafAsync(string leafUrl, CancellationToken cancellationToken = default) 35 | { 36 | return await GetAndValidateLeafAsync( 37 | CatalogLeafType.PackageDelete, 38 | leafUrl, 39 | cancellationToken); 40 | } 41 | 42 | public async Task GetPackageDetailsLeafAsync(string leafUrl, CancellationToken cancellationToken = default) 43 | { 44 | return await GetAndValidateLeafAsync( 45 | CatalogLeafType.PackageDetails, 46 | leafUrl, 47 | cancellationToken); 48 | } 49 | 50 | private async Task GetAndValidateLeafAsync( 51 | CatalogLeafType type, 52 | string leafUrl, 53 | CancellationToken cancellationToken) where T : CatalogLeaf 54 | { 55 | var result = await _httpClient.DeserializeUrlAsync(leafUrl, cancellationToken); 56 | var leaf = result.GetResultOrThrow(); 57 | 58 | if (leaf.Type != type) 59 | { 60 | throw new ArgumentException( 61 | $"The leaf type found in the document does not match the expected '{type}' type.", 62 | nameof(type)); 63 | } 64 | 65 | return leaf; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Catalog/ICatalogClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NugetProxy.Protocol.Models; 4 | 5 | namespace NugetProxy.Protocol 6 | { 7 | /// 8 | /// The Catalog client, used to discover package events. 9 | /// You can use this resource to query for all published packages. 10 | /// 11 | /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource 12 | /// 13 | public interface ICatalogClient 14 | { 15 | /// 16 | /// Get the entry point for the catalog resource. 17 | /// See: https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-index 18 | /// 19 | /// A token to cancel the task. 20 | /// The catalog index. 21 | Task GetIndexAsync(CancellationToken cancellationToken = default); 22 | 23 | /// 24 | /// Get a single catalog page, used to discover catalog leafs. 25 | /// See: https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-page 26 | /// 27 | /// The URL of the page, from the . 28 | /// A token to cancel the task. 29 | /// A catalog page. 30 | Task GetPageAsync( 31 | string pageUrl, 32 | CancellationToken cancellationToken = default); 33 | 34 | /// 35 | /// Get a single catalog leaf, representing a package deletion event. 36 | /// See: https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-leaf 37 | /// 38 | /// The URL of the leaf, from a . 39 | /// A token to cancel the task. 40 | /// A catalog leaf. 41 | Task GetPackageDeleteLeafAsync( 42 | string leafUrl, 43 | CancellationToken cancellationToken = default); 44 | 45 | /// 46 | /// Get a single catalog leaf, representing a package creation or update event. 47 | /// See: https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-leaf 48 | /// 49 | /// The URL of the leaf, from a . 50 | /// A token to cancel the task. 51 | /// A catalog leaf. 52 | Task GetPackageDetailsLeafAsync( 53 | string leafUrl, 54 | CancellationToken cancellationToken = default); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/PackageContent/IPackageContentClient.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Protocol.Models; 5 | using NuGet.Versioning; 6 | 7 | namespace NugetProxy.Protocol 8 | { 9 | /// 10 | /// The Package Content resource, used to download NuGet packages and to fetch other metadata. 11 | /// 12 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource 13 | /// 14 | public interface IPackageContentClient 15 | { 16 | /// 17 | /// Get a package's versions, or null if the package does not exist. 18 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions 19 | /// 20 | /// The package ID. 21 | /// A token to cancel the task. 22 | /// The package's versions, or null if the package does not exist. 23 | Task GetPackageVersionsOrNullAsync( 24 | string packageId, 25 | CancellationToken cancellationToken = default); 26 | 27 | /// 28 | /// Download a package, or null if the package does not exist. 29 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg 30 | /// 31 | /// The package ID. 32 | /// The package's version. 33 | /// A token to cancel the task. 34 | /// 35 | /// The package's content stream, or null if the package does not exist. The stream may not be seekable. 36 | /// 37 | Task GetPackageContentStreamOrNullAsync( 38 | string packageId, 39 | NuGetVersion packageVersion, 40 | CancellationToken cancellationToken = default); 41 | 42 | /// 43 | /// Download a package's manifest (nuspec), or null if the package does not exist. 44 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec 45 | /// 46 | /// The package id. 47 | /// The package's version. 48 | /// A token to cancel the task. 49 | /// 50 | /// The package's manifest stream, or null if the package does not exist. The stream may not be seekable. 51 | /// 52 | Task GetPackageManifestStreamOrNullAsync( 53 | string packageId, 54 | NuGetVersion packageVersion, 55 | CancellationToken cancellationToken = default); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Entities/Package.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NuGet.Versioning; 4 | 5 | namespace NugetProxy.Core 6 | { 7 | // See NuGetGallery's: https://github.com/NuGet/NuGetGallery/blob/master/src/NuGetGallery.Core/Entities/Package.cs 8 | public class Package 9 | { 10 | public int Key { get; set; } 11 | 12 | public string Id { get; set; } 13 | 14 | public NuGetVersion Version 15 | { 16 | get 17 | { 18 | // Favor the original version string as it contains more information. 19 | // Packages uploaded with older versions of NugetProxy may not have the original version string. 20 | return NuGetVersion.Parse( 21 | OriginalVersionString != null 22 | ? OriginalVersionString 23 | : NormalizedVersionString); 24 | } 25 | 26 | set 27 | { 28 | NormalizedVersionString = value.ToNormalizedString().ToLowerInvariant(); 29 | OriginalVersionString = value.OriginalVersion; 30 | } 31 | } 32 | 33 | public string[] Authors { get; set; } 34 | public string Description { get; set; } 35 | public long Downloads { get; set; } 36 | public bool HasReadme { get; set; } 37 | public bool IsPrerelease { get; set; } 38 | public string Language { get; set; } 39 | public bool Listed { get; set; } 40 | public string MinClientVersion { get; set; } 41 | public DateTime Published { get; set; } 42 | public bool RequireLicenseAcceptance { get; set; } 43 | public SemVerLevel SemVerLevel { get; set; } 44 | public string Summary { get; set; } 45 | public string Title { get; set; } 46 | 47 | public Uri IconUrl { get; set; } 48 | public Uri LicenseUrl { get; set; } 49 | public Uri ProjectUrl { get; set; } 50 | 51 | public Uri RepositoryUrl { get; set; } 52 | public string RepositoryType { get; set; } 53 | 54 | public string[] Tags { get; set; } 55 | 56 | /// 57 | /// Used for optimistic concurrency. 58 | /// 59 | public byte[] RowVersion { get; set; } 60 | 61 | public List Dependencies { get; set; } 62 | public List PackageTypes { get; set; } 63 | public List TargetFrameworks { get; set; } 64 | 65 | public string NormalizedVersionString { get; set; } 66 | public string OriginalVersionString { get; set; } 67 | 68 | 69 | public string IconUrlString => IconUrl?.AbsoluteUri ?? string.Empty; 70 | public string LicenseUrlString => LicenseUrl?.AbsoluteUri ?? string.Empty; 71 | public string ProjectUrlString => ProjectUrl?.AbsoluteUri ?? string.Empty; 72 | public string RepositoryUrlString => RepositoryUrl?.AbsoluteUri ?? string.Empty; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/Controllers/PackageContentController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Core.Content; 5 | using NugetProxy.Protocol.Models; 6 | using Microsoft.AspNetCore.Mvc; 7 | using NuGet.Versioning; 8 | 9 | namespace NugetProxy.Controllers 10 | { 11 | /// 12 | /// The Package Content resource, used to download content from packages. 13 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource 14 | /// 15 | public class PackageContentController : Controller 16 | { 17 | private readonly IPackageContentService _content; 18 | 19 | public PackageContentController(IPackageContentService content) 20 | { 21 | _content = content ?? throw new ArgumentNullException(nameof(content)); 22 | } 23 | 24 | public async Task> GetPackageVersionsAsync(string id, CancellationToken cancellationToken) 25 | { 26 | var versions = await _content.GetPackageVersionsOrNullAsync(id, cancellationToken); 27 | if (versions == null) 28 | { 29 | return NotFound(); 30 | } 31 | 32 | return versions; 33 | } 34 | 35 | public async Task DownloadPackageAsync(string id, string version, CancellationToken cancellationToken) 36 | { 37 | if (!NuGetVersion.TryParse(version, out var nugetVersion)) 38 | { 39 | return NotFound(); 40 | } 41 | 42 | var packageStream = await _content.GetPackageContentStreamOrNullAsync(id, nugetVersion, cancellationToken); 43 | if (packageStream == null) 44 | { 45 | return NotFound(); 46 | } 47 | 48 | return File(packageStream, "application/octet-stream"); 49 | } 50 | 51 | public async Task DownloadNuspecAsync(string id, string version, CancellationToken cancellationToken) 52 | { 53 | if (!NuGetVersion.TryParse(version, out var nugetVersion)) 54 | { 55 | return NotFound(); 56 | } 57 | 58 | var nuspecStream = await _content.GetPackageManifestStreamOrNullAsync(id, nugetVersion, cancellationToken); 59 | if (nuspecStream == null) 60 | { 61 | return NotFound(); 62 | } 63 | 64 | return File(nuspecStream, "text/xml"); 65 | } 66 | 67 | public async Task DownloadReadmeAsync(string id, string version, CancellationToken cancellationToken) 68 | { 69 | if (!NuGetVersion.TryParse(version, out var nugetVersion)) 70 | { 71 | return NotFound(); 72 | } 73 | 74 | var readmeStream = await _content.GetPackageReadmeStreamOrNullAsync(id, nugetVersion, cancellationToken); 75 | if (readmeStream == null) 76 | { 77 | return NotFound(); 78 | } 79 | 80 | return File(readmeStream, "text/markdown"); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Search/NullSearchService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Protocol.Models; 5 | 6 | namespace NugetProxy.Core 7 | { 8 | /// 9 | /// A minimal search service implementation, used for advanced scenarios. 10 | /// 11 | public class NullSearchService : ISearchService 12 | { 13 | private static readonly IReadOnlyList EmptyStringList = new List(); 14 | 15 | private static readonly Task EmptyAutocompleteResponseTask = 16 | Task.FromResult(new AutocompleteResponse 17 | { 18 | TotalHits = 0, 19 | Data = EmptyStringList, 20 | Context = AutocompleteContext.Default 21 | }); 22 | 23 | private static readonly Task EmptyDependentsResponseTask = 24 | Task.FromResult(new DependentsResponse 25 | { 26 | TotalHits = 0, 27 | Data = EmptyStringList 28 | }); 29 | 30 | private static readonly Task EmptySearchResponseTask = 31 | Task.FromResult(new SearchResponse 32 | { 33 | TotalHits = 0, 34 | Data = new List() 35 | }); 36 | 37 | public Task AutocompleteAsync( 38 | string query = null, 39 | AutocompleteType type = AutocompleteType.PackageIds, 40 | int skip = 0, 41 | int take = 20, 42 | bool includePrerelease = true, 43 | bool includeSemVer2 = true, 44 | CancellationToken cancellationToken = default) 45 | { 46 | return EmptyAutocompleteResponseTask; 47 | } 48 | 49 | public Task FindDependentsAsync( 50 | string packageId, 51 | int skip = 0, 52 | int take = 20, 53 | CancellationToken cancellationToken = default) 54 | { 55 | return EmptyDependentsResponseTask; 56 | } 57 | 58 | public Task IndexAsync(Package package, CancellationToken cancellationToken = default) 59 | { 60 | return Task.CompletedTask; 61 | } 62 | 63 | public Task SearchAsync( 64 | string query = null, 65 | int skip = 0, 66 | int take = 20, 67 | bool includePrerelease = true, 68 | bool includeSemVer2 = true, 69 | string packageType = null, 70 | string framework = null, 71 | CancellationToken cancellationToken = default) 72 | { 73 | return EmptySearchResponseTask; 74 | } 75 | 76 | public Task SearchAsync( 77 | string query = null, 78 | int skip = 0, 79 | int take = 20, 80 | bool includePrerelease = true, 81 | bool includeSemVer2 = true, 82 | CancellationToken cancellationToken = default) 83 | { 84 | return EmptySearchResponseTask; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/PackageMetadata/RawPackageMetadataClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using NugetProxy.Protocol.Models; 7 | using NuGet.Versioning; 8 | 9 | namespace NugetProxy.Protocol.Internal 10 | { 11 | /// 12 | /// The client to interact with an upstream source's Package Metadata resource. 13 | /// 14 | public class RawPackageMetadataClient : IPackageMetadataClient 15 | { 16 | private readonly HttpClient _httpClient; 17 | private readonly string _packageMetadataUrl; 18 | 19 | /// 20 | /// Create a new Package Metadata client. 21 | /// 22 | /// The HTTP client used to send requests. 23 | /// The NuGet server's registration resource URL. 24 | public RawPackageMetadataClient(HttpClient httpClient, string registrationBaseUrl) 25 | { 26 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 27 | _packageMetadataUrl = registrationBaseUrl ?? throw new ArgumentNullException(nameof(registrationBaseUrl)); 28 | } 29 | 30 | /// 31 | public async Task GetRegistrationIndexOrNullAsync( 32 | string packageId, 33 | CancellationToken cancellationToken = default) 34 | { 35 | var url = $"{_packageMetadataUrl}/{packageId.ToLowerInvariant()}/index.json"; 36 | var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); 37 | 38 | if (response.StatusCode == HttpStatusCode.NotFound) 39 | { 40 | return null; 41 | } 42 | 43 | return response.GetResultOrThrow(); 44 | } 45 | 46 | /// 47 | public async Task GetRegistrationPageAsync( 48 | string pageUrl, 49 | CancellationToken cancellationToken = default) 50 | { 51 | var response = await _httpClient.DeserializeUrlAsync(pageUrl, cancellationToken); 52 | 53 | return response.GetResultOrThrow(); 54 | } 55 | 56 | /// 57 | public async Task GetRegistrationLeafOrNullAsync( 58 | string packageId, 59 | NuGetVersion packageVersion, 60 | CancellationToken cancellationToken = default) 61 | { 62 | var id = packageId.ToLowerInvariant(); 63 | var version = packageVersion.ToNormalizedString().ToLowerInvariant(); 64 | 65 | var url = $"{_packageMetadataUrl}/{id}/{version}.json"; 66 | var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); 67 | 68 | if (response.StatusCode == HttpStatusCode.NotFound) 69 | { 70 | return null; 71 | } 72 | 73 | return response.GetResultOrThrow(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/SearchResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NugetProxy.Protocol.Internal; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | /// 8 | /// A package that matched a search query. 9 | /// 10 | /// See https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result 11 | /// 12 | public class SearchResult 13 | { 14 | /// 15 | /// The ID of the matched package. 16 | /// 17 | [JsonProperty("id")] 18 | public string PackageId { get; set; } 19 | 20 | /// 21 | /// The latest version of the matched pacakge. This is the full NuGet version after normalization, 22 | /// including any SemVer 2.0.0 build metadata. 23 | /// 24 | [JsonProperty("version")] 25 | public string Version { get; set; } 26 | 27 | /// 28 | /// The description of the matched package. 29 | /// 30 | [JsonProperty("description")] 31 | public string Description { get; set; } 32 | 33 | /// 34 | /// The authors of the matched package. 35 | /// 36 | [JsonProperty("authors")] 37 | [JsonConverter(typeof(SingleOrListConverter))] 38 | public IReadOnlyList Authors { get; set; } 39 | 40 | /// 41 | /// The URL of the matched package's icon. 42 | /// 43 | [JsonProperty("iconUrl")] 44 | public string IconUrl { get; set; } 45 | 46 | /// 47 | /// The URL of the matched package's license. 48 | /// 49 | [JsonProperty("licenseUrl")] 50 | public string LicenseUrl { get; set; } 51 | 52 | /// 53 | /// The URL of the matched package's homepage. 54 | /// 55 | [JsonProperty("projectUrl")] 56 | public string ProjectUrl { get; set; } 57 | 58 | /// 59 | /// The URL for the matched package's registration index. 60 | /// 61 | [JsonProperty("registration")] 62 | public string RegistrationIndexUrl { get; set; } 63 | 64 | /// 65 | /// The summary of the matched package. 66 | /// 67 | [JsonProperty("summary")] 68 | public string Summary { get; set; } 69 | 70 | /// 71 | /// The tags of the matched package. 72 | /// 73 | [JsonProperty("tags")] 74 | public IReadOnlyList Tags { get; set; } 75 | 76 | /// 77 | /// The title of the matched package. 78 | /// 79 | [JsonProperty("title")] 80 | public string Title { get; set; } 81 | 82 | /// 83 | /// The total downloads for all versions of the matched package. 84 | /// 85 | [JsonProperty("totalDownloads")] 86 | public long TotalDownloads { get; set; } 87 | 88 | /// 89 | /// The versions of the matched package. 90 | /// 91 | [JsonProperty("versions")] 92 | public IReadOnlyList Versions { get; set; } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/NugetProxy/Extensions/IServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Core; 2 | using NugetProxy.Core.Content; 3 | using NugetProxy.Core.Server.Extensions; 4 | using NugetProxy.Protocol; 5 | 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Options; 9 | 10 | using System.Net; 11 | using System.Net.Http; 12 | using System.Reflection; 13 | 14 | namespace NugetProxy.Extensions 15 | { 16 | public static class IServiceCollectionExtensions 17 | { 18 | public static IServiceCollection ConfigureNugetProxy( 19 | this IServiceCollection services, 20 | IConfiguration configuration, 21 | bool httpServices = false) 22 | { 23 | services.ConfigureAndValidate(configuration.GetSection("Mirror")); 24 | 25 | if (httpServices) 26 | { 27 | services.ConfigureHttpServices(); 28 | } 29 | 30 | services.AddSingleton(); 31 | services.AddSingleton(); 32 | services.AddSingleton(); 33 | services.AddSingleton(); 34 | services.AddMirrorServices(); 35 | 36 | return services; 37 | } 38 | 39 | /// 40 | /// Add the services that mirror an upstream package source. 41 | /// 42 | /// The defined services. 43 | public static IServiceCollection AddMirrorServices(this IServiceCollection services) 44 | { 45 | services.AddTransient(); 46 | 47 | services.AddTransient(provider => 48 | { 49 | return provider.GetRequiredService(); 50 | }); 51 | 52 | services.AddSingleton(); 53 | services.AddSingleton(provider => 54 | { 55 | var httpClient = provider.GetRequiredService(); 56 | var options = provider.GetRequiredService>(); 57 | 58 | return new NuGetClientFactory( 59 | httpClient, 60 | options.Value.PackageSource.ToString(), 61 | options.Value.AccessToken); 62 | }); 63 | 64 | services.AddSingleton(provider => 65 | { 66 | var assembly = Assembly.GetEntryAssembly(); 67 | var assemblyName = assembly.GetName().Name; 68 | var assemblyVersion = assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; 69 | 70 | var client = new HttpClient(new HttpClientHandler 71 | { 72 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, 73 | }); 74 | 75 | client.DefaultRequestHeaders.Add("User-Agent", $"{assemblyName}/{assemblyVersion}"); 76 | 77 | return client; 78 | }); 79 | 80 | services.AddSingleton(); 81 | 82 | return services; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This is the default for the codeline. 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{htm,html,js,ts,tsx,css,sass,scss,less,svg,vue}] 11 | indent_size = 2 12 | 13 | [*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}] 14 | indent_size = 2 15 | 16 | [*.json] 17 | indent_size = 2 18 | 19 | [*.{ps1,psm1}] 20 | indent_size = 4 21 | 22 | [*.sh] 23 | indent_size = 4 24 | end_of_line = lf 25 | 26 | [*.cs] 27 | indent_size = 4 28 | 29 | # .NET Coding Conventions 30 | 31 | # Organize usings 32 | dotnet_sort_system_directives_first = true:warning 33 | 34 | # this. preferences 35 | dotnet_style_qualification_for_field = false:warning 36 | dotnet_style_qualification_for_property = false:warning 37 | dotnet_style_qualification_for_method = false:warning 38 | dotnet_style_qualification_for_event = false:warning 39 | 40 | # Language keywords vs BCL type preferences 41 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 42 | dotnet_style_predefined_type_for_member_access = true:warning 43 | 44 | # Modifier preferences 45 | dotnet_style_require_accessibility_modifiers = always:warning 46 | dotnet_style_readonly_field = true:warning 47 | 48 | # Naming Conventions 49 | 50 | # Style Definitions 51 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 52 | 53 | # Use PascalCase for constant fields 54 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 55 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 56 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 57 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 58 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 59 | dotnet_naming_symbols.constant_fields.required_modifiers = const 60 | 61 | # C# Code Style Rules 62 | 63 | # var preferences 64 | csharp_style_var_for_built_in_types = true:warning 65 | csharp_style_var_when_type_is_apparent = true:warning 66 | csharp_style_var_elsewhere = true:warning 67 | 68 | # Modifier preferences 69 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 70 | 71 | # New line preferences 72 | csharp_new_line_before_open_brace = all 73 | csharp_new_line_before_else = true 74 | csharp_new_line_before_catch = true 75 | csharp_new_line_before_finally = true 76 | 77 | # Indentation preferences 78 | csharp_indent_case_contents = true 79 | csharp_indent_switch_labels = true 80 | 81 | # Space preferences 82 | csharp_space_after_cast = false 83 | csharp_space_after_keywords_in_control_flow_statements = true 84 | csharp_space_between_method_call_parameter_list_parentheses = false 85 | csharp_space_between_method_declaration_parameter_list_parentheses = false 86 | csharp_space_between_parentheses = false 87 | csharp_space_before_colon_in_inheritance_clause = true 88 | csharp_space_after_colon_in_inheritance_clause = true 89 | csharp_space_around_binary_operators = before_and_after 90 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 91 | csharp_space_between_method_call_name_and_opening_parenthesis = false 92 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 93 | 94 | # Wrapping preferences 95 | csharp_preserve_single_line_statements = true 96 | csharp_preserve_single_line_blocks = true 97 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Extensions/ServiceIndexModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using NugetProxy.Protocol.Models; 3 | 4 | namespace NugetProxy.Protocol 5 | { 6 | /// 7 | /// These are documented interpretations of values returned by the Service Index resource. 8 | /// 9 | public static class ServiceIndexModelExtensions 10 | { 11 | // See: https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/Constants.cs 12 | private static readonly string Version200 = "/2.0.0"; 13 | private static readonly string Version300beta = "/3.0.0-beta"; 14 | private static readonly string Version300 = "/3.0.0"; 15 | private static readonly string Version340 = "/3.4.0"; 16 | private static readonly string Version360 = "/3.6.0"; 17 | private static readonly string Version470 = "/4.7.0"; 18 | private static readonly string Version490 = "/4.9.0"; 19 | 20 | private static readonly string[] Catalog = { "Catalog" + Version300 }; 21 | private static readonly string[] SearchQueryService = { "SearchQueryService" + Version340, "SearchQueryService" + Version300beta, "SearchQueryService" }; 22 | private static readonly string[] RegistrationsBaseUrl = { "RegistrationsBaseUrl" + Version360, "RegistrationsBaseUrl" + Version340, "RegistrationsBaseUrl" + Version300beta, "RegistrationsBaseUrl" }; 23 | private static readonly string[] SearchAutocompleteService = { "SearchAutocompleteService", "SearchAutocompleteService" + Version300beta }; 24 | private static readonly string[] ReportAbuse = { "ReportAbuseUriTemplate", "ReportAbuseUriTemplate" + Version300 }; 25 | private static readonly string[] LegacyGallery = { "LegacyGallery" + Version200 }; 26 | private static readonly string[] PackagePublish = { "PackagePublish" + Version200 }; 27 | private static readonly string[] PackageBaseAddress = { "PackageBaseAddress" + Version300 }; 28 | private static readonly string[] RepositorySignatures = { "RepositorySignatures" + Version490, "RepositorySignatures" + Version470 }; 29 | private static readonly string[] SymbolPackagePublish = { "SymbolPackagePublish" + Version490 }; 30 | 31 | public static string GetPackageContentResourceUrl(this ServiceIndexResponse serviceIndex) 32 | { 33 | return serviceIndex.GetResourceUrl(PackageBaseAddress); 34 | } 35 | 36 | public static string GetPackageMetadataResourceUrl(this ServiceIndexResponse serviceIndex) 37 | { 38 | return serviceIndex.GetResourceUrl(RegistrationsBaseUrl); 39 | } 40 | 41 | public static string GetCatalogResourceUrl(this ServiceIndexResponse serviceIndex) 42 | { 43 | return serviceIndex.GetResourceUrl(Catalog); 44 | } 45 | 46 | public static string GetSearchQueryResourceUrl(this ServiceIndexResponse serviceIndex) 47 | { 48 | return serviceIndex.GetResourceUrl(SearchQueryService); 49 | } 50 | 51 | public static string GetSearchAutocompleteResourceUrl(this ServiceIndexResponse serviceIndex) 52 | { 53 | return serviceIndex.GetResourceUrl(SearchAutocompleteService); 54 | } 55 | 56 | public static string GetResourceUrl(this ServiceIndexResponse serviceIndex, string[] types) 57 | { 58 | var resource = types.SelectMany(t => serviceIndex.Resources.Where(r => r.Type == t)).FirstOrDefault(); 59 | 60 | return resource?.ResourceUrl.Trim('/'); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NugetProxy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29104.9 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0B44364D-952B-497A-82E0-C9AAE94E0369}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NugetProxy.Protocol", "src\NugetProxy.Protocol\NugetProxy.Protocol.csproj", "{8AECA9D7-95CA-4362-A116-EC549A3DCA72}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NugetProxy.Core", "src\NugetProxy.Core\NugetProxy.Core.csproj", "{71141BB1-1322-4356-966B-AC81D0B6EAB9}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NugetProxy.Core.Server", "src\NugetProxy.Core.Server\NugetProxy.Core.Server.csproj", "{E177C33D-3360-43B3-9CDF-3492B08A49FA}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NugetProxy", "src\NugetProxy\NugetProxy.csproj", "{681F3956-8C95-482B-AA0A-A605DD2C3939}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {8AECA9D7-95CA-4362-A116-EC549A3DCA72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {8AECA9D7-95CA-4362-A116-EC549A3DCA72}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {8AECA9D7-95CA-4362-A116-EC549A3DCA72}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {8AECA9D7-95CA-4362-A116-EC549A3DCA72}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {71141BB1-1322-4356-966B-AC81D0B6EAB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {71141BB1-1322-4356-966B-AC81D0B6EAB9}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {71141BB1-1322-4356-966B-AC81D0B6EAB9}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {71141BB1-1322-4356-966B-AC81D0B6EAB9}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {E177C33D-3360-43B3-9CDF-3492B08A49FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {E177C33D-3360-43B3-9CDF-3492B08A49FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {E177C33D-3360-43B3-9CDF-3492B08A49FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {E177C33D-3360-43B3-9CDF-3492B08A49FA}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {681F3956-8C95-482B-AA0A-A605DD2C3939}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {681F3956-8C95-482B-AA0A-A605DD2C3939}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {681F3956-8C95-482B-AA0A-A605DD2C3939}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {681F3956-8C95-482B-AA0A-A605DD2C3939}.Release|Any CPU.Build.0 = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(SolutionProperties) = preSolution 45 | HideSolutionNode = FALSE 46 | EndGlobalSection 47 | GlobalSection(NestedProjects) = preSolution 48 | {8AECA9D7-95CA-4362-A116-EC549A3DCA72} = {26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC} 49 | {71141BB1-1322-4356-966B-AC81D0B6EAB9} = {26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC} 50 | {E177C33D-3360-43B3-9CDF-3492B08A49FA} = {26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC} 51 | {681F3956-8C95-482B-AA0A-A605DD2C3939} = {26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC} 52 | EndGlobalSection 53 | GlobalSection(ExtensibilityGlobals) = postSolution 54 | SolutionGuid = {1423C027-2C90-417F-8629-2A4CF107C055} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Content/IPackageContentService.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NugetProxy.Protocol.Models; 5 | using NuGet.Versioning; 6 | 7 | namespace NugetProxy.Core.Content 8 | { 9 | /// 10 | /// The Package Content resource, used to download NuGet packages and to fetch other metadata. 11 | /// 12 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource 13 | /// 14 | public interface IPackageContentService 15 | { 16 | /// 17 | /// Get a package's versions, or null if the package does not exist. 18 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions 19 | /// 20 | /// The package ID. 21 | /// A token to cancel the task. 22 | /// The package's versions, or null if the package does not exist. 23 | Task GetPackageVersionsOrNullAsync( 24 | string packageId, 25 | CancellationToken cancellationToken = default); 26 | 27 | /// 28 | /// Download a package, or null if the package does not exist. 29 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg 30 | /// 31 | /// The package ID. 32 | /// The package's version. 33 | /// A token to cancel the task. 34 | /// 35 | /// The package's content stream, or null if the package does not exist. The stream may not be seekable. 36 | /// 37 | Task GetPackageContentStreamOrNullAsync( 38 | string packageId, 39 | NuGetVersion packageVersion, 40 | CancellationToken cancellationToken = default); 41 | 42 | /// 43 | /// Download a package's manifest (nuspec), or null if the package does not exist. 44 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec 45 | /// 46 | /// The package id. 47 | /// The package's version. 48 | /// A token to cancel the task. 49 | /// 50 | /// The package's manifest stream, or null if the package does not exist. The stream may not be seekable. 51 | /// 52 | Task GetPackageManifestStreamOrNullAsync( 53 | string packageId, 54 | NuGetVersion packageVersion, 55 | CancellationToken cancellationToken = default); 56 | 57 | /// 58 | /// Download a package's readme, or null if the package or readme does not exist. 59 | /// 60 | /// The package id. 61 | /// The package's version. 62 | /// A token to cancel the task. 63 | /// 64 | /// The package's readme stream, or null if the package or readme does not exist. The stream may not be seekable. 65 | /// 66 | Task GetPackageReadmeStreamOrNullAsync( 67 | string id, 68 | NuGetVersion version, 69 | CancellationToken cancellationToken = default); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/PackageContent/RawPackageContentClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using NugetProxy.Protocol.Models; 8 | using NuGet.Versioning; 9 | 10 | namespace NugetProxy.Protocol.Internal 11 | { 12 | /// 13 | /// The client to interact with an upstream source's Package Content resource. 14 | /// 15 | public class RawPackageContentClient : IPackageContentClient 16 | { 17 | private readonly HttpClient _httpClient; 18 | private readonly string _packageContentUrl; 19 | 20 | /// 21 | /// Create a new Package Content client. 22 | /// 23 | /// The HTTP client used to send requests. 24 | /// The NuGet Server's package content URL. 25 | public RawPackageContentClient(HttpClient httpClient, string packageContentUrl) 26 | { 27 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 28 | _packageContentUrl = packageContentUrl?.TrimEnd('/') 29 | ?? throw new ArgumentNullException(nameof(packageContentUrl)); 30 | } 31 | 32 | /// 33 | public async Task GetPackageVersionsOrNullAsync( 34 | string packageId, 35 | CancellationToken cancellationToken = default) 36 | { 37 | var id = packageId.ToLowerInvariant(); 38 | 39 | var url = $"{_packageContentUrl}/{id}/index.json"; 40 | var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); 41 | 42 | if (response.StatusCode == HttpStatusCode.NotFound) 43 | { 44 | return null; 45 | } 46 | 47 | return response.GetResultOrThrow(); 48 | } 49 | 50 | /// 51 | public async Task GetPackageContentStreamOrNullAsync( 52 | string packageId, 53 | NuGetVersion packageVersion, 54 | CancellationToken cancellationToken = default) 55 | { 56 | var id = packageId.ToLowerInvariant(); 57 | var version = packageVersion.ToNormalizedString().ToLowerInvariant(); 58 | 59 | // The response will be disposed when the returned content stream is disposed. 60 | var url = $"{_packageContentUrl}/{id}/{version}/{id}.{version}.nupkg"; 61 | var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); 62 | 63 | if (response.StatusCode == HttpStatusCode.NotFound) 64 | { 65 | return null; 66 | } 67 | 68 | return await response.Content.ReadAsStreamAsync(); 69 | } 70 | 71 | /// 72 | public async Task GetPackageManifestStreamOrNullAsync( 73 | string packageId, 74 | NuGetVersion packageVersion, 75 | CancellationToken cancellationToken = default) 76 | { 77 | var id = packageId.ToLowerInvariant(); 78 | var version = packageVersion.ToNormalizedString().ToLowerInvariant(); 79 | 80 | // The response will be disposed when the returned content stream is disposed. 81 | var url = $"{_packageContentUrl}/{id}/{version}/{id}.nuspec"; 82 | var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); 83 | 84 | if (response.StatusCode == HttpStatusCode.NotFound) 85 | { 86 | return null; 87 | } 88 | 89 | return await response.Content.ReadAsStreamAsync(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Search/RawSearchClient.cs: -------------------------------------------------------------------------------- 1 | using NugetProxy.Protocol.Models; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Text.Encodings.Web; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace NugetProxy.Protocol.Internal 12 | { 13 | /// 14 | /// The client used to search for packages. 15 | /// 16 | /// See https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource 17 | /// 18 | public class RawSearchClient : ISearchClient 19 | { 20 | private readonly HttpClient _httpClient; 21 | private readonly string _searchUrl; 22 | private readonly string _autocompleteUrl; 23 | 24 | /// 25 | /// Create a new Search client. 26 | /// 27 | /// The HTTP client used to send requests. 28 | /// The NuGet server's search URL. 29 | /// The NuGet server's autocomplete URL. 30 | public RawSearchClient(HttpClient httpClient, string searchUrl, string autocompleteUrl) 31 | { 32 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 33 | _searchUrl = searchUrl ?? throw new ArgumentNullException(nameof(searchUrl)); 34 | _autocompleteUrl = autocompleteUrl; //?? throw new ArgumentNullException(nameof(autocompleteUrl)); 35 | } 36 | 37 | public async Task SearchAsync( 38 | string query = null, 39 | int skip = 0, 40 | int take = 20, 41 | bool includePrerelease = true, 42 | bool includeSemVer2 = true, 43 | CancellationToken cancellationToken = default) 44 | { 45 | var url = AddSearchQueryString(_searchUrl, query, skip, take, includePrerelease, includeSemVer2, "q"); 46 | 47 | var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); 48 | 49 | return response.GetResultOrThrow(); 50 | } 51 | 52 | private string AddSearchQueryString( 53 | string uri, 54 | string query, 55 | int skip, 56 | int take, 57 | bool includePrerelease, 58 | bool includeSemVer2, 59 | string queryParamName) 60 | { 61 | var queryString = new Dictionary(); 62 | 63 | if (skip != 0) queryString["skip"] = skip.ToString(); 64 | if (take != 0) queryString["take"] = take.ToString(); 65 | if (includePrerelease) queryString["prerelease"] = true.ToString(); 66 | if (includeSemVer2) queryString["semVerLevel"] = "2.0.0"; 67 | 68 | if (!string.IsNullOrEmpty(query)) 69 | { 70 | queryString[queryParamName] = query; 71 | } 72 | 73 | return AddQueryString(uri, queryString); 74 | } 75 | 76 | // See: https://github.com/aspnet/AspNetCore/blob/8c02467b4a218df3b1b0a69bceb50f5b64f482b1/src/Http/WebUtilities/src/QueryHelpers.cs#L63 77 | private string AddQueryString(string uri, Dictionary queryString) 78 | { 79 | if (uri.IndexOf('#') != -1) throw new InvalidOperationException("URL anchors are not supported"); 80 | if (uri.IndexOf('?') != -1) throw new InvalidOperationException("Adding query strings to URL with query strings is not supported"); 81 | 82 | var builder = new StringBuilder(uri); 83 | var hasQuery = false; 84 | 85 | foreach (var parameter in queryString) 86 | { 87 | builder.Append(hasQuery ? '&' : '?'); 88 | builder.Append(UrlEncoder.Default.Encode(parameter.Key)); 89 | builder.Append('='); 90 | builder.Append(UrlEncoder.Default.Encode(parameter.Value)); 91 | hasQuery = true; 92 | } 93 | 94 | return builder.ToString(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Search/ISearchService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NugetProxy.Protocol.Models; 4 | 5 | namespace NugetProxy.Core 6 | { 7 | /// 8 | /// The service used to search for packages. 9 | /// 10 | /// See https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource 11 | /// 12 | public interface ISearchService 13 | { 14 | /// 15 | /// Add a package to the search index. 16 | /// 17 | /// The package to add. 18 | /// A token to cancel the task. 19 | /// A task that completes once the package has been added. 20 | Task IndexAsync(Package package, CancellationToken cancellationToken = default); 21 | 22 | /// 23 | /// Perform a search query. 24 | /// See: https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages 25 | /// 26 | /// The search query. 27 | /// How many results to skip. 28 | /// How many results to return. 29 | /// Whether pre-release packages should be returned. 30 | /// Whether packages that require SemVer 2.0.0 compatibility should be returned. 31 | /// The type of packages that should be returned. 32 | /// The Target Framework that results should be compatible. 33 | /// A token to cancel the task. 34 | /// The search response. 35 | Task SearchAsync( 36 | string query = null, 37 | int skip = 0, 38 | int take = 20, 39 | bool includePrerelease = true, 40 | bool includeSemVer2 = true, 41 | string packageType = null, 42 | string framework = null, 43 | CancellationToken cancellationToken = default); 44 | 45 | /// 46 | /// Perform an autocomplete query. 47 | /// See: https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource 48 | /// 49 | /// The autocomplete query. 50 | /// The autocomplete request type. 51 | /// How many results to skip. 52 | /// How many results to return. 53 | /// Whether pre-release packages should be returned. 54 | /// Whether packages that require SemVer 2.0.0 compatibility should be returned. 55 | /// A token to cancel the task. 56 | /// The autocomplete response. 57 | Task AutocompleteAsync( 58 | string query = null, 59 | AutocompleteType type = AutocompleteType.PackageIds, 60 | int skip = 0, 61 | int take = 20, 62 | bool includePrerelease = true, 63 | bool includeSemVer2 = true, 64 | CancellationToken cancellationToken = default); 65 | 66 | /// 67 | /// Find the packages that depend on a given package. 68 | /// 69 | /// The package whose dependents should be found. 70 | /// How many results to skip. 71 | /// How many results to return. 72 | /// A token to cancel the task. 73 | /// The dependents response. 74 | Task FindDependentsAsync( 75 | string packageId, 76 | int skip = 0, 77 | int take = 20, 78 | CancellationToken cancellationToken = default); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/IUrlGenerator.cs: -------------------------------------------------------------------------------- 1 | using NuGet.Versioning; 2 | 3 | namespace NugetProxy.Core 4 | { 5 | /// 6 | /// Used to create URLs to resources in the NuGet protocol. 7 | /// 8 | public interface IUrlGenerator 9 | { 10 | /// 11 | /// Get the URL for the root of the package content resource. 12 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource 13 | /// 14 | string GetPackageContentResourceUrl(); 15 | 16 | /// 17 | /// Get the URL for the root of the package metadata resource. 18 | /// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource 19 | /// 20 | string GetPackageMetadataResourceUrl(); 21 | 22 | /// 23 | /// Get the URL to publish packages. 24 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-publish-resource 25 | /// 26 | string GetPackagePublishResourceUrl(); 27 | 28 | /// 29 | /// Get the URL to publish symbol packages. 30 | /// See: https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource 31 | /// 32 | string GetSymbolPublishResourceUrl(); 33 | 34 | /// 35 | /// Get the URL to search for packages. 36 | /// See: https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource 37 | /// 38 | string GetSearchResourceUrl(); 39 | 40 | /// 41 | /// Get the URL to autocomplete package IDs. 42 | /// See: https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource 43 | /// 44 | string GetAutocompleteResourceUrl(); 45 | 46 | /// 47 | /// Get the URL for the entry point of a package's metadata. 48 | /// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index 49 | /// 50 | /// The package's ID. 51 | string GetRegistrationIndexUrl(string id); 52 | 53 | /// 54 | /// Get the URL for the metadata of several versions of a single package. 55 | /// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page 56 | /// 57 | /// The package's ID 58 | /// The lowest SemVer 2.0.0 version in the page (inclusive) 59 | /// The highest SemVer 2.0.0 version in the page (inclusive) 60 | string GetRegistrationPageUrl(string id, NuGetVersion lower, NuGetVersion upper); 61 | 62 | /// 63 | /// Get the URL for the metadata of a specific package ID and version. 64 | /// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf 65 | /// 66 | /// The package's ID 67 | /// The package's version 68 | string GetRegistrationLeafUrl(string id, NuGetVersion version); 69 | 70 | /// 71 | /// Get the URL that lists a package's versions. 72 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions 73 | /// 74 | /// The package's ID 75 | string GetPackageVersionsUrl(string id); 76 | 77 | /// 78 | /// Get the URL to download a package (.nupkg). 79 | /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg 80 | /// 81 | /// The package's ID 82 | /// The package's version 83 | string GetPackageDownloadUrl(string id, NuGetVersion version); 84 | 85 | /// 86 | /// Get the URL to download a package's manifest (.nuspec). 87 | /// 88 | /// The package's ID 89 | /// The package's version 90 | string GetPackageManifestDownloadUrl(string id, NuGetVersion version); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/PackageMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | /// 8 | /// A package's metadata. 9 | /// 10 | /// See https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry 11 | /// 12 | public class PackageMetadata 13 | { 14 | /// 15 | /// The URL to the document used to produce this object. 16 | /// 17 | [JsonProperty("@id")] 18 | public string CatalogLeafUrl { get; set; } 19 | 20 | /// 21 | /// The ID of the package. 22 | /// 23 | [JsonProperty("id")] 24 | public string PackageId { get; set; } 25 | 26 | /// 27 | /// The full NuGet version after normalization, including any SemVer 2.0.0 build metadata. 28 | /// 29 | [JsonProperty("version")] 30 | public string Version { get; set; } 31 | 32 | /// 33 | /// The package's authors. 34 | /// 35 | [JsonProperty("authors")] 36 | public string Authors { get; set; } 37 | 38 | /// 39 | /// The package's description. 40 | /// 41 | [JsonProperty("description")] 42 | public string Description { get; set; } 43 | 44 | /// 45 | /// The URL to the package's icon. 46 | /// 47 | [JsonProperty("iconUrl")] 48 | public string IconUrl { get; set; } 49 | 50 | /// 51 | /// The package's language. 52 | /// 53 | [JsonProperty("language")] 54 | public string Language { get; set; } 55 | 56 | /// 57 | /// The URL to the package's license. 58 | /// 59 | [JsonProperty("licenseUrl")] 60 | public string LicenseUrl { get; set; } 61 | 62 | /// 63 | /// Whether the package is listed in search results. 64 | /// If , the package should be considered as listed. 65 | /// 66 | [JsonProperty("listed")] 67 | public bool? Listed { get; set; } 68 | 69 | /// 70 | /// The minimum NuGet client version needed to use this package. 71 | /// 72 | [JsonProperty("minClientVersion")] 73 | public string MinClientVersion { get; set; } 74 | 75 | /// 76 | /// The URL to download the package's content. 77 | /// 78 | [JsonProperty("packageContent")] 79 | public string PackageContentUrl { get; set; } 80 | 81 | /// 82 | /// The URL for the package's home page. 83 | /// 84 | [JsonProperty("projectUrl")] 85 | public string ProjectUrl { get; set; } 86 | 87 | /// 88 | /// The package's publish date. 89 | /// 90 | [JsonProperty("published")] 91 | public DateTimeOffset Published { get; set; } 92 | 93 | /// 94 | /// If true, the package requires its license to be accepted. 95 | /// 96 | [JsonProperty("requireLicenseAcceptance")] 97 | public bool RequireLicenseAcceptance { get; set; } 98 | 99 | /// 100 | /// The package's summary. 101 | /// 102 | [JsonProperty("summary")] 103 | public string Summary { get; set; } 104 | 105 | /// 106 | /// The package's tags. 107 | /// 108 | [JsonProperty("tags")] 109 | public IReadOnlyList Tags { get; set; } 110 | 111 | /// 112 | /// The package's title. 113 | /// 114 | [JsonProperty("title")] 115 | public string Title { get; set; } 116 | 117 | /// 118 | /// The dependencies of the package, grouped by target framework. 119 | /// 120 | [JsonProperty("dependencyGroups")] 121 | public IReadOnlyList DependencyGroups { get; set; } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/Extensions/IRouteBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Routing; 3 | using Microsoft.AspNetCore.Routing.Constraints; 4 | 5 | namespace NugetProxy.Extensions 6 | { 7 | public static class IRouteBuilderExtensions 8 | { 9 | public static IRouteBuilder MapServiceIndexRoutes(this IRouteBuilder routes) 10 | { 11 | return routes.MapRoute( 12 | name: Routes.IndexRouteName, 13 | template: "v3/index.json", 14 | defaults: new { controller = "ServiceIndex", action = "GetAsync" }); 15 | } 16 | 17 | public static IRouteBuilder MapSymbolRoutes(this IRouteBuilder routes) 18 | { 19 | routes.MapRoute( 20 | name: Routes.UploadSymbolRouteName, 21 | template: "api/v2/symbol", 22 | defaults: new { controller = "Symbol", action = "Upload" }, 23 | constraints: new { httpMethod = new HttpMethodRouteConstraint("PUT") }); 24 | 25 | routes.MapRoute( 26 | name: Routes.SymbolDownloadRouteName, 27 | template: "api/download/symbols/{file}/{key}/{file2}", 28 | defaults: new { controller = "Symbol", action = "Get" }); 29 | 30 | routes.MapRoute( 31 | name: Routes.SymbolDownloadRouteName, 32 | template: "api/download/symbols/{prefix}/{file}/{key}/{file2}", 33 | defaults: new { controller = "Symbol", action = "Get" }); 34 | 35 | return routes; 36 | } 37 | 38 | public static IRouteBuilder MapSearchRoutes(this IRouteBuilder routes) 39 | { 40 | routes.MapRoute( 41 | name: Routes.SearchRouteName, 42 | template: "v3/search", 43 | defaults: new { controller = "Search", action = "SearchAsync" }); 44 | 45 | routes.MapRoute( 46 | name: Routes.AutocompleteRouteName, 47 | template: "v3/autocomplete", 48 | defaults: new { controller = "Search", action = "AutocompleteAsync" }); 49 | 50 | // This is an unofficial API to find packages that depend on a given package. 51 | routes.MapRoute( 52 | name: Routes.DependentsRouteName, 53 | template: "v3/dependents", 54 | defaults: new { controller = "Search", action = "DependentsAsync" }); 55 | 56 | return routes; 57 | } 58 | 59 | public static IRouteBuilder MapPackageMetadataRoutes(this IRouteBuilder routes) 60 | { 61 | routes.MapRoute( 62 | name: Routes.RegistrationIndexRouteName, 63 | template: "v3/registration/{id}/index.json", 64 | defaults: new { controller = "PackageMetadata", action = "RegistrationIndexAsync" }); 65 | 66 | routes.MapRoute( 67 | name: Routes.RegistrationLeafRouteName, 68 | template: "v3/registration/{id}/{version}.json", 69 | defaults: new { controller = "PackageMetadata", action = "RegistrationLeafAsync" }); 70 | 71 | return routes; 72 | } 73 | 74 | public static IRouteBuilder MapPackageContentRoutes(this IRouteBuilder routes) 75 | { 76 | routes.MapRoute( 77 | name: Routes.PackageVersionsRouteName, 78 | template: "v3/package/{id}/index.json", 79 | defaults: new { controller = "PackageContent", action = "GetPackageVersionsAsync" }); 80 | 81 | routes.MapRoute( 82 | name: Routes.PackageDownloadRouteName, 83 | template: "v3/package/{id}/{version}/{idVersion}.nupkg", 84 | defaults: new { controller = "PackageContent", action = "DownloadPackageAsync" }); 85 | 86 | routes.MapRoute( 87 | name: Routes.PackageDownloadManifestRouteName, 88 | template: "v3/package/{id}/{version}/{id2}.nuspec", 89 | defaults: new { controller = "PackageContent", action = "DownloadNuspecAsync" }); 90 | 91 | routes.MapRoute( 92 | name: Routes.PackageDownloadReadmeRouteName, 93 | template: "v3/package/{id}/{version}/readme", 94 | defaults: new { controller = "PackageContent", action = "DownloadReadmeAsync" }); 95 | 96 | return routes; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Mirror/PackageDownloadsJsonSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | using NuGet.Versioning; 11 | 12 | namespace NugetProxy.Core 13 | { 14 | // See https://github.com/NuGet/NuGet.Services.Metadata/blob/master/src/NuGet.Indexing/Downloads.cs 15 | public class PackageDownloadsJsonSource : IPackageDownloadsSource 16 | { 17 | public const string PackageDownloadsV1Url = "https://nugetprod0.blob.core.windows.net/ng-search-data/downloads.v1.json"; 18 | 19 | private readonly HttpClient _httpClient; 20 | private readonly ILogger _logger; 21 | 22 | public PackageDownloadsJsonSource(HttpClient httpClient, ILogger logger) 23 | { 24 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 25 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 26 | } 27 | 28 | public async Task>> GetPackageDownloadsAsync() 29 | { 30 | _logger.LogInformation("Fetching package downloads..."); 31 | 32 | var serializer = new JsonSerializer(); 33 | var results = new Dictionary>(); 34 | 35 | using (var downloadsStream = await GetDownloadsStreamAsync()) 36 | using (var downloadStreamReader = new StreamReader(downloadsStream)) 37 | using (var jsonReader = new JsonTextReader(downloadStreamReader)) 38 | { 39 | _logger.LogInformation("Parsing package downloads..."); 40 | 41 | jsonReader.Read(); 42 | 43 | while (jsonReader.Read()) 44 | { 45 | try 46 | { 47 | if (jsonReader.TokenType == JsonToken.StartArray) 48 | { 49 | // TODO: This line reads the entire document into memory... 50 | var record = JToken.ReadFrom(jsonReader); 51 | var id = string.Intern(record[0].ToString().ToLowerInvariant()); 52 | 53 | // The second entry in each record should be an array of versions, if not move on to next entry. 54 | // This is a check to safe guard against invalid entries. 55 | if (record.Count() == 2 && record[1].Type != JTokenType.Array) 56 | { 57 | continue; 58 | } 59 | 60 | if (!results.ContainsKey(id)) 61 | { 62 | results.Add(id, new Dictionary()); 63 | } 64 | 65 | foreach (var token in record) 66 | { 67 | if (token != null && token.Count() == 2) 68 | { 69 | var version = string.Intern(NuGetVersion.Parse(token[0].ToString()).ToNormalizedString().ToLowerInvariant()); 70 | var downloads = token[1].ToObject(); 71 | 72 | results[id][version] = downloads; 73 | } 74 | } 75 | } 76 | } 77 | catch (JsonReaderException e) 78 | { 79 | _logger.LogError(e, "Invalid entry in downloads.v1.json"); 80 | } 81 | } 82 | 83 | _logger.LogInformation("Parsed package downloads"); 84 | } 85 | 86 | return results; 87 | } 88 | 89 | private async Task GetDownloadsStreamAsync() 90 | { 91 | _logger.LogInformation("Downloading downloads.v1.json..."); 92 | 93 | var fileStream = File.Open(Path.GetTempFileName(), FileMode.Create); 94 | var response = await _httpClient.GetAsync(PackageDownloadsV1Url, HttpCompletionOption.ResponseHeadersRead); 95 | 96 | response.EnsureSuccessStatusCode(); 97 | 98 | using (var networkStream = await response.Content.ReadAsStreamAsync()) 99 | { 100 | await networkStream.CopyToAsync(fileStream); 101 | } 102 | 103 | fileStream.Seek(0, SeekOrigin.Begin); 104 | 105 | _logger.LogInformation("Downloaded downloads.v1.json"); 106 | 107 | return fileStream; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/NugetProxy.Core.Server/BaGetUrlGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NugetProxy.Core; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Routing; 5 | using NuGet.Versioning; 6 | 7 | namespace NugetProxy 8 | { 9 | // TODO: This should validate the "Host" header against known valid values 10 | public class NugetProxyUrlGenerator : IUrlGenerator 11 | { 12 | private readonly IHttpContextAccessor _httpContextAccessor; 13 | private readonly LinkGenerator _linkGenerator; 14 | 15 | public NugetProxyUrlGenerator(IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator) 16 | { 17 | _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); 18 | _linkGenerator = linkGenerator ?? throw new ArgumentNullException(nameof(linkGenerator)); 19 | } 20 | 21 | public string GetPackageContentResourceUrl() 22 | { 23 | return AbsoluteUrl("v3/package"); 24 | } 25 | 26 | public string GetPackageMetadataResourceUrl() 27 | { 28 | return AbsoluteUrl("v3/registration"); 29 | } 30 | 31 | public string GetPackagePublishResourceUrl() 32 | { 33 | return _linkGenerator.GetUriByRouteValues( 34 | _httpContextAccessor.HttpContext, 35 | Routes.UploadPackageRouteName, 36 | values: null); 37 | } 38 | 39 | public string GetSymbolPublishResourceUrl() 40 | { 41 | return _linkGenerator.GetUriByRouteValues( 42 | _httpContextAccessor.HttpContext, 43 | Routes.UploadSymbolRouteName, 44 | values: null); 45 | } 46 | 47 | public string GetSearchResourceUrl() 48 | { 49 | return _linkGenerator.GetUriByRouteValues( 50 | _httpContextAccessor.HttpContext, 51 | Routes.SearchRouteName, 52 | values: null); 53 | } 54 | 55 | public string GetAutocompleteResourceUrl() 56 | { 57 | return _linkGenerator.GetUriByRouteValues( 58 | _httpContextAccessor.HttpContext, 59 | Routes.AutocompleteRouteName, 60 | values: null); 61 | } 62 | 63 | public string GetRegistrationIndexUrl(string id) 64 | { 65 | return _linkGenerator.GetUriByRouteValues( 66 | _httpContextAccessor.HttpContext, 67 | Routes.RegistrationIndexRouteName, 68 | values: new { Id = id.ToLowerInvariant() }); 69 | } 70 | 71 | public string GetRegistrationPageUrl(string id, NuGetVersion lower, NuGetVersion upper) 72 | { 73 | // NugetProxy does not support paging the registration resource. 74 | throw new NotImplementedException(); 75 | } 76 | 77 | public string GetRegistrationLeafUrl(string id, NuGetVersion version) 78 | { 79 | return _linkGenerator.GetUriByRouteValues( 80 | _httpContextAccessor.HttpContext, 81 | Routes.RegistrationLeafRouteName, 82 | values: new 83 | { 84 | Id = id.ToLowerInvariant(), 85 | Version = version.ToNormalizedString().ToLowerInvariant(), 86 | }); 87 | } 88 | 89 | public string GetPackageVersionsUrl(string id) 90 | { 91 | return _linkGenerator.GetUriByRouteValues( 92 | _httpContextAccessor.HttpContext, 93 | Routes.PackageVersionsRouteName, 94 | values: new { Id = id.ToLowerInvariant() }); 95 | } 96 | 97 | public string GetPackageDownloadUrl(string id, NuGetVersion version) 98 | { 99 | id = id.ToLowerInvariant(); 100 | var versionString = version.ToNormalizedString().ToLowerInvariant(); 101 | 102 | return _linkGenerator.GetUriByRouteValues( 103 | _httpContextAccessor.HttpContext, 104 | Routes.PackageDownloadRouteName, 105 | values: new 106 | { 107 | Id = id, 108 | Version = versionString, 109 | IdVersion = $"{id}.{versionString}" 110 | }); 111 | } 112 | 113 | public string GetPackageManifestDownloadUrl(string id, NuGetVersion version) 114 | { 115 | id = id.ToLowerInvariant(); 116 | var versionString = version.ToNormalizedString().ToLowerInvariant(); 117 | 118 | return _linkGenerator.GetUriByRouteValues( 119 | _httpContextAccessor.HttpContext, 120 | Routes.PackageDownloadRouteName, 121 | values: new 122 | { 123 | Id = id, 124 | Version = versionString, 125 | Id2 = id, 126 | }); 127 | } 128 | 129 | private string AbsoluteUrl(string relativePath) 130 | { 131 | var request = _httpContextAccessor.HttpContext.Request; 132 | 133 | return string.Concat( 134 | request.Scheme, 135 | "://", 136 | request.Host.ToUriComponent(), 137 | request.PathBase.ToUriComponent(), 138 | "/", 139 | relativePath); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/NugetProxy.Core/Validation/RequiredIfAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Globalization; 4 | 5 | namespace NugetProxy.Core 6 | { 7 | /// 8 | /// Provides conditional validation based on related property value. 9 | /// 10 | /// Inspiration: https://stackoverflow.com/a/27666044 11 | /// 12 | [AttributeUsage(AttributeTargets.Property)] 13 | public sealed class RequiredIfAttribute : ValidationAttribute 14 | { 15 | #region Properties 16 | 17 | /// 18 | /// Gets or sets the other property name that will be used during validation. 19 | /// 20 | /// 21 | /// The other property name. 22 | /// 23 | public string OtherProperty { get; } 24 | 25 | /// 26 | /// Gets or sets the display name of the other property. 27 | /// 28 | /// 29 | /// The display name of the other property. 30 | /// 31 | public string OtherPropertyDisplayName { get; set; } 32 | 33 | /// 34 | /// Gets or sets the other property value that will be relevant for validation. 35 | /// 36 | /// 37 | /// The other property value. 38 | /// 39 | public object OtherPropertyValue { get; } 40 | 41 | /// 42 | /// Gets or sets a value indicating whether other property's value should match or differ from provided other property's value (default is false). 43 | /// 44 | /// 45 | /// true if other property's value validation should be inverted; otherwise, false. 46 | /// 47 | /// 48 | /// How this works 49 | /// - true: validated property is required when other property doesn't equal provided value 50 | /// - false: validated property is required when other property matches provided value 51 | /// 52 | public bool IsInverted { get; set; } 53 | 54 | /// 55 | /// Gets a value that indicates whether the attribute requires validation context. 56 | /// 57 | /// true if the attribute requires validation context; otherwise, false. 58 | public override bool RequiresValidationContext => true; 59 | 60 | #endregion 61 | 62 | #region Constructor 63 | 64 | /// 65 | /// Initializes a new instance of the class. 66 | /// 67 | /// The other property. 68 | /// The other property value. 69 | public RequiredIfAttribute(string otherProperty, object otherPropertyValue) 70 | : base("'{0}' is required because '{1}' has a value {3}'{2}'.") 71 | { 72 | OtherProperty = otherProperty; 73 | OtherPropertyValue = otherPropertyValue; 74 | IsInverted = false; 75 | } 76 | 77 | #endregion 78 | 79 | /// 80 | /// Applies formatting to an error message, based on the data field where the error occurred. 81 | /// 82 | /// The name to include in the formatted message. 83 | /// 84 | /// An instance of the formatted error message. 85 | /// 86 | public override string FormatErrorMessage(string name) 87 | { 88 | return string.Format( 89 | CultureInfo.CurrentCulture, 90 | ErrorMessageString, 91 | name, 92 | OtherPropertyDisplayName ?? OtherProperty, 93 | OtherPropertyValue, 94 | IsInverted ? "other than " : "of "); 95 | } 96 | 97 | /// 98 | /// Validates the specified value with respect to the current validation attribute. 99 | /// 100 | /// The value to validate. 101 | /// The context information about the validation operation. 102 | /// 103 | /// An instance of the class. 104 | /// 105 | protected override ValidationResult IsValid(object value, ValidationContext validationContext) 106 | { 107 | if (validationContext == null) 108 | throw new ArgumentNullException(nameof(validationContext)); 109 | 110 | var otherProperty = validationContext.ObjectType.GetProperty(OtherProperty); 111 | if (otherProperty == null) 112 | { 113 | return new ValidationResult( 114 | string.Format(CultureInfo.CurrentCulture, "Could not find a property named '{0}'.", OtherProperty)); 115 | } 116 | 117 | var otherValue = otherProperty.GetValue(validationContext.ObjectInstance); 118 | 119 | // Check if this value is actually required and validate it. 120 | if (!IsInverted && Equals(otherValue, OtherPropertyValue) || 121 | IsInverted && !Equals(otherValue, OtherPropertyValue)) 122 | { 123 | if (value == null) 124 | return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); 125 | 126 | // Additional check for strings so they're not empty 127 | if (value is string val && val.Trim().Length == 0) 128 | return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); 129 | } 130 | 131 | return ValidationResult.Success; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | .cache/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | project.fragment.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # Visual Studio code coverage results 115 | *.coverage 116 | *.coveragexml 117 | 118 | # NCrunch 119 | _NCrunch_* 120 | .*crunch*.local.xml 121 | nCrunchTemp_* 122 | 123 | # MightyMoose 124 | *.mm.* 125 | AutoTest.Net/ 126 | 127 | # Web workbench (sass) 128 | .sass-cache/ 129 | 130 | # Installshield output folder 131 | [Ee]xpress/ 132 | 133 | # DocProject is a documentation generator add-in 134 | DocProject/buildhelp/ 135 | DocProject/Help/*.HxT 136 | DocProject/Help/*.HxC 137 | DocProject/Help/*.hhc 138 | DocProject/Help/*.hhk 139 | DocProject/Help/*.hhp 140 | DocProject/Help/Html2 141 | DocProject/Help/html 142 | 143 | # Click-Once directory 144 | publish/ 145 | 146 | # Publish Web Output 147 | *.[Pp]ublish.xml 148 | *.azurePubxml 149 | # TODO: Comment the next line if you want to checkin your web deploy settings 150 | # but database connection strings (with potential passwords) will be unencrypted 151 | *.pubxml 152 | *.publishproj 153 | 154 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 155 | # checkin your Azure Web App publish settings, but sensitive information contained 156 | # in these scripts will be unencrypted 157 | PublishScripts/ 158 | 159 | # NuGet Packages 160 | *.nupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | # NuGet v3's project.json files produces more ignoreable files 168 | *.nuget.props 169 | *.nuget.targets 170 | 171 | # Microsoft Azure Build Output 172 | csx/ 173 | *.build.csdef 174 | 175 | # Microsoft Azure Emulator 176 | ecf/ 177 | rcf/ 178 | 179 | # Windows Store app package directories and files 180 | AppPackages/ 181 | BundleArtifacts/ 182 | Package.StoreAssociation.xml 183 | _pkginfo.txt 184 | 185 | # Visual Studio cache files 186 | # files ending in .cache can be ignored 187 | *.[Cc]ache 188 | # but keep track of directories ending in .cache 189 | !*.[Cc]ache/ 190 | 191 | # Others 192 | ClientBin/ 193 | ~$* 194 | *~ 195 | *.dbmdl 196 | *.dbproj.schemaview 197 | *.jfm 198 | *.pfx 199 | *.publishsettings 200 | node_modules/ 201 | orleans.codegen.cs 202 | 203 | # Since there are multiple workflows, uncomment next line to ignore bower_components 204 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 205 | #bower_components/ 206 | 207 | # RIA/Silverlight projects 208 | Generated_Code/ 209 | 210 | # Backup & report files from converting an old project file 211 | # to a newer Visual Studio version. Backup files are not needed, 212 | # because we have git ;-) 213 | _UpgradeReport_Files/ 214 | Backup*/ 215 | UpgradeLog*.XML 216 | UpgradeLog*.htm 217 | 218 | # SQL Server files 219 | *.mdf 220 | *.ldf 221 | 222 | # Business Intelligence projects 223 | *.rdl.data 224 | *.bim.layout 225 | *.bim_*.settings 226 | 227 | # Microsoft Fakes 228 | FakesAssemblies/ 229 | 230 | # GhostDoc plugin setting file 231 | *.GhostDoc.xml 232 | 233 | # Node.js Tools for Visual Studio 234 | .ntvs_analysis.dat 235 | 236 | # Visual Studio 6 build log 237 | *.plg 238 | 239 | # Visual Studio 6 workspace options file 240 | *.opt 241 | 242 | # Visual Studio LightSwitch build output 243 | **/*.HTMLClient/GeneratedArtifacts 244 | **/*.DesktopClient/GeneratedArtifacts 245 | **/*.DesktopClient/ModelManifest.xml 246 | **/*.Server/GeneratedArtifacts 247 | **/*.Server/ModelManifest.xml 248 | _Pvt_Extensions 249 | 250 | # Paket dependency manager 251 | .paket/paket.exe 252 | paket-files/ 253 | 254 | # FAKE - F# Make 255 | .fake/ 256 | 257 | # JetBrains Rider 258 | .idea/ 259 | *.sln.iml 260 | 261 | # CodeRush 262 | .cr/ 263 | 264 | # Python Tools for Visual Studio (PTVS) 265 | __pycache__/ 266 | *.pyc 267 | 268 | # Cake - Uncomment if you are using it 269 | # tools/ 270 | 271 | # Ignore client app cache 272 | **/NugetProxy.UI/.cache 273 | 274 | # Ignore client dist files 275 | **/NugetProxy.UI/dist/ 276 | 277 | # Ignore database file 278 | **/NugetProxy.db 279 | -------------------------------------------------------------------------------- /src/NugetProxy.Protocol/Models/PackageDetailsCatalogLeaf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace NugetProxy.Protocol.Models 6 | { 7 | /// This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/PackageDetailsCatalogLeaf.cs 8 | 9 | /// 10 | /// A "package details" catalog leaf. Represents a single package create or update event. 11 | /// s can be discovered from a . 12 | /// 13 | /// See: https://docs.microsoft.com/en-us/nuget/api/catalog-resource#catalog-leaf 14 | /// 15 | public class PackageDetailsCatalogLeaf : CatalogLeaf 16 | { 17 | /// 18 | /// The package's authors. 19 | /// 20 | [JsonProperty("authors")] 21 | public string Authors { get; set; } 22 | 23 | /// 24 | /// The package's copyright. 25 | /// 26 | [JsonProperty("copyright")] 27 | public string Copyright { get; set; } 28 | 29 | /// 30 | /// A timestamp of when the package was first created. Fallback property: . 31 | /// 32 | [JsonProperty("created")] 33 | public DateTimeOffset Created { get; set; } 34 | 35 | /// 36 | /// A timestamp of when the package was last edited. 37 | /// 38 | [JsonProperty("lastEdited")] 39 | public DateTimeOffset LastEdited { get; set; } 40 | 41 | /// 42 | /// The dependencies of the package, grouped by target framework. 43 | /// 44 | [JsonProperty("dependencyGroups")] 45 | public List DependencyGroups { get; set; } 46 | 47 | /// 48 | /// The package's description. 49 | /// 50 | [JsonProperty("description")] 51 | public string Description { get; set; } 52 | 53 | /// 54 | /// The URL to the package's icon. 55 | /// 56 | [JsonProperty("iconUrl")] 57 | public string IconUrl { get; set; } 58 | 59 | /// 60 | /// Whether or not the package is prerelease. Can be detected from . 61 | /// Note that the NuGet.org catalog had this wrong in some cases. 62 | /// Example: https://api.nuget.org/v3/catalog0/data/2016.03.11.21.02.55/mvid.fody.2.json 63 | /// 64 | [JsonProperty("isPrerelease")] 65 | public bool IsPrerelease { get; set; } 66 | 67 | /// 68 | /// The package's language. 69 | /// 70 | [JsonProperty("language")] 71 | public string Language { get; set; } 72 | 73 | /// 74 | /// THe URL to the package's license. 75 | /// 76 | [JsonProperty("licenseUrl")] 77 | public string LicenseUrl { get; set; } 78 | 79 | /// 80 | /// Whether the pacakge is listed. 81 | /// 82 | [JsonProperty("listed")] 83 | public bool? Listed { get; set; } 84 | 85 | /// 86 | /// The minimum NuGet client version needed to use this package. 87 | /// 88 | [JsonProperty("minClientVersion")] 89 | public string MinClientVersion { get; set; } 90 | 91 | /// 92 | /// The hash of the package encoded using Base64. 93 | /// Hash algorithm can be detected using . 94 | /// 95 | [JsonProperty("packageHash")] 96 | public string PackageHash { get; set; } 97 | 98 | /// 99 | /// The algorithm used to hash . 100 | /// 101 | [JsonProperty("packageHashAlgorithm")] 102 | public string PackageHashAlgorithm { get; set; } 103 | 104 | /// 105 | /// The size of the package .nupkg in bytes. 106 | /// 107 | [JsonProperty("packageSize")] 108 | public long PackageSize { get; set; } 109 | 110 | /// 111 | /// The URL for the package's home page. 112 | /// 113 | [JsonProperty("projectUrl")] 114 | public string ProjectUrl { get; set; } 115 | 116 | /// 117 | /// The package's release notes. 118 | /// 119 | [JsonProperty("releaseNotes")] 120 | public string ReleaseNotes { get; set; } 121 | 122 | /// 123 | /// If true, the package requires its license to be accepted. 124 | /// 125 | [JsonProperty("requireLicenseAgreement")] 126 | public bool? RequireLicenseAgreement { get; set; } 127 | 128 | /// 129 | /// The package's summary. 130 | /// 131 | [JsonProperty("summary")] 132 | public string Summary { get; set; } 133 | 134 | /// 135 | /// The package's tags. 136 | /// 137 | [JsonProperty("tags")] 138 | public List Tags { get; set; } 139 | 140 | /// 141 | /// The package's title. 142 | /// 143 | [JsonProperty("title")] 144 | public string Title { get; set; } 145 | 146 | /// 147 | /// The version string as it's originally found in the .nuspec. 148 | /// 149 | [JsonProperty("verbatimVersion")] 150 | public string VerbatimVersion { get; set; } 151 | 152 | /// 153 | /// The package's License Expression. 154 | /// 155 | [JsonProperty("licenseExpression")] 156 | public string LicenseExpression { get; set; } 157 | 158 | /// 159 | /// The package's license file. 160 | /// 161 | [JsonProperty("licenseFile")] 162 | public string LicenseFile { get; set; } 163 | 164 | /// 165 | /// The package's icon file. 166 | /// 167 | [JsonProperty("iconFile")] 168 | public string IconFile { get; set; } 169 | } 170 | } 171 | --------------------------------------------------------------------------------