Skip to content

Commit 1e60d31

Browse files
committed
feat(http11): add h2c upgrade detection with IProtocolSwitchCapable signaling
1 parent 1b01747 commit 1e60d31

2 files changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using System.Text;
2+
using Akka.Actor;
3+
using Akka.Event;
4+
using Servus.Akka.Transport;
5+
using TurboHTTP.Protocol;
6+
using TurboHTTP.Protocol.Syntax.Http11.Server;
7+
using TurboHTTP.Protocol.Syntax.Http2.Server;
8+
using TurboHTTP.Server;
9+
using TurboHTTP.Streams.Stages.Server;
10+
11+
namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server;
12+
13+
public sealed class Http11UpgradeH2cSpec
14+
{
15+
private sealed class FakeOps : IServerStageOperations
16+
{
17+
public List<HttpRequestMessage> EmittedRequests { get; } = [];
18+
public List<ITransportOutbound> EmittedOutbound { get; } = [];
19+
public ILoggingAdapter Log { get; } = NoLogger.Instance;
20+
public IActorRef StageActor { get; set; } = ActorRefs.Nobody;
21+
22+
public void OnRequest(HttpRequestMessage request) => EmittedRequests.Add(request);
23+
public void OnOutbound(ITransportOutbound item) => EmittedOutbound.Add(item);
24+
public void OnScheduleTimer(string name, TimeSpan delay) { }
25+
public void OnCancelTimer(string name) { }
26+
}
27+
28+
private sealed class SwitchCapableOps : IServerStageOperations, IProtocolSwitchCapable
29+
{
30+
private readonly FakeOps _inner = new();
31+
public Func<IServerStageOperations, IServerStateMachine>? SwitchFactory { get; private set; }
32+
33+
public List<HttpRequestMessage> EmittedRequests => _inner.EmittedRequests;
34+
public List<ITransportOutbound> EmittedOutbound => _inner.EmittedOutbound;
35+
public ILoggingAdapter Log => _inner.Log;
36+
public IActorRef StageActor { get => _inner.StageActor; set => _inner.StageActor = value; }
37+
38+
public void OnRequest(HttpRequestMessage request) => _inner.OnRequest(request);
39+
public void OnOutbound(ITransportOutbound item) => _inner.OnOutbound(item);
40+
public void OnScheduleTimer(string name, TimeSpan delay) => _inner.OnScheduleTimer(name, delay);
41+
public void OnCancelTimer(string name) => _inner.OnCancelTimer(name);
42+
43+
public void RequestProtocolSwitch(Func<IServerStageOperations, IServerStateMachine> newSmFactory)
44+
{
45+
SwitchFactory = newSmFactory;
46+
}
47+
}
48+
49+
private static TransportData MakeData(string raw)
50+
{
51+
var data = Encoding.ASCII.GetBytes(raw);
52+
var buffer = TransportBuffer.Rent(data.Length);
53+
data.CopyTo(buffer.FullMemory.Span);
54+
buffer.Length = data.Length;
55+
return new TransportData(buffer);
56+
}
57+
58+
[Fact(Timeout = 5000)]
59+
[Trait("RFC", "RFC9113-3.2")]
60+
public void DecodeClientData_should_trigger_switch_when_upgrade_h2c_with_switchable_ops()
61+
{
62+
var ops = new SwitchCapableOps();
63+
var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops);
64+
65+
sm.DecodeClientData(MakeData(
66+
"GET / HTTP/1.1\r\n" +
67+
"Host: localhost\r\n" +
68+
"Connection: Upgrade, HTTP2-Settings\r\n" +
69+
"Upgrade: h2c\r\n" +
70+
"HTTP2-Settings: AAMAAABkAAQBAAAAAAIAAAAA\r\n" +
71+
"Content-Length: 0\r\n" +
72+
"\r\n"));
73+
74+
Assert.NotNull(ops.SwitchFactory);
75+
var outbound = ops.EmittedOutbound.OfType<TransportData>().ToList();
76+
Assert.NotEmpty(outbound);
77+
var responseText = Encoding.ASCII.GetString(outbound[0].Buffer.Span);
78+
Assert.Contains("101", responseText);
79+
Assert.Contains("Upgrade: h2c", responseText);
80+
}
81+
82+
[Fact(Timeout = 5000)]
83+
[Trait("RFC", "RFC9113-3.2")]
84+
public void DecodeClientData_should_ignore_upgrade_when_ops_not_switchable()
85+
{
86+
var ops = new FakeOps();
87+
var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops);
88+
89+
sm.DecodeClientData(MakeData(
90+
"GET / HTTP/1.1\r\n" +
91+
"Host: localhost\r\n" +
92+
"Connection: Upgrade, HTTP2-Settings\r\n" +
93+
"Upgrade: h2c\r\n" +
94+
"HTTP2-Settings: AAMAAABkAAQBAAAAAAIAAAAA\r\n" +
95+
"Content-Length: 0\r\n" +
96+
"\r\n"));
97+
98+
Assert.Single(ops.EmittedRequests);
99+
Assert.Equal("GET", ops.EmittedRequests[0].Method.Method);
100+
}
101+
102+
[Fact(Timeout = 5000)]
103+
[Trait("RFC", "RFC9113-3.2")]
104+
public void DecodeClientData_should_ignore_upgrade_without_http2_settings()
105+
{
106+
var ops = new SwitchCapableOps();
107+
var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops);
108+
109+
sm.DecodeClientData(MakeData(
110+
"GET / HTTP/1.1\r\n" +
111+
"Host: localhost\r\n" +
112+
"Connection: Upgrade\r\n" +
113+
"Upgrade: h2c\r\n" +
114+
"Content-Length: 0\r\n" +
115+
"\r\n"));
116+
117+
Assert.Null(ops.SwitchFactory);
118+
Assert.Single(ops.EmittedRequests);
119+
}
120+
}

src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerStateMachine.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Akka.Event;
22
using Servus.Akka.Transport;
33
using TurboHTTP.Protocol.Syntax.Http11.Options;
4+
using TurboHTTP.Protocol.Syntax.Http2.Server;
45
using TurboHTTP.Server;
56
using TurboHTTP.Streams;
67
using TurboHTTP.Streams.Stages.Server;
@@ -21,6 +22,7 @@ internal sealed class Http11ServerStateMachine : IServerStateMachine
2122
private int _pendingResponseCount;
2223
private bool _outboundBodyPending;
2324
private bool _requestHeadersTimerActive;
25+
private readonly TurboServerOptions _serverOptions;
2426

2527
public bool CanAcceptResponse => !_outboundBodyPending && _pendingResponseCount > 0;
2628
public bool ShouldComplete { get; private set; }
@@ -29,6 +31,7 @@ public Http11ServerStateMachine(TurboServerOptions options, IServerStageOperatio
2931
{
3032
_ops = ops ?? throw new ArgumentNullException(nameof(ops));
3133
ArgumentNullException.ThrowIfNull(options);
34+
_serverOptions = options;
3235

3336
var shared = SharedHttpOptions.Default with
3437
{
@@ -121,6 +124,12 @@ public void DecodeClientData(ITransportInbound data)
121124
ShouldComplete = true;
122125
}
123126

127+
if (TryHandleH2cUpgrade(request))
128+
{
129+
_decoder.Reset();
130+
break;
131+
}
132+
124133
_pendingResponseCount++;
125134
_ops.OnRequest(request);
126135
_decoder.Reset();
@@ -219,6 +228,36 @@ public void OnBodyMessage(object msg)
219228
}
220229
}
221230

231+
private bool TryHandleH2cUpgrade(HttpRequestMessage request)
232+
{
233+
if (_ops is not IProtocolSwitchCapable switchable)
234+
{
235+
return false;
236+
}
237+
238+
if (!request.Headers.TryGetValues("Upgrade", out var upgradeValues)
239+
|| !upgradeValues.Any(v => v.Equals("h2c", StringComparison.OrdinalIgnoreCase)))
240+
{
241+
return false;
242+
}
243+
244+
if (!request.Headers.TryGetValues("HTTP2-Settings", out _))
245+
{
246+
return false;
247+
}
248+
249+
var responseBytes = "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: h2c\r\n\r\n"u8;
250+
var responseBuffer = TransportBuffer.Rent(responseBytes.Length);
251+
responseBytes.CopyTo(responseBuffer.FullMemory.Span);
252+
responseBuffer.Length = responseBytes.Length;
253+
_ops.OnOutbound(new TransportData(responseBuffer));
254+
255+
switchable.RequestProtocolSwitch(
256+
ops => new Http2ServerStateMachine(_serverOptions, ops));
257+
258+
return true;
259+
}
260+
222261
public void Cleanup()
223262
{
224263
_encoder.CancelActiveBody();

0 commit comments

Comments
 (0)