Skip to content

Commit ff9cf57

Browse files
authored
Merge pull request #51 from ladeak/test
User friendly error message for SSL errors
2 parents 1bbcb8d + 590a768 commit ff9cf57

15 files changed

Lines changed: 165 additions & 61 deletions

File tree

.github/workflows/CI.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ jobs:
3333
run: dotnet build CHttpTools.slnx -c ${{ env.CONFIGURATION }}
3434
- name: Test
3535
run: |
36-
dotnet test --no-build -c ${{ env.CONFIGURATION }}
36+
dotnet test ./tests/CHttp.Tests --no-build -c ${{ env.CONFIGURATION }}
37+
dotnet test ./tests/CHttpExecutor.Tests --no-build -c ${{ env.CONFIGURATION }}
38+
dotnet test ./tests/TestWebApplication.Tests --no-build -c ${{ env.CONFIGURATION }}
39+
dotnet test ./tests/CHttp.Api.Tests --no-build -c ${{ env.CONFIGURATION }}
40+
dotnet test ./tests/CHttpServer.Tests --no-build -c ${{ env.CONFIGURATION }}
3741
- name: Publish VSCE
3842
run: |
3943
pushd ./src/VSCodeExt/

src/CHttp/Binders/HttpBehaviorBinder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ namespace CHttp.Binders;
55

66
internal sealed class HttpBehaviorBinder(
77
Option<bool> redirectBinder,
8-
Option<bool> enableCertificateValidationBinder,
8+
Option<bool> validateCertificateValidationBinder,
99
Option<double> timeout,
1010
Option<FileInfo?> cookieContainerOption,
1111
Option<bool> kerberosAuthOption,
1212
Option<bool> decompressResponse)
1313
{
1414
private readonly Option<bool> _redirectBinder = redirectBinder;
15-
private readonly Option<bool> _enableCertificateValidationBinder = enableCertificateValidationBinder;
15+
private readonly Option<bool> _validateCertificateValidationBinder = validateCertificateValidationBinder;
1616
private readonly Option<double> _timeoutOption = timeout;
1717
private readonly Option<FileInfo?> _cookieContainerOption = cookieContainerOption;
1818
private readonly Option<bool> _kerberosAuthOption = kerberosAuthOption;
@@ -21,7 +21,7 @@ internal sealed class HttpBehaviorBinder(
2121
internal HttpBehavior Bind(ParseResult parseResult)
2222
{
2323
var redirects = parseResult.GetValue(_redirectBinder);
24-
var enableCertificateValidation = parseResult.GetValue(_enableCertificateValidationBinder);
24+
var enableCertificateValidation = !parseResult.GetValue(_validateCertificateValidationBinder);
2525
var timeout = parseResult.GetValue(_timeoutOption);
2626
var cookieContainer = parseResult.GetValue(_cookieContainerOption)?.FullName ?? string.Empty;
2727
var kerberosAuth = parseResult.GetValue(_kerberosAuthOption);

src/CHttp/CHttp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFrameworks>net9.0;net11.0</TargetFrameworks>
5+
<TargetFrameworks>net9.0;net10.0;net11.0</TargetFrameworks>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
<PackAsTool>true</PackAsTool>

src/CHttp/CommandFactory.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ public static Command CreateRootCommand(
2020
IFileSystem? fileSystem = null)
2121
{
2222
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
23-
// kerberos auth
24-
// certificates
2523
// TLS
2624
// proxy
2725

@@ -122,9 +120,9 @@ public static Command CreateRootCommand(
122120
{
123121
var value = parseResult.Tokens.FirstOrDefault()?.Value;
124122
if (value == null)
125-
return false;
123+
return true;
126124
if (bool.TryParse(value, out var result))
127-
return !result; // Invert the value to match the option name
125+
return result; // Invert the value to match the option name
128126
parseResult.AddError("Invalid value for --no-certificate-validation. Expected 'true' or 'false' or no value.");
129127
return false;
130128
},

src/CHttp/Data/Summary.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ internal Summary(string url, DateTime startTime, TimeSpan duration)
3737
private long _endTime;
3838
public long EndTime
3939
{
40-
get => _endTime;
40+
readonly get => _endTime;
4141
set
4242
{
4343
if (_endTime != default || StartTime == default)
@@ -59,7 +59,7 @@ public void RequestCompleted(HttpStatusCode statusCode)
5959
HttpStatusCode = (int)statusCode;
6060
}
6161

62-
public override string ToString()
62+
public readonly override string ToString()
6363
{
6464
if (!string.IsNullOrEmpty(Error))
6565
return Error;
@@ -69,19 +69,19 @@ public override string ToString()
6969
inputs.Url.CopyTo(buffer);
7070
buffer = buffer.Slice(inputs.Url.Length);
7171
buffer[0] = ' ';
72-
buffer = buffer.Slice(1);
72+
buffer = buffer[1..];
7373
var responseSize = inputs.Length;
7474
if (SizeFormatter<double>.TryFormatSize(responseSize, buffer, out var count))
7575
{
76-
buffer = buffer.Slice(count);
76+
buffer = buffer[count..];
7777
buffer[0] = ' ';
78-
buffer = buffer.Slice(1);
78+
buffer = buffer[1..];
7979
}
8080
if (!inputs.Duration.TryFormat(buffer, out count, "c"))
8181
ThrowInvalidOperationException();
82-
buffer = buffer.Slice(count);
82+
buffer = buffer[count..];
8383
buffer[0] = 's';
84-
buffer = buffer.Slice(1);
84+
buffer = buffer[1..];
8585
buffer.Fill(' ');
8686
});
8787
}

src/CHttp/Http/HttpMessageSender.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.IO.Pipelines;
22
using System.Net.Http.Headers;
3+
using System.Security.Authentication;
34
using System.Text;
45
using CHttp.Data;
56
using CHttp.Writers;
@@ -46,7 +47,7 @@ public async Task SendRequestAsync(HttpRequestDetails requestData, CancellationT
4647

4748
private async Task SendRequestAsync(HttpClient client, HttpRequestMessage request, CancellationToken token = default)
4849
{
49-
Summary summary = new Summary(request.RequestUri?.ToString() ?? string.Empty);
50+
Summary summary = new(request.RequestUri?.ToString() ?? string.Empty);
5051
HttpResponseHeaders? trailers = null;
5152
{
5253
try
@@ -61,7 +62,7 @@ private async Task SendRequestAsync(HttpClient client, HttpRequestMessage reques
6162
}
6263
catch (HttpRequestException requestException)
6364
{
64-
summary = summary with { Error = $"Request Error {requestException}", ErrorCode = ErrorType.HttpRequestException };
65+
summary = summary with { Error = HandleRequestException(requestException), ErrorCode = ErrorType.HttpRequestException };
6566
}
6667
catch (HttpProtocolException protocolException)
6768
{
@@ -112,4 +113,21 @@ private void SetHeaders(HttpRequestDetails requestData, HttpRequestMessage reque
112113
}
113114
}
114115
}
116+
117+
private string HandleRequestException(HttpRequestException requestException)
118+
{
119+
if (requestException.InnerException is AuthenticationException authException)
120+
{
121+
if (authException.Message.StartsWith("The remote certificate is invalid"))
122+
return $"Enable flag --no-certificate-validation true'. SSL error: {authException.Message}";
123+
if (authException.Message.StartsWith("Cannot determine the frame size or a corrupted frame was received"))
124+
return $"Invalid http(s) schema: {authException.Message}";
125+
}
126+
127+
if (requestException.InnerException is HttpIOException ioException)
128+
{
129+
return $"Invalid http(s) schema: {ioException.Message}";
130+
}
131+
return $"Request Error {requestException}";
132+
}
115133
}

src/CHttpServer/CHttpServer/Http3/Http3Connection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ private async Task TryCloseConnection()
143143
/// <summary>Graceful shutdown (until token gets cancelled).</summary>
144144
internal async Task StopAsync(CancellationToken token)
145145
{
146-
using var _ = token.Register(() => _abruptCts.Cancel()); // Registers for immediate abort.
146+
using var _ = token.Register(() => { try { _abruptCts.Cancel(); } catch (ObjectDisposedException) { } }); // Registers for immediate abort.
147147
_context.ConnectionCancellation.Cancel(); // Graceful shutdown
148148
await _processingCompleted.Task;
149149
}

src/CHttpServer/CHttpServer/ManualResetValueTaskSource.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ internal class ManualResetValueTaskSource<T> : IValueTaskSource<T>, IValueTaskSo
88
public bool RunContinuationsAsynchronously { get => _core.RunContinuationsAsynchronously; set => _core.RunContinuationsAsynchronously = value; }
99
public short Version => _core.Version;
1010
public void Reset() => _core.Reset();
11-
public void SetException(Exception error) => _core.SetException(error);
11+
public void SetException(Exception error)
12+
{
13+
if (_core.GetStatus(_core.Version) == ValueTaskSourceStatus.Pending)
14+
_core.SetException(error);
15+
}
1216
void IValueTaskSource.GetResult(short token) => _core.GetResult(token);
1317
public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token);
1418
public void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);

tests/CHttp.Tests/AwaiterTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ public async Task WaitAsync_Waits50Ms()
1414
timeProvider.Advance(TimeSpan.FromMilliseconds(49));
1515
Assert.False(waiting.IsCompleted);
1616
timeProvider.Advance(TimeSpan.FromMilliseconds(1));
17-
await waiting.WaitAsync(TimeSpan.FromSeconds(1));
17+
await waiting.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
1818
}
1919
}

tests/CHttp.Tests/Http/CHttpFunctionalTests.cs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Diagnostics;
21
using System.IO.Compression;
32
using System.Text;
43
using CHttp.Abstractions;
@@ -350,6 +349,86 @@ public async Task Diff_FunctionalTest()
350349
Assert.Contains("probability, the base session's true mean latency is", console.Text);
351350
}
352351

352+
[Fact]
353+
public async Task CertificateValidationError_WithProposedSuggestion()
354+
{
355+
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http2);
356+
await host.StartAsync(TestContext.Current.CancellationToken);
357+
var console = new TestConsolePerWrite();
358+
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);
359+
360+
var client = await CommandFactory.CreateRootCommand(writer)
361+
.Parse("--method GET --uri https://localhost:5011 -v 2")
362+
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
363+
364+
await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
365+
Assert.Contains("Enable flag --no-certificate-validation true'. SSL error: The remote certificate is invalid because of errors in the certificate chain", console.Text);
366+
}
367+
368+
[Fact]
369+
public async Task CertificateValidationError_HttpsServer_HttpRequest_H2()
370+
{
371+
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http2);
372+
await host.StartAsync(TestContext.Current.CancellationToken);
373+
var console = new TestConsolePerWrite();
374+
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);
375+
376+
var client = await CommandFactory.CreateRootCommand(writer)
377+
.Parse("--method GET --uri http://localhost:5011 -v 2")
378+
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
379+
380+
await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
381+
Assert.Contains("Invalid http(s) schema: An HTTP/2 connection could not be established", console.Text);
382+
}
383+
384+
[Fact]
385+
public async Task CertificateValidationError_HttpsServer_HttpRequest_H3()
386+
{
387+
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http3);
388+
await host.StartAsync(TestContext.Current.CancellationToken);
389+
var console = new TestConsolePerWrite();
390+
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);
391+
392+
var client = await CommandFactory.CreateRootCommand(writer)
393+
.Parse("--method GET --uri http://localhost:5011 -v 3")
394+
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
395+
396+
await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
397+
Assert.Contains("Requesting HTTP version 3.0 with version policy RequestVersionExact while unable to establish HTTP/3 connection.", console.Text);
398+
}
399+
400+
[Fact]
401+
public async Task CertificateValidationError_HttpsServer_HttpRequest_H1()
402+
{
403+
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http1);
404+
await host.StartAsync(TestContext.Current.CancellationToken);
405+
var console = new TestConsolePerWrite();
406+
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);
407+
408+
var client = await CommandFactory.CreateRootCommand(writer)
409+
.Parse("--method GET --uri http://localhost:5011 -v 1.1")
410+
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
411+
412+
await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
413+
Assert.Contains("Invalid http(s) schema: The response ended prematurely.", console.Text);
414+
}
415+
416+
[Fact]
417+
public async Task CertificateValidationError_HttpServer_HttpsRequest()
418+
{
419+
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http1, withHttps: false);
420+
await host.StartAsync(TestContext.Current.CancellationToken);
421+
var console = new TestConsolePerWrite();
422+
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);
423+
424+
var client = await CommandFactory.CreateRootCommand(writer)
425+
.Parse("--method GET --uri https://localhost:5011 -v 1.1")
426+
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
427+
428+
await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
429+
Assert.Contains("Invalid http(s) schema: Cannot determine the frame size or a corrupted frame was received.", console.Text);
430+
}
431+
353432
private class Request
354433
{
355434
public string? Data { get; set; }

0 commit comments

Comments
 (0)