Skip to content
6 changes: 5 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ jobs:
run: dotnet build CHttpTools.slnx -c ${{ env.CONFIGURATION }}
- name: Test
run: |
dotnet test --no-build -c ${{ env.CONFIGURATION }}
dotnet test ./tests/CHttp.Tests --no-build -c ${{ env.CONFIGURATION }}
dotnet test ./tests/CHttpExecutor.Tests --no-build -c ${{ env.CONFIGURATION }}
dotnet test ./tests/TestWebApplication.Tests --no-build -c ${{ env.CONFIGURATION }}
dotnet test ./tests/CHttp.Api.Tests --no-build -c ${{ env.CONFIGURATION }}
dotnet test ./tests/CHttpServer.Tests --no-build -c ${{ env.CONFIGURATION }}
- name: Publish VSCE
run: |
pushd ./src/VSCodeExt/
Expand Down
6 changes: 3 additions & 3 deletions src/CHttp/Binders/HttpBehaviorBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ namespace CHttp.Binders;

internal sealed class HttpBehaviorBinder(
Option<bool> redirectBinder,
Option<bool> enableCertificateValidationBinder,
Option<bool> validateCertificateValidationBinder,
Option<double> timeout,
Option<FileInfo?> cookieContainerOption,
Option<bool> kerberosAuthOption,
Option<bool> decompressResponse)
{
private readonly Option<bool> _redirectBinder = redirectBinder;
private readonly Option<bool> _enableCertificateValidationBinder = enableCertificateValidationBinder;
private readonly Option<bool> _validateCertificateValidationBinder = validateCertificateValidationBinder;
private readonly Option<double> _timeoutOption = timeout;
private readonly Option<FileInfo?> _cookieContainerOption = cookieContainerOption;
private readonly Option<bool> _kerberosAuthOption = kerberosAuthOption;
Expand All @@ -21,7 +21,7 @@ internal sealed class HttpBehaviorBinder(
internal HttpBehavior Bind(ParseResult parseResult)
{
var redirects = parseResult.GetValue(_redirectBinder);
var enableCertificateValidation = parseResult.GetValue(_enableCertificateValidationBinder);
var enableCertificateValidation = !parseResult.GetValue(_validateCertificateValidationBinder);
var timeout = parseResult.GetValue(_timeoutOption);
var cookieContainer = parseResult.GetValue(_cookieContainerOption)?.FullName ?? string.Empty;
var kerberosAuth = parseResult.GetValue(_kerberosAuthOption);
Expand Down
2 changes: 1 addition & 1 deletion src/CHttp/CHttp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net9.0;net11.0</TargetFrameworks>
<TargetFrameworks>net9.0;net10.0;net11.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackAsTool>true</PackAsTool>
Expand Down
6 changes: 2 additions & 4 deletions src/CHttp/CommandFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ public static Command CreateRootCommand(
IFileSystem? fileSystem = null)
{
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// kerberos auth
// certificates
// TLS
// proxy

Expand Down Expand Up @@ -122,9 +120,9 @@ public static Command CreateRootCommand(
{
var value = parseResult.Tokens.FirstOrDefault()?.Value;
if (value == null)
return false;
return true;
if (bool.TryParse(value, out var result))
return !result; // Invert the value to match the option name
return result; // Invert the value to match the option name
parseResult.AddError("Invalid value for --no-certificate-validation. Expected 'true' or 'false' or no value.");
return false;
},
Expand Down
14 changes: 7 additions & 7 deletions src/CHttp/Data/Summary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal Summary(string url, DateTime startTime, TimeSpan duration)
private long _endTime;
public long EndTime
{
get => _endTime;
readonly get => _endTime;
set
{
if (_endTime != default || StartTime == default)
Expand All @@ -59,7 +59,7 @@ public void RequestCompleted(HttpStatusCode statusCode)
HttpStatusCode = (int)statusCode;
}

public override string ToString()
public readonly override string ToString()
{
if (!string.IsNullOrEmpty(Error))
return Error;
Expand All @@ -69,19 +69,19 @@ public override string ToString()
inputs.Url.CopyTo(buffer);
buffer = buffer.Slice(inputs.Url.Length);
buffer[0] = ' ';
buffer = buffer.Slice(1);
buffer = buffer[1..];
var responseSize = inputs.Length;
if (SizeFormatter<double>.TryFormatSize(responseSize, buffer, out var count))
{
buffer = buffer.Slice(count);
buffer = buffer[count..];
buffer[0] = ' ';
buffer = buffer.Slice(1);
buffer = buffer[1..];
}
if (!inputs.Duration.TryFormat(buffer, out count, "c"))
ThrowInvalidOperationException();
buffer = buffer.Slice(count);
buffer = buffer[count..];
buffer[0] = 's';
buffer = buffer.Slice(1);
buffer = buffer[1..];
buffer.Fill(' ');
});
}
Expand Down
22 changes: 20 additions & 2 deletions src/CHttp/Http/HttpMessageSender.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO.Pipelines;
using System.Net.Http.Headers;
using System.Security.Authentication;
using System.Text;
using CHttp.Data;
using CHttp.Writers;
Expand Down Expand Up @@ -46,7 +47,7 @@ public async Task SendRequestAsync(HttpRequestDetails requestData, CancellationT

private async Task SendRequestAsync(HttpClient client, HttpRequestMessage request, CancellationToken token = default)
{
Summary summary = new Summary(request.RequestUri?.ToString() ?? string.Empty);
Summary summary = new(request.RequestUri?.ToString() ?? string.Empty);
HttpResponseHeaders? trailers = null;
{
try
Expand All @@ -61,7 +62,7 @@ private async Task SendRequestAsync(HttpClient client, HttpRequestMessage reques
}
catch (HttpRequestException requestException)
{
summary = summary with { Error = $"Request Error {requestException}", ErrorCode = ErrorType.HttpRequestException };
summary = summary with { Error = HandleRequestException(requestException), ErrorCode = ErrorType.HttpRequestException };
}
catch (HttpProtocolException protocolException)
{
Expand Down Expand Up @@ -112,4 +113,21 @@ private void SetHeaders(HttpRequestDetails requestData, HttpRequestMessage reque
}
}
}

private string HandleRequestException(HttpRequestException requestException)
{
if (requestException.InnerException is AuthenticationException authException)
{
if (authException.Message.StartsWith("The remote certificate is invalid"))
return $"Enable flag --no-certificate-validation true'. SSL error: {authException.Message}";
if (authException.Message.StartsWith("Cannot determine the frame size or a corrupted frame was received"))
return $"Invalid http(s) schema: {authException.Message}";
}

if (requestException.InnerException is HttpIOException ioException)
{
return $"Invalid http(s) schema: {ioException.Message}";
}
return $"Request Error {requestException}";
}
}
2 changes: 1 addition & 1 deletion src/CHttpServer/CHttpServer/Http3/Http3Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private async Task TryCloseConnection()
/// <summary>Graceful shutdown (until token gets cancelled).</summary>
internal async Task StopAsync(CancellationToken token)
{
using var _ = token.Register(() => _abruptCts.Cancel()); // Registers for immediate abort.
using var _ = token.Register(() => { try { _abruptCts.Cancel(); } catch (ObjectDisposedException) { } }); // Registers for immediate abort.
_context.ConnectionCancellation.Cancel(); // Graceful shutdown
await _processingCompleted.Task;
}
Expand Down
6 changes: 5 additions & 1 deletion src/CHttpServer/CHttpServer/ManualResetValueTaskSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ internal class ManualResetValueTaskSource<T> : IValueTaskSource<T>, IValueTaskSo
public bool RunContinuationsAsynchronously { get => _core.RunContinuationsAsynchronously; set => _core.RunContinuationsAsynchronously = value; }
public short Version => _core.Version;
public void Reset() => _core.Reset();
public void SetException(Exception error) => _core.SetException(error);
public void SetException(Exception error)
{
if (_core.GetStatus(_core.Version) == ValueTaskSourceStatus.Pending)
_core.SetException(error);
}
void IValueTaskSource.GetResult(short token) => _core.GetResult(token);
public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token);
public void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
Expand Down
2 changes: 1 addition & 1 deletion tests/CHttp.Tests/AwaiterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ public async Task WaitAsync_Waits50Ms()
timeProvider.Advance(TimeSpan.FromMilliseconds(49));
Assert.False(waiting.IsCompleted);
timeProvider.Advance(TimeSpan.FromMilliseconds(1));
await waiting.WaitAsync(TimeSpan.FromSeconds(1));
await waiting.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
}
}
81 changes: 80 additions & 1 deletion tests/CHttp.Tests/Http/CHttpFunctionalTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.IO.Compression;
using System.Text;
using CHttp.Abstractions;
Expand Down Expand Up @@ -350,6 +349,86 @@ public async Task Diff_FunctionalTest()
Assert.Contains("probability, the base session's true mean latency is", console.Text);
}

[Fact]
public async Task CertificateValidationError_WithProposedSuggestion()
{
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http2);
await host.StartAsync(TestContext.Current.CancellationToken);
var console = new TestConsolePerWrite();
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);

var client = await CommandFactory.CreateRootCommand(writer)
.Parse("--method GET --uri https://localhost:5011 -v 2")
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);

await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
Assert.Contains("Enable flag --no-certificate-validation true'. SSL error: The remote certificate is invalid because of errors in the certificate chain", console.Text);
}

[Fact]
public async Task CertificateValidationError_HttpsServer_HttpRequest_H2()
{
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http2);
await host.StartAsync(TestContext.Current.CancellationToken);
var console = new TestConsolePerWrite();
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);

var client = await CommandFactory.CreateRootCommand(writer)
.Parse("--method GET --uri http://localhost:5011 -v 2")
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);

await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
Assert.Contains("Invalid http(s) schema: An HTTP/2 connection could not be established", console.Text);
}

[Fact]
public async Task CertificateValidationError_HttpsServer_HttpRequest_H3()
{
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http3);
await host.StartAsync(TestContext.Current.CancellationToken);
var console = new TestConsolePerWrite();
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);

var client = await CommandFactory.CreateRootCommand(writer)
.Parse("--method GET --uri http://localhost:5011 -v 3")
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);

await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
Assert.Contains("Requesting HTTP version 3.0 with version policy RequestVersionExact while unable to establish HTTP/3 connection.", console.Text);
}

[Fact]
public async Task CertificateValidationError_HttpsServer_HttpRequest_H1()
{
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http1);
await host.StartAsync(TestContext.Current.CancellationToken);
var console = new TestConsolePerWrite();
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);

var client = await CommandFactory.CreateRootCommand(writer)
.Parse("--method GET --uri http://localhost:5011 -v 1.1")
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);

await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
Assert.Contains("Invalid http(s) schema: The response ended prematurely.", console.Text);
}

[Fact]
public async Task CertificateValidationError_HttpServer_HttpsRequest()
{
using var host = HttpServer.CreateHostBuilder(context => context.Response.WriteAsync("test"), HttpProtocols.Http1, withHttps: false);
await host.StartAsync(TestContext.Current.CancellationToken);
var console = new TestConsolePerWrite();
var writer = new SilentConsoleWriter(new TextBufferedProcessor(), console);

var client = await CommandFactory.CreateRootCommand(writer)
.Parse("--method GET --uri https://localhost:5011 -v 1.1")
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);

await writer.CompleteAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
Assert.Contains("Invalid http(s) schema: Cannot determine the frame size or a corrupted frame was received.", console.Text);
}

private class Request
{
public string? Data { get; set; }
Expand Down
54 changes: 28 additions & 26 deletions tests/CHttp.Tests/HttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,34 @@ namespace CHttp.Tests;

public static class HttpServer
{
public static WebApplication CreateHostBuilder(RequestDelegate? requestDelegate = null,
HttpProtocols? protocol = null,
Action<KestrelServerOptions>? configureKestrel = null,
Action<IServiceCollection>? configureServices = null,
Action<WebApplication>? configureApp = null,
int port = 5011,
string path = "/")
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseKestrel(kestrel =>
{
kestrel.ListenAnyIP(port, options =>
{
options.UseHttps(X509CertificateLoader.LoadPkcs12FromFile("testCert.pfx", "testPassword"));
options.Protocols = protocol ?? HttpProtocols.Http3;
});
configureKestrel?.Invoke(kestrel);
});
public static WebApplication CreateHostBuilder(RequestDelegate? requestDelegate = null,
HttpProtocols? protocol = null,
Action<KestrelServerOptions>? configureKestrel = null,
Action<IServiceCollection>? configureServices = null,
Action<WebApplication>? configureApp = null,
int port = 5011,
string path = "/",
bool withHttps = true)
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseKestrel(kestrel =>
{
kestrel.ListenAnyIP(port, options =>
{
if (withHttps)
options.UseHttps(X509CertificateLoader.LoadPkcs12FromFile("testCert.pfx", "testPassword"));
options.Protocols = protocol ?? HttpProtocols.Http3;
});
configureKestrel?.Invoke(kestrel);
});

configureServices?.Invoke(builder.Services);
var app = builder.Build();
configureServices?.Invoke(builder.Services);
var app = builder.Build();

if (requestDelegate != null)
app.Map(path, requestDelegate);
else if (configureApp != null)
configureApp.Invoke(app);
return app;
}
if (requestDelegate != null)
app.Map(path, requestDelegate);
else
configureApp?.Invoke(app);
return app;
}
}
10 changes: 5 additions & 5 deletions tests/CHttp.Tests/Writers/StreamBufferedProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public async Task Run_CopiesDataTo_OutputStream()
var input = Enumerable.Range(0, 100).Select(x => (byte)x).ToArray();
var sut = new StreamBufferedProcessor(outputStream);
var sutRun = sut.RunAsync(_ => Task.CompletedTask);
await sut.Pipe.WriteAsync(input);
await sut.Pipe.WriteAsync(input, TestContext.Current.CancellationToken);
sut.Pipe.Complete();
await sutRun;
Assert.True(input.SequenceEqual(outputStream.ToArray()));
Expand All @@ -31,7 +31,7 @@ public async Task EmptyArrayRun_CopiesDataTo_OutputStream()
var input = new byte[0];
var sut = new StreamBufferedProcessor(outputStream);
var sutRun = sut.RunAsync(_ => Task.CompletedTask);
await sut.Pipe.WriteAsync(input);
await sut.Pipe.WriteAsync(input, TestContext.Current.CancellationToken);
sut.Pipe.Complete();
await sutRun;
Assert.True(input.SequenceEqual(outputStream.ToArray()));
Expand All @@ -44,7 +44,7 @@ public async Task LargeArrayRun_CopiesDataTo_OutputStream()
var input = Enumerable.Range(0, 100000).Select(x => (byte)x).ToArray();
var sut = new StreamBufferedProcessor(outputStream);
var sutRun = sut.RunAsync(_ => Task.CompletedTask);
await sut.Pipe.WriteAsync(input);
await sut.Pipe.WriteAsync(input, TestContext.Current.CancellationToken);
sut.Pipe.Complete();
await sutRun;
Assert.True(input.SequenceEqual(outputStream.ToArray()));
Expand All @@ -63,7 +63,7 @@ public async Task MultiWrittenArrayRun_CopiesDataTo_OutputStream(int segmentLeng

while (remainderToWrite.Length > 0)
{
await sut.Pipe.WriteAsync(remainderToWrite.Slice(0, segmentLength));
await sut.Pipe.WriteAsync(remainderToWrite.Slice(0, segmentLength), TestContext.Current.CancellationToken);
remainderToWrite = remainderToWrite.Slice(segmentLength);
}

Expand All @@ -79,7 +79,7 @@ public async Task WhenCancelled_Run_Completes()
var sut = new StreamBufferedProcessor(outputStream);
var sutRun = sut.RunAsync(_ => Task.CompletedTask);
sut.Cancel();
await sutRun.WaitAsync(TimeSpan.FromSeconds(1));
await sutRun.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
Assert.Equal(0, outputStream.Length);
Assert.Equal(0, sut.Position);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/CHttpExecutor.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ public async Task FailingAssert_ReturnsErrors()
{
using var host = HttpServer.CreateHostBuilder(async context =>
{
await Task.Delay(1);
await Task.Delay(2);
await context.Response.WriteAsync("test");
}, HttpProtocols.Http2, port: Port);
await host.StartAsync(TestContext.Current.CancellationToken);
Expand Down
Loading
Loading