Skip to content
This repository was archived by the owner on Jul 9, 2023. It is now read-only.

Commit 66c4113

Browse files
authored
Merge pull request #584 from honfika/master
HTTP/2: allow to read the body
2 parents 03cd1e1 + e6a201e commit 66c4113

File tree

7 files changed

+167
-62
lines changed

7 files changed

+167
-62
lines changed

examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public partial class MainWindow : Window
3636
public MainWindow()
3737
{
3838
proxyServer = new ProxyServer();
39+
40+
proxyServer.EnableHttp2 = true;
41+
3942
//proxyServer.CertificateManager.CertificateEngine = CertificateEngine.DefaultWindows;
4043

4144
////Set a password for the .pfx file
@@ -150,11 +153,9 @@ private async Task ProxyServer_BeforeRequest(object sender, SessionEventArgs e)
150153
SessionListItem item = null;
151154
await Dispatcher.InvokeAsync(() => { item = addSession(e); });
152155

153-
if (e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Http2)
154-
{
155-
// GetRequestBody for HTTP/2 currently not supported
156-
return;
157-
}
156+
//if (e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Http2)
157+
//{
158+
//}
158159

159160
if (e.HttpClient.Request.HasBody)
160161
{
@@ -174,11 +175,9 @@ await Dispatcher.InvokeAsync(() =>
174175
}
175176
});
176177

177-
if (e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Http2)
178-
{
179-
// GetRequestBody for HTTP/2 currently not supported
180-
return;
181-
}
178+
//if (e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Http2)
179+
//{
180+
//}
182181

183182
if (item != null)
184183
{

src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,28 @@ private async Task readRequestBodyAsync(CancellationToken cancellationToken)
8989
// If not already read (not cached yet)
9090
if (!request.IsBodyRead)
9191
{
92-
var body = await readBodyAsync(true, cancellationToken);
93-
request.Body = body;
92+
if (request.HttpVersion == HttpHeader.Version20)
93+
{
94+
request.Http2BodyData = new MemoryStream();
95+
96+
var tcs = new TaskCompletionSource<bool>();
97+
request.ReadHttp2BodyTaskCompletionSource = tcs;
98+
99+
// signal to HTTP/2 copy frame method to continue
100+
request.ReadHttp2BeforeHandlerTaskCompletionSource.SetResult(true);
101+
102+
await tcs.Task;
103+
}
104+
else
105+
{
106+
var body = await readBodyAsync(true, cancellationToken);
107+
request.Body = body;
94108

95-
// Now set the flag to true
96-
// So that next time we can deliver body from cache
97-
request.IsBodyRead = true;
98-
OnDataSent(body, 0, body.Length);
109+
// Now set the flag to true
110+
// So that next time we can deliver body from cache
111+
request.IsBodyRead = true;
112+
OnDataSent(body, 0, body.Length);
113+
}
99114
}
100115
}
101116

@@ -140,13 +155,28 @@ private async Task readResponseBodyAsync(CancellationToken cancellationToken)
140155
// If not already read (not cached yet)
141156
if (!response.IsBodyRead)
142157
{
143-
var body = await readBodyAsync(false, cancellationToken);
144-
response.Body = body;
158+
if (response.HttpVersion == HttpHeader.Version20)
159+
{
160+
response.Http2BodyData = new MemoryStream();
161+
162+
var tcs = new TaskCompletionSource<bool>();
163+
response.ReadHttp2BodyTaskCompletionSource = tcs;
164+
165+
// signal to HTTP/2 copy frame method to continue
166+
response.ReadHttp2BeforeHandlerTaskCompletionSource.SetResult(true);
167+
168+
await tcs.Task;
169+
}
170+
else
171+
{
172+
var body = await readBodyAsync(false, cancellationToken);
173+
response.Body = body;
145174

146-
// Now set the flag to true
147-
// So that next time we can deliver body from cache
148-
response.IsBodyRead = true;
149-
OnDataReceived(body, 0, body.Length);
175+
// Now set the flag to true
176+
// So that next time we can deliver body from cache
177+
response.IsBodyRead = true;
178+
OnDataReceived(body, 0, body.Length);
179+
}
150180
}
151181
}
152182

src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,25 +55,7 @@ protected SessionEventArgsBase(ProxyServer server, ProxyEndPoint endPoint,
5555
HttpClient = new HttpWebClient(request);
5656
LocalEndPoint = endPoint;
5757

58-
HttpClient.ProcessId = new Lazy<int>(() =>
59-
{
60-
if (RunTime.IsWindows)
61-
{
62-
var remoteEndPoint = ClientEndPoint;
63-
64-
// If client is localhost get the process id
65-
if (NetworkHelper.IsLocalIpAddress(remoteEndPoint.Address))
66-
{
67-
var ipVersion = endPoint.IpV6Enabled ? IpVersion.Ipv6 : IpVersion.Ipv4;
68-
return TcpHelper.GetProcessIdByLocalPort(ipVersion, remoteEndPoint.Port);
69-
}
70-
71-
// can't access process Id of remote request from remote machine
72-
return -1;
73-
}
74-
75-
throw new PlatformNotSupportedException();
76-
});
58+
HttpClient.ProcessId = new Lazy<int>(() => ProxyClient.Connection.GetProcessId(endPoint));
7759
}
7860

7961
/// <summary>

src/Titanium.Web.Proxy/Http/RequestResponseBase.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Diagnostics.CodeAnalysis;
44
using System.IO;
55
using System.Text;
6+
using System.Threading.Tasks;
67
using Titanium.Web.Proxy.Compression;
78
using Titanium.Web.Proxy.Extensions;
89
using Titanium.Web.Proxy.Helpers;
@@ -49,6 +50,12 @@ public abstract class RequestResponseBase
4950
/// </summary>
5051
internal string OriginalContentEncoding { get; set; }
5152

53+
internal TaskCompletionSource<bool> ReadHttp2BeforeHandlerTaskCompletionSource;
54+
55+
internal TaskCompletionSource<bool> ReadHttp2BodyTaskCompletionSource;
56+
57+
internal MemoryStream Http2BodyData;
58+
5259
/// <summary>
5360
/// Keeps the body data after the session is finished.
5461
/// </summary>

src/Titanium.Web.Proxy/Http2/Http2Helper.cs

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,14 @@ internal static async Task SendHttp2(Stream clientStream, Stream serverStream, i
3939
CancellationTokenSource cancellationTokenSource, Guid connectionId,
4040
ExceptionHandler exceptionFunc)
4141
{
42-
var decoder = new Decoder(8192, 4096 * 16);
4342
var sessions = new ConcurrentDictionary<int, SessionEventArgs>();
4443

4544
// Now async relay all server=>client & client=>server data
4645
var sendRelay =
47-
copyHttp2FrameAsync(clientStream, serverStream, onDataSend, sessionFactory, decoder, sessions, onBeforeRequest,
46+
copyHttp2FrameAsync(clientStream, serverStream, onDataSend, sessionFactory, sessions, onBeforeRequest,
4847
bufferSize, connectionId, true, cancellationTokenSource.Token, exceptionFunc);
4948
var receiveRelay =
50-
copyHttp2FrameAsync(serverStream, clientStream, onDataReceive, sessionFactory, decoder, sessions, onBeforeResponse,
49+
copyHttp2FrameAsync(serverStream, clientStream, onDataReceive, sessionFactory, sessions, onBeforeResponse,
5150
bufferSize, connectionId, false, cancellationTokenSource.Token, exceptionFunc);
5251

5352
await Task.WhenAny(sendRelay, receiveRelay);
@@ -57,11 +56,13 @@ internal static async Task SendHttp2(Stream clientStream, Stream serverStream, i
5756
}
5857

5958
private static async Task copyHttp2FrameAsync(Stream input, Stream output, Action<byte[], int, int> onCopy,
60-
Func<SessionEventArgs> sessionFactory, Decoder decoder, ConcurrentDictionary<int, SessionEventArgs> sessions,
59+
Func<SessionEventArgs> sessionFactory, ConcurrentDictionary<int, SessionEventArgs> sessions,
6160
Func<SessionEventArgs, Task> onBeforeRequestResponse,
6261
int bufferSize, Guid connectionId, bool isClient, CancellationToken cancellationToken,
6362
ExceptionHandler exceptionFunc)
6463
{
64+
var decoder = new Decoder(8192, 4096 * 16);
65+
6566
var headerBuffer = new byte[9];
6667
var buffer = new byte[32768];
6768
while (true)
@@ -88,14 +89,61 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio
8889

8990
bool endStream = false;
9091

91-
//System.Diagnostics.Debug.WriteLine("CLIENT: " + isClient + ", STREAM: " + streamId + ", TYPE: " + type);
92+
SessionEventArgs args = null;
93+
RequestResponseBase rr = null;
94+
if (type == 0 || type == 1)
95+
{
96+
if (!sessions.TryGetValue(streamId, out args))
97+
{
98+
if (type == 0)
99+
{
100+
throw new ProxyHttpException("HTTP Body data received before any header frame.", null, args);
101+
}
102+
103+
if (!isClient)
104+
{
105+
throw new ProxyHttpException("HTTP Response received before any Request header frame.", null, args);
106+
}
107+
108+
args = sessionFactory();
109+
sessions.TryAdd(streamId, args);
110+
}
111+
112+
rr = isClient ? (RequestResponseBase)args.HttpClient.Request : args.HttpClient.Response;
113+
}
114+
115+
//System.Diagnostics.Debug.WriteLine("CONN: " + connectionId + ", CLIENT: " + isClient + ", STREAM: " + streamId + ", TYPE: " + type);
92116
if (type == 0 /* data */)
93117
{
94118
bool endStreamFlag = (flags & (int)Http2FrameFlag.EndStream) != 0;
95119
if (endStreamFlag)
96120
{
97121
endStream = true;
98122
}
123+
124+
if (rr.ReadHttp2BodyTaskCompletionSource != null)
125+
{
126+
// Get body method was called in the "before" event handler
127+
128+
var data = rr.Http2BodyData;
129+
data.Write(buffer, 0, length);
130+
131+
if (endStream)
132+
{
133+
rr.Body = data.ToArray();
134+
rr.IsBodyRead = true;
135+
136+
var tcs = rr.ReadHttp2BodyTaskCompletionSource;
137+
rr.ReadHttp2BodyTaskCompletionSource = null;
138+
139+
if (!tcs.Task.IsCompleted)
140+
{
141+
tcs.SetResult(true);
142+
}
143+
144+
rr.Http2BodyData = null;
145+
}
146+
}
99147
}
100148
else if (type == 1 /*headers*/)
101149
{
@@ -125,13 +173,6 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio
125173
dataLength -= buffer[0];
126174
}
127175

128-
if (!sessions.TryGetValue(streamId, out var args))
129-
{
130-
// todo: remove sessions when finished, otherwise it will be a "memory leak"
131-
args = sessionFactory();
132-
sessions.TryAdd(streamId, args);
133-
}
134-
135176
var headerListener = new MyHeaderListener(
136177
(name, value) =>
137178
{
@@ -149,16 +190,18 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio
149190

150191
if (isClient)
151192
{
152-
args.HttpClient.Request.HttpVersion = HttpVersion.Version20;
153-
args.HttpClient.Request.Method = headerListener.Method;
154-
args.HttpClient.Request.OriginalUrl = headerListener.Status;
155-
args.HttpClient.Request.RequestUri = headerListener.GetUri();
193+
var request = args.HttpClient.Request;
194+
request.HttpVersion = HttpVersion.Version20;
195+
request.Method = headerListener.Method;
196+
request.OriginalUrl = headerListener.Status;
197+
request.RequestUri = headerListener.GetUri();
156198
}
157199
else
158200
{
159-
args.HttpClient.Response.HttpVersion = HttpVersion.Version20;
201+
var response = args.HttpClient.Response;
202+
response.HttpVersion = HttpVersion.Version20;
160203
int.TryParse(headerListener.Status, out int statusCode);
161-
args.HttpClient.Response.StatusCode = statusCode;
204+
response.StatusCode = statusCode;
162205
}
163206
}
164207
catch (Exception ex)
@@ -168,13 +211,25 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio
168211

169212
if (endHeaders)
170213
{
171-
await onBeforeRequestResponse(args);
214+
var tcs = new TaskCompletionSource<bool>();
215+
rr.ReadHttp2BeforeHandlerTaskCompletionSource = tcs;
216+
217+
var handler = onBeforeRequestResponse(args);
218+
219+
if (handler == await Task.WhenAny(tcs.Task, handler))
220+
{
221+
rr.ReadHttp2BeforeHandlerTaskCompletionSource = null;
222+
tcs.SetResult(true);
223+
}
224+
225+
rr.Locked = true;
172226
}
173227
}
174228

175229
if (!isClient && endStream)
176230
{
177231
sessions.TryRemove(streamId, out _);
232+
//System.Diagnostics.Debug.WriteLine("REMOVED CONN: " + connectionId + ", CLIENT: " + isClient + ", STREAM: " + streamId + ", TYPE: " + type);
178233
}
179234

180235
// do not cancel the write operation
@@ -186,7 +241,7 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio
186241
return;
187242
}
188243

189-
/*using (var fs = new System.IO.FileStream($@"c:\11\{connectionId}.{streamId}.dat", FileMode.Append))
244+
/*using (var fs = new System.IO.FileStream($@"c:\temp\{connectionId}.{streamId}.dat", FileMode.Append))
190245
{
191246
fs.Write(headerBuffer, 0, headerBuffer.Length);
192247
fs.Write(buffer, 0, length);

src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Net.Sockets;
88
using System.Threading.Tasks;
99
using Titanium.Web.Proxy.Extensions;
10+
using Titanium.Web.Proxy.Helpers;
11+
using Titanium.Web.Proxy.Models;
1012

1113
namespace Titanium.Web.Proxy.Network.Tcp
1214
{
@@ -34,11 +36,42 @@ internal TcpClientConnection(ProxyServer proxyServer, TcpClient tcpClient)
3436

3537
private readonly TcpClient tcpClient;
3638

39+
private int? processId;
40+
3741
public Stream GetStream()
3842
{
3943
return tcpClient.GetStream();
4044
}
4145

46+
public int GetProcessId(ProxyEndPoint endPoint)
47+
{
48+
if (processId.HasValue)
49+
{
50+
return processId.Value;
51+
}
52+
53+
if (RunTime.IsWindows)
54+
{
55+
var remoteEndPoint = (IPEndPoint)RemoteEndPoint;
56+
57+
// If client is localhost get the process id
58+
if (NetworkHelper.IsLocalIpAddress(remoteEndPoint.Address))
59+
{
60+
var ipVersion = endPoint.IpV6Enabled ? IpVersion.Ipv6 : IpVersion.Ipv4;
61+
processId = TcpHelper.GetProcessIdByLocalPort(ipVersion, remoteEndPoint.Port);
62+
}
63+
else
64+
{
65+
// can't access process Id of remote request from remote machine
66+
processId = -1;
67+
}
68+
69+
return processId.Value;
70+
}
71+
72+
throw new PlatformNotSupportedException();
73+
}
74+
4275
/// <summary>
4376
/// Dispose.
4477
/// </summary>

src/Titanium.Web.Proxy/ProxyServer.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ public ProxyServer(string rootCertificateName, string rootCertificateIssuerName,
149149
/// Enable disable HTTP/2 support.
150150
/// Warning: HTTP/2 support is very limited
151151
/// - only enabled when both client and server supports it (no protocol changing in proxy)
152-
/// - GetRequest/ResponseBody(AsString) methods are not supported
153152
/// - cannot modify the request/response (e.g header modifications in BeforeRequest/Response events are ignored)
154153
/// </summary>
155154
public bool EnableHttp2 { get; set; } = false;

0 commit comments

Comments
 (0)