Skip to content

Commit 2372955

Browse files
authored
Use partial classes
1 parent 51738d1 commit 2372955

4 files changed

+253
-232
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) David Pine. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace HaveIBeenPwned.Client;
5+
6+
internal sealed partial class DefaultPwnedClient
7+
{
8+
/// <inheritdoc cref="IPwnedBreachesClient.GetBreachAsync(string)" />
9+
async Task<BreachDetails?> IPwnedBreachesClient.GetBreachAsync(string breachName)
10+
{
11+
if (string.IsNullOrWhiteSpace(breachName))
12+
{
13+
throw new ArgumentException(
14+
"The breachName cannot be either null, empty or whitespace.", nameof(breachName));
15+
}
16+
17+
try
18+
{
19+
var client = _httpClientFactory.CreateClient(HibpClient);
20+
var breachDetails =
21+
await client.GetFromJsonAsync<BreachDetails>(
22+
$"breach/{breachName}");
23+
24+
return breachDetails;
25+
}
26+
catch (Exception ex)
27+
{
28+
_logger.LogError(ex, ex.Message);
29+
30+
return null!;
31+
}
32+
}
33+
34+
/// <inheritdoc cref="IPwnedBreachesClient.GetBreachesAsync(string?)" />
35+
async Task<BreachHeader[]> IPwnedBreachesClient.GetBreachesAsync(string? domain)
36+
{
37+
try
38+
{
39+
var client = _httpClientFactory.CreateClient(HibpClient);
40+
var queryString = string.IsNullOrWhiteSpace(domain)
41+
? ""
42+
: $"?domain={domain}";
43+
44+
var breachHeaders =
45+
await client.GetFromJsonAsync<BreachHeader[]>(
46+
$"breaches{queryString}");
47+
48+
return breachHeaders ?? Array.Empty<BreachHeader>();
49+
}
50+
catch (Exception ex)
51+
{
52+
_logger.LogError(ex, ex.Message);
53+
54+
return Array.Empty<BreachHeader>();
55+
}
56+
}
57+
58+
/// <inheritdoc cref="IPwnedBreachesClient.GetBreachesForAccountAsync(string)" />
59+
async Task<BreachDetails[]> IPwnedBreachesClient.GetBreachesForAccountAsync(string account)
60+
{
61+
if (string.IsNullOrWhiteSpace(account))
62+
{
63+
throw new ArgumentException(
64+
"The account cannot be either null, empty or whitespace.", nameof(account));
65+
}
66+
67+
try
68+
{
69+
var client = _httpClientFactory.CreateClient(HibpClient);
70+
var breachDetails =
71+
await client.GetFromJsonAsync<BreachDetails[]>(
72+
$"breachedaccount/{HttpUtility.UrlEncode(account)}?truncateResponse=false");
73+
74+
return breachDetails ?? Array.Empty<BreachDetails>();
75+
}
76+
catch (Exception ex)
77+
{
78+
_logger.LogError(ex, ex.Message);
79+
80+
return Array.Empty<BreachDetails>();
81+
}
82+
}
83+
84+
/// <inheritdoc cref="IPwnedBreachesClient.GetBreachHeadersForAccountAsync(string)" />
85+
async Task<BreachHeader[]> IPwnedBreachesClient.GetBreachHeadersForAccountAsync(string account)
86+
{
87+
if (string.IsNullOrWhiteSpace(account))
88+
{
89+
throw new ArgumentException(
90+
"The account cannot be either null, empty or whitespace.", nameof(account));
91+
}
92+
93+
try
94+
{
95+
var client = _httpClientFactory.CreateClient(HibpClient);
96+
var breachDetails =
97+
await client.GetFromJsonAsync<BreachDetails[]>(
98+
$"breachedaccount/{HttpUtility.UrlEncode(account)}?truncateResponse=true");
99+
100+
return breachDetails ?? Array.Empty<BreachDetails>();
101+
}
102+
catch (Exception ex)
103+
{
104+
_logger.LogError(ex, ex.Message);
105+
106+
return Array.Empty<BreachDetails>();
107+
}
108+
}
109+
110+
/// <inheritdoc cref="IPwnedBreachesClient.GetDataClassesAsync" />
111+
async Task<string[]> IPwnedBreachesClient.GetDataClassesAsync()
112+
{
113+
try
114+
{
115+
var client = _httpClientFactory.CreateClient(HibpClient);
116+
var dataClasses =
117+
await client.GetFromJsonAsync<string[]>("dataclasses");
118+
119+
return dataClasses ?? Array.Empty<string>();
120+
}
121+
catch (Exception ex)
122+
{
123+
_logger.LogError(ex, ex.Message);
124+
125+
return Array.Empty<string>();
126+
}
127+
}
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) David Pine. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace HaveIBeenPwned.Client;
5+
6+
internal sealed partial class DefaultPwnedClient : IPwnedClient
7+
{
8+
/// <inheritdoc cref="IPwnedPasswordsClient.GetPwnedPasswordAsync(string)" />
9+
async Task<PwnedPassword> IPwnedPasswordsClient.GetPwnedPasswordAsync(string plainTextPassword)
10+
{
11+
if (plainTextPassword is null or { Length: 0 })
12+
{
13+
throw new ArgumentException(
14+
"The plainTextPassword cannot be either null, or empty.", nameof(plainTextPassword));
15+
}
16+
17+
var pwnedPassword = new PwnedPassword(plainTextPassword);
18+
if (pwnedPassword.IsInvalid())
19+
{
20+
return pwnedPassword;
21+
}
22+
23+
try
24+
{
25+
var passwordHash = plainTextPassword.ToSha1Hash()!;
26+
var firstFiveChars = passwordHash[..5];
27+
var client = _httpClientFactory.CreateClient(PasswordsClient);
28+
var passwordHashesInRange =
29+
await client.GetStringAsync($"range/{firstFiveChars}");
30+
31+
pwnedPassword =
32+
ParsePasswordRangeResponseText(pwnedPassword, passwordHashesInRange, passwordHash);
33+
}
34+
catch (Exception ex)
35+
{
36+
_logger.LogError(ex, ex.Message);
37+
}
38+
39+
return pwnedPassword;
40+
}
41+
42+
internal static PwnedPassword ParsePasswordRangeResponseText(
43+
PwnedPassword pwnedPassword, string passwordRangeResponseText, string passwordHash)
44+
{
45+
pwnedPassword = pwnedPassword with
46+
{
47+
HashedPassword = passwordHash
48+
};
49+
50+
if (passwordRangeResponseText is not null)
51+
{
52+
// Example passwordRangeResponseText
53+
// The remaining hash characters, less the first five separated by a : with the corresponding count.
54+
// <Remaining Hash>:<Count>
55+
56+
// 0018A45C4D1DEF81644B54AB7F969B88D65:10
57+
// 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2
58+
// 011053FD0102E94D6AE2F8B83D76FAF94F6:701
59+
// 012A7CA357541F0AC487871FEEC1891C49C:2
60+
// 0136E006E24E7D152139815FB0FC6A50B15:2
61+
62+
var hashCountMap =
63+
passwordRangeResponseText
64+
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
65+
.Select(hashCountPair =>
66+
{
67+
var pair = hashCountPair
68+
.Replace('\r', '\0')
69+
.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
70+
71+
return pair?.Length != 2 || !long.TryParse(pair[1], out var count)
72+
? (Hash: "", Count: 0L, IsValid: false)
73+
: (Hash: pair[0], Count: count, IsValid: true);
74+
})
75+
.Where(t => t.IsValid)
76+
.ToDictionary(t => t.Hash, t => t.Count, StringComparer.OrdinalIgnoreCase);
77+
78+
var hashSuffix = passwordHash[5..];
79+
if (hashCountMap.TryGetValue(hashSuffix, out var count))
80+
{
81+
pwnedPassword = pwnedPassword with
82+
{
83+
PwnedCount = count,
84+
IsPwned = count > 0,
85+
};
86+
}
87+
}
88+
89+
return pwnedPassword;
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) David Pine. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace HaveIBeenPwned.Client;
5+
6+
internal sealed partial class DefaultPwnedClient : IPwnedClient
7+
{
8+
/// <inheritdoc cref="IPwnedPastesClient.GetPastesAsync(string)" />
9+
async Task<Pastes[]> IPwnedPastesClient.GetPastesAsync(string account)
10+
{
11+
if (string.IsNullOrWhiteSpace(account))
12+
{
13+
throw new ArgumentException(
14+
"The account cannot be either null, empty or whitespace.", nameof(account));
15+
}
16+
17+
try
18+
{
19+
var client = _httpClientFactory.CreateClient(HibpClient);
20+
var pastes =
21+
await client.GetFromJsonAsync<Pastes[]>(
22+
$"pasteaccount/{HttpUtility.UrlEncode(account)}");
23+
24+
return pastes ?? Array.Empty<Pastes>();
25+
}
26+
catch (Exception ex)
27+
{
28+
_logger.LogError(ex, ex.Message);
29+
30+
return Array.Empty<Pastes>();
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)