Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/actions/ServeHttp11Action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## serveHttp11Action

### Description

Force the downstream connection (client-to-proxy) to use HTTP/1.1. When the global ServeH2 option is enabled, this action overrides it for matched exchanges by only advertising HTTP/1.1 during ALPN negotiation with the client.

### Evaluation scope

Evaluation scope defines the timing where this filter will be applied.

{.alert .alert-info}
:::
**onAuthorityReceived** This scope denotes the moment fluxzy is aware the destination authority. In a regular proxy connection, it will occur the moment where fluxzy parsed the CONNECT request.
:::

### YAML configuration name

serveHttp11Action

### Settings

This action has no specific characteristic

### Example of usage

The following examples apply this action to any exchanges

Force HTTP/1.1 serving for a specific host even when ServeH2 is globally enabled.

```yaml
rules:
- filter:
typeKind: AnyFilter
actions:
- typeKind: ServeHttp11Action
```



### .NET reference

View definition of [ServeHttp11Action](https://docs.fluxzy.io/api/Fluxzy.Rules.Actions.ServeHttp11Action.html) for .NET integration.

### See also

This action has no related action

2 changes: 1 addition & 1 deletion docs/searchable-items.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/Fluxzy.Core/Core/ExchangeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ public ExchangeContext(

public bool AlwaysSendClientCertificate { get; set; }

/// <summary>
/// When true, force the downstream (client-to-proxy) connection to use HTTP/1.1
/// by only advertising HTTP/1.1 in the ALPN negotiation, regardless of the global ServeH2 setting.
/// </summary>
public bool ForceServeHttp11 { get; set; }

/// <summary>
/// Register a response body substitution
/// </summary>
Expand Down
6 changes: 4 additions & 2 deletions src/Fluxzy.Core/Core/Impl/SecureConnectionUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ public async Task<SecureConnectionUpdateResult> AuthenticateAsServer(

try {

var effectiveServeH2 = _serveH2 && !context.ForceServeHttp11;

var sslProtocols = SslProtocols.None;

if (_serveH2) {
if (effectiveServeH2) {
sslProtocols = SslProtocols.Tls12;

#if NET8_0_OR_GREATER
Expand All @@ -85,7 +87,7 @@ public async Task<SecureConnectionUpdateResult> AuthenticateAsServer(

var sslServerAuthenticationOptions = new SslServerAuthenticationOptions
{
ApplicationProtocols = _serveH2 ? H11AndH2Protocols : H11Protocols,
ApplicationProtocols = effectiveServeH2 ? H11AndH2Protocols : H11Protocols,
EnabledSslProtocols = sslProtocols,
ClientCertificateRequired = false,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
Expand Down
40 changes: 40 additions & 0 deletions src/Fluxzy.Core/Rules/Actions/ServeHttp11Action.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak

using System.Collections.Generic;
using System.Threading.Tasks;
using Fluxzy.Core;
using Fluxzy.Core.Breakpoints;

namespace Fluxzy.Rules.Actions
{
/// <summary>
/// Force the downstream connection (client-to-proxy) to use HTTP/1.1 by only advertising
/// HTTP/1.1 during ALPN negotiation, even when the global ServeH2 option is enabled.
/// </summary>
[ActionMetadata(
"Force the downstream connection (client-to-proxy) to use HTTP/1.1. " +
"When the global ServeH2 option is enabled, this action overrides it for matched exchanges " +
"by only advertising HTTP/1.1 during ALPN negotiation with the client.")]
public class ServeHttp11Action : Action
{
public override FilterScope ActionScope => FilterScope.OnAuthorityReceived;

public override string DefaultDescription => "Serve HTTP/1.1 to client";

public override ValueTask InternalAlter(
ExchangeContext context, Exchange? exchange, Connection? connection, FilterScope scope,
BreakPointManager breakPointManager)
{
context.ForceServeHttp11 = true;

return default;
}

public override IEnumerable<ActionExample> GetExamples()
{
yield return new ActionExample(
"Force HTTP/1.1 serving for a specific host even when ServeH2 is globally enabled",
new ServeHttp11Action());
}
}
}
135 changes: 135 additions & 0 deletions test/Fluxzy.Tests/UnitTests/H2Serve/ServeHttp11ActionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Fluxzy.Rules.Actions;
using Fluxzy.Rules.Filters;
using Fluxzy.Rules.Filters.RequestFilters;
using Fluxzy.Tests._Fixtures;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Xunit;

namespace Fluxzy.Tests.UnitTests.H2Serve
{
public class ServeHttp11ActionTests
{
/// <summary>
/// When ServeH2 is enabled globally but ServeHttp11Action is applied,
/// the client should receive an HTTP/1.1 response.
/// </summary>
[Fact]
public async Task ServeHttp11Action_OverridesGlobalServeH2()
{
var host = await InProcessHost.Create(app =>
{
app.MapGet("/test", async (HttpContext ctx) =>
{
ctx.Response.ContentType = "text/plain";
await ctx.Response.WriteAsync("OK");
});
});

await using var _ = host;

var setting = FluxzySetting.CreateLocalRandomPort();
setting.SetServeH2(true);

setting.AddAlterationRules(new SkipRemoteCertificateValidationAction(), AnyFilter.Default);

setting.ConfigureRule()
.WhenAny()
.Do(new ServeHttp11Action());

await using var proxy = new Proxy(setting);
var endPoints = proxy.Run();
var proxyEndPoint = endPoints.First();

// Client prefers H2 but will accept H1.1 if server only offers it
using var client = Socks5ClientFactory.Create(proxyEndPoint);
client.BaseAddress = new Uri(host.BaseUrl);
client.DefaultRequestVersion = new Version(2, 0);
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;

var response = await client.GetAsync("/test");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version);

var body = await response.Content.ReadAsStringAsync();
Assert.Equal("OK", body);
}

/// <summary>
/// When ServeH2 is enabled and ServeHttp11Action is NOT applied,
/// the client should receive an HTTP/2 response (control test).
/// </summary>
[Fact]
public async Task WithoutServeHttp11Action_ClientGetsH2()
{
await using var setup = await ProxiedHostSetup.Create(
configureSetting: setting =>
{
setting.SetServeH2(true);
},
configureRoutes: app =>
{
app.MapGet("/test", async (HttpContext ctx) =>
{
ctx.Response.ContentType = "text/plain";
await ctx.Response.WriteAsync("OK");
});
},
httpVersion: new Version(2, 0));

var response = await setup.Client.GetAsync("/test");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(new Version(2, 0), response.Version);
}

/// <summary>
/// When ServeHttp11Action is applied per-host filter, only matching hosts should be downgraded.
/// </summary>
[Fact]
public async Task ServeHttp11Action_AppliedPerHost()
{
var host = await InProcessHost.Create(app =>
{
app.MapGet("/test", async (HttpContext ctx) =>
{
ctx.Response.ContentType = "text/plain";
await ctx.Response.WriteAsync("OK");
});
});

await using var _ = host;

var setting = FluxzySetting.CreateLocalRandomPort();
setting.SetServeH2(true);

setting.AddAlterationRules(new SkipRemoteCertificateValidationAction(), AnyFilter.Default);

setting.ConfigureRule()
.When(new HostFilter("localhost"))
.Do(new ServeHttp11Action());

await using var proxy = new Proxy(setting);
var endPoints = proxy.Run();
var proxyEndPoint = endPoints.First();

using var client = Socks5ClientFactory.Create(proxyEndPoint);
client.BaseAddress = new Uri(host.BaseUrl);
client.DefaultRequestVersion = new Version(2, 0);
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;

var response = await client.GetAsync("/test");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version);
}
}
}
Loading