Skip to content

Commit 5cc6c85

Browse files
committed
Code refactoring and general library maintenance
1 parent e10a9c1 commit 5cc6c85

11 files changed

+146
-86
lines changed

.github/workflows/build-and-release.yml

Whitespace-only changes.

DnsOverHttps/Constants.cs

+19-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,29 @@ namespace DnsOverHttps
44
{
55
internal class Constants
66
{
7+
/// <summary>
8+
/// The hostname of the Dns-over-HTTPS resolver.
9+
/// </summary>
710
public const string Hostname = "1.1.1.1";
11+
/// <summary>
12+
/// The base URI to send requests to.
13+
/// </summary>
814
public static readonly Uri BaseUri = new($"https://{Hostname}/dns-query");
15+
/// <summary>
16+
/// The preferred HTTP request version to use.
17+
/// </summary>
918
public static readonly Version HttpVersion = new(2, 0);
10-
19+
/// <summary>
20+
/// The <c>User-Agent</c> header value to send along requests.
21+
/// </summary>
1122
public const string UserAgent = "C# DnsOverHttps Client - actually-akac/DnsOverHttps";
23+
/// <summary>
24+
/// The <c>Accept</c> header value to send along requests.
25+
/// </summary>
1226
public const string ContentType = "application/dns-json";
13-
public const int MaxPreviewLength = 500;
27+
/// <summary>
28+
/// The maximum string length when displaying a preview of a response body.
29+
/// </summary>
30+
public const int PreviewMaxLength = 500;
1431
}
1532
}

DnsOverHttps/DnsOverHttps.cs

+15-13
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,23 @@ public async Task<Response> Resolve(string name, ResourceRecordType type = Resou
4646
{
4747
if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name), "Name is null or empty.");
4848

49-
string url =
50-
$"?name={name.UrlEncode()}" +
51-
(type == ResourceRecordType.A ? "" : $"&type={type.ToString().UrlEncode()}") +
52-
(requestDnsSec == false ? "" : $"&do=1") +
53-
(validateDnsSec == false ? "" : $"&cd=1");
49+
string url = string.Concat(
50+
$"?name={name.UrlEncode()}",
51+
type == ResourceRecordType.A ? "" : $"&type={type.ToString().UrlEncode()}",
52+
requestDnsSec == false ? "" : $"&do=1",
53+
validateDnsSec == false ? "" : $"&cd=1");
5454

55-
HttpRequestMessage req = new(HttpMethod.Get, url);
56-
HttpResponseMessage res = await Client.SendAsync(req);
55+
using HttpRequestMessage req = new(HttpMethod.Get, url);
56+
using HttpResponseMessage res = await Client.SendAsync(req);
5757

5858
Response response = await res.Deseralize<Response>();
59-
59+
6060
if (res.StatusCode != HttpStatusCode.OK || !string.IsNullOrEmpty(response.Error) || response.Comments is not null)
6161
{
62-
string message =
63-
$"Failed to query type {type} of '{name}', received HTTP status code {res.StatusCode}." +
64-
(string.IsNullOrEmpty(response.Error) ? "" : $"\nError: {response.Error}") +
65-
(response.Comments is null ? "" : $"\nComments: {string.Join(", ", response.Comments)}");
62+
string message = string.Concat(
63+
$"Failed to query type {type} of \"{name}\", received HTTP status code {res.StatusCode}.",
64+
string.IsNullOrEmpty(response.Error) ? "" : $"\nError: {response.Error}",
65+
response.Comments is null ? "" : $"\nComments: {string.Join(", ", response.Comments)}");
6666

6767
throw new DnsOverHttpsException(message, response);
6868
}
@@ -112,9 +112,10 @@ public async Task<Response[]> Resolve(string name, ResourceRecordType[] types, b
112112
/// <returns></returns>
113113
/// <exception cref="DnsOverHttpsException"></exception>
114114
/// <exception cref="ArgumentNullException"></exception>
115-
public async Task<Answer> ResolveFirst(string name, ResourceRecordType type = ResourceRecordType.A, bool requestDnsSec = false, bool validateDnsSec = false)
115+
public async Task<Answer?> ResolveFirst(string name, ResourceRecordType type = ResourceRecordType.A, bool requestDnsSec = false, bool validateDnsSec = false)
116116
{
117117
Response res = await Resolve(name, type, requestDnsSec, validateDnsSec);
118+
118119
return res.Answers?.FirstOrDefault(x => x.Type == type);
119120
}
120121

@@ -134,6 +135,7 @@ public async Task<Answer> ResolveFirst(string name, ResourceRecordType type = Re
134135
public async Task<Answer[]> ResolveAll(string name, ResourceRecordType type = ResourceRecordType.A, bool requestDnsSec = false, bool validateDnsSec = false)
135136
{
136137
Response res = await Resolve(name, type, requestDnsSec, validateDnsSec);
138+
137139
return res.Answers.Where(x => x.Type == ResourceRecordType.A).ToArray();
138140
}
139141
}

DnsOverHttps/DnsOverHttps.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<!--Basic Information-->
5-
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
5+
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
66
<PackageId>DnsOverHttps</PackageId>
77
<Product>DnsOverHttps</Product>
88
<Authors>akac</Authors>
@@ -53,4 +53,4 @@
5353
</None>
5454
</ItemGroup>
5555

56-
</Project>
56+
</Project>

DnsOverHttps/Entities.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public enum ResourceRecordType : byte
206206
/// <summary>
207207
/// The result of a DNS over HTTPS query.
208208
/// </summary>
209-
public class Response
209+
public struct Response
210210
{
211211
/// <summary>
212212
/// A DNS response code.
@@ -289,7 +289,7 @@ public class Response
289289
/// <summary>
290290
/// A DNS question sent by the client.
291291
/// </summary>
292-
public class Question
292+
public struct Question
293293
{
294294
/// <summary>
295295
/// The FQDN record name requested.
@@ -307,7 +307,7 @@ public class Question
307307
/// <summary>
308308
/// A DNS answer sent by the server.
309309
/// </summary>
310-
public class Answer
310+
public struct Answer
311311
{
312312
/// <summary>
313313
/// The record owner.

DnsOverHttps/Exceptions.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ namespace DnsOverHttps
77
/// </summary>
88
public class DnsOverHttpsException : Exception
99
{
10+
/// <summary>
11+
/// The DNS response that caused this exception.
12+
/// </summary>
1013
public Response Response { get; set; }
1114

1215
public DnsOverHttpsException(string message) : base(message) { }
13-
public DnsOverHttpsException(string message, Response response) : base(message) { Response = response; }
16+
public DnsOverHttpsException(string message, Response res) : base(message) { Response = res; }
1417
}
15-
}
18+
}

DnsOverHttps/Extensions.cs

+53-12
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,68 @@ public static class Extensions
1111
{
1212
public static string UrlEncode(this string value) => WebUtility.UrlEncode(value);
1313

14-
public static async Task<string> GetPreview(this HttpResponseMessage res)
14+
/// <summary>
15+
/// Deserialize a JSON HTTP response into a given type.
16+
/// </summary>
17+
/// <typeparam name="T">The type to deserialize into.</typeparam>
18+
/// <param name="res">The HTTP response message with JSON as a body.</param>
19+
public static async Task<T> Deseralize<T>(this HttpResponseMessage res)
1520
{
16-
string text = await res.Content.ReadAsStringAsync();
17-
return text[..Math.Min(text.Length, Constants.MaxPreviewLength)];
18-
}
19-
20-
public static async Task<T> Deseralize<T>(this HttpResponseMessage res, JsonSerializerOptions options = null)
21-
{
22-
Stream stream = await res.Content.ReadAsStreamAsync();
21+
using Stream stream = await res.Content.ReadAsStreamAsync();
2322
if (stream.Length == 0) throw new DnsOverHttpsException("Response content is empty, can't parse as JSON.");
2423

2524
try
2625
{
27-
return await JsonSerializer.DeserializeAsync<T>(stream, options);
26+
return await JsonSerializer.DeserializeAsync<T>(stream);
2827
}
2928
catch (Exception ex)
3029
{
31-
throw new DnsOverHttpsException(
32-
$"Exception while parsing JSON: {ex.GetType().Name} => {ex.Message}\n" +
33-
$"Preview: {await res.GetPreview()}");
30+
throw new DnsOverHttpsException($"Exception while parsing JSON: {ex.GetType().Name} => {ex.Message}\nPreview: {await stream.GetPreview()}");
3431
}
3532
}
33+
34+
/// <summary>
35+
/// Serialize an object into a JSON HTTP Stream Content.
36+
/// </summary>
37+
/// <param name="obj">The object to serialize as JSON.</param>
38+
public static async Task<StreamContent> Serialize(this object obj)
39+
{
40+
MemoryStream ms = new();
41+
await JsonSerializer.SerializeAsync(ms, obj);
42+
ms.Position = 0;
43+
44+
StreamContent sc = new(ms);
45+
sc.Headers.ContentType = new("application/json");
46+
47+
return sc;
48+
}
49+
50+
/// <summary>
51+
/// Extract a short preview string from a HTTP response body.
52+
/// </summary>
53+
/// <param name="res">The HTTP response message with a body.</param>
54+
public static async Task<string> GetPreview(this HttpResponseMessage res)
55+
{
56+
using Stream stream = await res.Content.ReadAsStreamAsync();
57+
if (stream.Length == 0) throw new DnsOverHttpsException("Response content is empty, can't extract body.");
58+
59+
return await GetPreview(stream);
60+
}
61+
62+
/// <summary>
63+
/// Extract a short preview string from a HTTP response body.
64+
/// </summary>
65+
/// <param name="stream">The HTTP response stream.</param>
66+
public static async Task<string> GetPreview(this Stream stream)
67+
{
68+
stream.Position = 0;
69+
using StreamReader sr = new(stream);
70+
71+
char[] buffer = new char[Math.Min(stream.Length, Constants.PreviewMaxLength)];
72+
int bytesRead = await sr.ReadAsync(buffer, 0, buffer.Length);
73+
string text = new(buffer, 0, bytesRead);
74+
75+
return text;
76+
}
3677
}
3778
}

DnsOverHttps/NuGet.md

+18-18
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
### An async and lightweight C# library for Cloudflare's DNS over HTTPS.
66

77
## Usage
8-
Provides an easy interface for interacting with Cloudflare's DNS over HTTPS endpoints. Learn more about it [here](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/).
8+
This library provides an easy interface for interacting with Cloudflare's DNS over HTTPS endpoints.
99

10-
To get started, add the library into your solution with either the `NuGet Package Manager` or the `dotnet` CLI.
10+
DoH is a protocol that enhances the privacy and security of DNS queries by encrypting them using HTTPS. This helps prevent unauthorized access or tampering of DNS data during transmission. Learn more about it [here](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/).
11+
12+
To get started, import the library into your solution with either the `NuGet Package Manager` or the `dotnet` CLI.
1113
```rust
1214
dotnet add package DnsOverHttps
1315
```
@@ -19,15 +21,19 @@ using DnsOverHttps;
1921

2022
Need more examples? Under the `Example` directory you can find a working demo project that implements this library.
2123

22-
## Features
23-
- Built for **.NET 6** and **.NET 7**
24+
## Properties
25+
- Built for **.NET 8**, **.NET 7** and **.NET 6**
2426
- Fully **async**
25-
- Deep coverage of the API
2627
- Extensive **XML documentation**
27-
- **No external dependencies** (uses integrated HTTP and JSON)
28-
- **Custom exceptions** (`DnsOverHttpsException`) for advanced catching
28+
- **No external dependencies** (makes use of built-in `HttpClient` and `JsonSerializer`)
29+
- **Custom exceptions** (`DnsOverHttpsException`) for easy debugging
2930
- Example project to demonstrate all capabilities of the library
30-
- Execute DNS queries over HTTPS of any type
31+
32+
## Features
33+
- Resolve one or all DNS records under a hostname
34+
- Ask for DNSSEC validation
35+
- Query in parallel
36+
- Specify advanced parameters
3137

3238
## Code Samples
3339

@@ -38,26 +44,20 @@ DnsOverHttpsClient dns = new();
3844

3945
### Resolving A DNS records including DNSSEC
4046
```csharp
41-
Response response = await dns.Resolve("discord.com", "A", true, true);
47+
Response response = await Client.Resolve("discord.com", ResourceRecordType.A, true, true);
4248
```
4349

4450
### Using helper methods to return the first or all answers
4551
```csharp
46-
Answer nsAnswer = await dns.GetFirst("example.com", "NS");
47-
Answer[] aAnswers = await dns.GetAll("reddit.com", "A");
52+
Answer? nsAnswer = await Client.ResolveFirst("example.com", ResourceRecordType.NS);
53+
Answer[] aAnswers = await Client.ResolveAll("reddit.com", ResourceRecordType.A);
4854
```
4955

50-
## Available Methods
51-
- Task\<Response> **Resolve**(string name, ResourceRecordType type = ResourceRecordType.A, bool requestDnsSec = false, bool validateDnsSec = false)
52-
- Task\<Response[]> **Resolve**(string name, ResourceRecordType[] types, bool requestDnsSec = false, bool validateDnsSec = false)
53-
- Task\<Answer[]> **ResolveAll**(string name, ResourceRecordType type = ResourceRecordType.A, bool requestDnsSec = false, bool validateDnsSec = false)
54-
- Task\<Answer> **ResolveFirst**(string name, ResourceRecordType type = ResourceRecordType.A, bool requestDnsSec = false, bool validateDnsSec = false)
55-
5656
## Resources
5757
- Cloudflare: https://cloudflare.com
5858
- 1.1.1.1: https://1.1.1.1
5959
- Introduction: https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https
6060

61-
*This is a community-ran library. Not affiliated with Cloudflare.*
61+
*This is a community-ran library. Not affiliated with Cloudflare, Inc.*
6262

6363
*Icon made by **Freepik** at [Flaticon](https://www.flaticon.com).*

Example/Example.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
5+
<TargetFrameworks>net8.0</TargetFrameworks>
66
</PropertyGroup>
77

88
<ItemGroup>
99
<ProjectReference Include="..\DnsOverHttps\DnsOverHttps.csproj" />
1010
</ItemGroup>
1111

12-
</Project>
12+
</Project>

Example/Program.cs

+10-13
Original file line numberDiff line numberDiff line change
@@ -6,55 +6,52 @@ namespace Example
66
{
77
public static class Program
88
{
9+
private static readonly DnsOverHttpsClient Client = new();
10+
911
public static async Task Main()
1012
{
11-
DnsOverHttpsClient client = new();
12-
13-
1413
Console.WriteLine($"> Resolving the first NS record on example.com");
15-
Answer nsAnswer = await client.ResolveFirst("example.com", ResourceRecordType.A);
14+
Answer? nsAnswer = await Client.ResolveFirst("example.com", ResourceRecordType.NS);
1615

1716
Console.WriteLine($"Result:");
1817
PrintAnswer(nsAnswer);
1918

2019

2120
Console.WriteLine($"\n> Resolving all A records on reddit.com");
22-
Answer[] aAnswers = await client.ResolveAll("reddit.com", ResourceRecordType.A);
21+
Answer[] aAnswers = await Client.ResolveAll("reddit.com", ResourceRecordType.A);
2322

2423
Console.WriteLine($"Result:");
2524
foreach (Answer answer in aAnswers) PrintAnswer(answer);
2625
Console.WriteLine();
2726

2827

2928
Console.WriteLine($"\n> Resolving an invalid domain");
30-
Answer nxDomain = await client.ResolveFirst("5525fe855b7366f93447cd039ab885.com", ResourceRecordType.A);
31-
Console.WriteLine($"Result is {(nxDomain is null ? "null" : $"not null: {nxDomain.Data}")}");
29+
Answer? nxDomain = await Client.ResolveFirst("5525fe855b7366f93447cd039ab885.com", ResourceRecordType.A);
30+
Console.WriteLine($"Result is {(nxDomain is null ? "null" : $"not null: {nxDomain.Value.Data}")}");
3231
Console.WriteLine();
3332

3433

3534
Console.WriteLine($"\n> Resolving A records on discord.com with DNSSEC");
36-
Response response = await client.Resolve("discord.com", ResourceRecordType.A, true, true);
35+
Response response = await Client.Resolve("discord.com", ResourceRecordType.A, true, true);
3736

3837
Console.WriteLine($"Result:");
3938
foreach (Answer answer in response.Answers) PrintAnswer(answer);
4039

4140

4241
Console.WriteLine($"\n> Resolving multiple records in parallel on github.com");
43-
Response[] responses = await client.Resolve("discord.com", new ResourceRecordType[] { ResourceRecordType.A, ResourceRecordType.MX, ResourceRecordType.NS });
42+
Response[] responses = await Client.Resolve("github.com", [ResourceRecordType.A, ResourceRecordType.MX, ResourceRecordType.NS]);
4443

4544
foreach (Response resp in responses)
46-
{
4745
foreach (Answer answer in resp.Answers) PrintAnswer(answer);
48-
}
4946

5047

5148
Console.WriteLine("\nDemo finished");
5249
Console.ReadKey();
5350
}
5451

55-
public static void PrintAnswer(Answer answer)
52+
public static void PrintAnswer(Answer? answer)
5653
{
57-
Console.WriteLine($"\tType: {answer.Type}; TTL: {answer.TTL}; Translation: {answer.Name} => {answer.Data}");
54+
Console.WriteLine($"\tType: {answer.Value.Type}; TTL: {answer.Value.TTL}; Translation: {answer.Value.Name} => {answer.Value.Data}");
5855
}
5956
}
6057
}

0 commit comments

Comments
 (0)