Skip to content

Commit

Permalink
Merge pull request #65 from stebet/net8
Browse files Browse the repository at this point in the history
Modernizing the project a bit
  • Loading branch information
stebet authored May 29, 2024
2 parents e3b2902 + acb1bee commit f795510
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
Expand All @@ -17,8 +17,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.47.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.5.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageReference Include="System.IO.Pipelines" Version="8.0.0" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion src/HaveIBeenPwned.PwnedPasswords.Downloader/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private static async Task CompleteWriter(Task previousTask, object? state)

internal static async Task CopyFrom<T>(this SafeFileHandle handle, T stream, int offset = 0) where T : Stream
{
var pipe = GetPipe();
Pipe pipe = GetPipe();
Task copyTask = stream.CopyToAsync(pipe.Writer).ContinueWith(CompleteWriter, pipe.Writer).Unwrap();

try
Expand Down
129 changes: 92 additions & 37 deletions src/HaveIBeenPwned.PwnedPasswords.Downloader/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,86 @@

using HaveIBeenPwned.PwnedPasswords;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Win32.SafeHandles;

using Polly;
using Polly.Extensions.Http;
using Polly.Retry;
using Polly.Timeout;

using Spectre.Console;
using Spectre.Console.Cli;

var app = new CommandApp<PwnedPasswordsDownloader>();
IHostBuilder host = CreateHostBuilder(args);

var registrar = new TypeRegistrar(host);

var app = new CommandApp<PwnedPasswordsDownloader>(registrar);

app.Configure(config => config.PropagateExceptions());

try
{
return app.Run(args);
return await app.RunAsync(args);
}
catch (Exception ex)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
return -99;
}

static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
IHttpClientBuilder clientBuilder = services.AddHttpClient("PwnedPasswords");
clientBuilder.AddResilienceHandler("retry", b =>
{
b.AddRetry(new RetryStrategyOptions<HttpResponseMessage> { MaxRetryAttempts = 10, OnRetry = OnRequestErrorAsync });
});
clientBuilder.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
if (handler.SupportsAutomaticDecompression)
{
handler.AutomaticDecompression = DecompressionMethods.All;
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls13 | System.Security.Authentication.SslProtocols.Tls12;
}
return handler;
})
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("https://api.pwnedpasswords.com/range/");
string? process = Environment.ProcessPath;
if (process != null)
{
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("hibp-downloader", FileVersionInfo.GetVersionInfo(process).ProductVersion));
}
#if NET7_0_OR_GREATER
client.DefaultRequestVersion = HttpVersion.Version30;
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
#endif
});
});

static ValueTask OnRequestErrorAsync(OnRetryArguments<HttpResponseMessage> args)
{
string requestUri = args.Outcome.Result?.RequestMessage?.RequestUri?.ToString() ?? "";
AnsiConsole.MarkupLine(args.Outcome.Exception != null
? $"[yellow]Failed request #{args.AttemptNumber} while fetching {requestUri}. Exception message: {args.Outcome.Exception.Message}.[/]"
: $"[yellow]Failed attempt #{args.AttemptNumber} while fetching {requestUri}. Response contained HTTP Status code {args.Outcome.Result?.StatusCode}.[/]");
if (args.Outcome.Exception != null)
{
AnsiConsole.WriteException(args.Outcome.Exception, ExceptionFormats.ShortenEverything);
}

return ValueTask.CompletedTask;
}

internal sealed class Statistics
{
public int HashesDownloaded;
Expand All @@ -45,19 +102,12 @@ internal sealed class Statistics
internal sealed class PwnedPasswordsDownloader : Command<PwnedPasswordsDownloader.Settings>
{
private readonly Statistics _statistics = new();
private readonly HttpClient _httpClient = InitializeHttpClient();
private readonly AsyncRetryPolicy<HttpResponseMessage> _policy = HttpPolicyExtensions.HandleTransientHttpError().Or<TaskCanceledException>().RetryAsync(10, OnRequestError);
private readonly HttpClient _httpClient;

private static void OnRequestError(DelegateResult<HttpResponseMessage> arg1, int arg2)
public PwnedPasswordsDownloader(Statistics statistics, IHttpClientFactory httpClientFactory)
{
string requestUri = arg1.Result?.RequestMessage?.RequestUri?.ToString() ?? "";
AnsiConsole.MarkupLine(arg1.Exception != null
? $"[yellow]Failed request #{arg2} while fetching {requestUri}. Exception message: {arg1.Exception.Message}.[/]"
: $"[yellow]Failed attempt #{arg2} while fetching {requestUri}. Response contained HTTP Status code {arg1.Result?.StatusCode}.[/]");
if(arg1.Exception != null)
{
AnsiConsole.WriteException(arg1.Exception, ExceptionFormats.ShortenEverything);
}
_statistics = statistics;
_httpClient = httpClientFactory.CreateClient("PwnedPasswords");
}

public sealed class Settings : CommandSettings
Expand Down Expand Up @@ -159,26 +209,6 @@ public override int Execute([NotNull]CommandContext context, [NotNull]Settings s
return 0;
}

private static HttpClient InitializeHttpClient()
{
var handler = new HttpClientHandler();

if (handler.SupportsAutomaticDecompression)
{
handler.AutomaticDecompression = DecompressionMethods.All;
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls13 | System.Security.Authentication.SslProtocols.Tls12;
}

HttpClient client = new(handler) { BaseAddress = new Uri("https://api.pwnedpasswords.com/range/") };
string? process = Environment.ProcessPath;
if (process != null)
{
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("hibp-downloader", FileVersionInfo.GetVersionInfo(process).ProductVersion));
}

return client;
}

private async Task<Stream> GetPwnedPasswordsRangeFromWeb(int i, bool fetchNtlm)
{
var cloudflareTimer = Stopwatch.StartNew();
Expand All @@ -188,7 +218,7 @@ private async Task<Stream> GetPwnedPasswordsRangeFromWeb(int i, bool fetchNtlm)
requestUri += "?mode=ntlm";
}

var response = await _policy.ExecuteAsync(async () => await _httpClient.GetAsync(requestUri));
HttpResponseMessage response = await _httpClient.GetAsync(requestUri);
Stream content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
Interlocked.Add(ref _statistics.CloudflareRequestTimeTotal, cloudflareTimer.ElapsedMilliseconds);
Interlocked.Increment(ref _statistics.CloudflareRequests);
Expand Down Expand Up @@ -245,7 +275,11 @@ private async Task ProcessRanges(Settings settings)
}
else
{
await Parallel.ForEachAsync(EnumerateRanges(), new ParallelOptions { MaxDegreeOfParallelism = settings.Parallelism }, async (i, _) =>
await Parallel.ForEachAsync(EnumerateRanges(), new ParallelOptions
{
MaxDegreeOfParallelism = settings.Parallelism,
TaskScheduler = TaskScheduler.Default
}, async (i, _) =>
{
await DownloadRangeToFile(i, settings.OutputFile, settings.FetchNtlm).ConfigureAwait(false);
});
Expand Down Expand Up @@ -286,3 +320,24 @@ private async Task DownloadRangeToFile(int currentHash, string outputDirectory,
Interlocked.Increment(ref _statistics.HashesDownloaded);
}
}

public sealed class TypeRegistrar : ITypeRegistrar
{
private readonly IHostBuilder _builder;
public TypeRegistrar(IHostBuilder builder) => _builder = builder;
public ITypeResolver Build() => new TypeResolver(_builder.Build());
public void Register(Type service, Type implementation) => _builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
public void RegisterInstance(Type service, object implementation) => _builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
public void RegisterLazy(Type service, Func<object> func)
{
ArgumentNullException.ThrowIfNull(func);
_builder.ConfigureServices((_, services) => services.AddSingleton(service, _ => func()));
}
}
public sealed class TypeResolver : ITypeResolver, IDisposable
{
private readonly IHost _host;
public TypeResolver(IHost provider) => _host = provider ?? throw new ArgumentNullException(nameof(provider));
public object? Resolve(Type? type) => type != null ? _host.Services.GetService(type) : null;
public void Dispose() => _host.Dispose();
}

0 comments on commit f795510

Please sign in to comment.