Commit b37d7c8f authored by Honfika's avatar Honfika

Basic HTTP/2 support. Disabled by default. Warning added to the enable flag:

- only enabled when both client and server supports it (no protocol changing in proxy)
- GetRequest/ResponseBody(AsString) methods are not supported
- cannot modify the request/response (e.g header modifications in BeforeRequest/Response events are ignored)
parent a8a822a4
...@@ -150,6 +150,12 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -150,6 +150,12 @@ namespace Titanium.Web.Proxy.Examples.Wpf
SessionListItem item = null; SessionListItem item = null;
await Dispatcher.InvokeAsync(() => { item = addSession(e); }); await Dispatcher.InvokeAsync(() => { item = addSession(e); });
if (e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Http2)
{
// GetRequestBody for HTTP/2 currently not supported
return;
}
if (e.HttpClient.Request.HasBody) if (e.HttpClient.Request.HasBody)
{ {
e.HttpClient.Request.KeepBody = true; e.HttpClient.Request.KeepBody = true;
...@@ -168,6 +174,12 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -168,6 +174,12 @@ namespace Titanium.Web.Proxy.Examples.Wpf
} }
}); });
if (e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Http2)
{
// GetRequestBody for HTTP/2 currently not supported
return;
}
if (item != null) if (item != null)
{ {
if (e.HttpClient.Response.HasBody) if (e.HttpClient.Response.HasBody)
...@@ -217,6 +229,12 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -217,6 +229,12 @@ namespace Titanium.Web.Proxy.Examples.Wpf
var session = (SessionEventArgsBase)sender; var session = (SessionEventArgsBase)sender;
if (sessionDictionary.TryGetValue(session.HttpClient, out var li)) if (sessionDictionary.TryGetValue(session.HttpClient, out var li))
{ {
var tunnelType = session.HttpClient.ConnectRequest?.TunnelType ?? TunnelType.Unknown;
if (tunnelType != TunnelType.Unknown)
{
li.Protocol = TunnelTypeToString(tunnelType);
}
li.ReceivedDataCount += args.Count; li.ReceivedDataCount += args.Count;
} }
}; };
...@@ -226,6 +244,12 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -226,6 +244,12 @@ namespace Titanium.Web.Proxy.Examples.Wpf
var session = (SessionEventArgsBase)sender; var session = (SessionEventArgsBase)sender;
if (sessionDictionary.TryGetValue(session.HttpClient, out var li)) if (sessionDictionary.TryGetValue(session.HttpClient, out var li))
{ {
var tunnelType = session.HttpClient.ConnectRequest?.TunnelType ?? TunnelType.Unknown;
if (tunnelType != TunnelType.Unknown)
{
li.Protocol = TunnelTypeToString(tunnelType);
}
li.SentDataCount += args.Count; li.SentDataCount += args.Count;
} }
}; };
...@@ -235,6 +259,21 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -235,6 +259,21 @@ namespace Titanium.Web.Proxy.Examples.Wpf
return item; return item;
} }
private string TunnelTypeToString(TunnelType tunnelType)
{
switch (tunnelType)
{
case TunnelType.Https:
return "https";
case TunnelType.Websocket:
return "websocket";
case TunnelType.Http2:
return "http2";
}
return null;
}
private void ListViewSessions_OnKeyDown(object sender, KeyEventArgs e) private void ListViewSessions_OnKeyDown(object sender, KeyEventArgs e)
{ {
if (e.Key == Key.Delete) if (e.Key == Key.Delete)
......
...@@ -11,7 +11,7 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -11,7 +11,7 @@ namespace Titanium.Web.Proxy.Examples.Wpf
private long? bodySize; private long? bodySize;
private Exception exception; private Exception exception;
private string host; private string host;
private string process; private int processId;
private string protocol; private string protocol;
private long receivedDataCount; private long receivedDataCount;
private long sentDataCount; private long sentDataCount;
...@@ -54,10 +54,32 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -54,10 +54,32 @@ namespace Titanium.Web.Proxy.Examples.Wpf
set => SetField(ref bodySize, value); set => SetField(ref bodySize, value);
} }
public int ProcessId
{
get => processId;
set
{
if (SetField(ref processId, value))
{
OnPropertyChanged(nameof(Process));
}
}
}
public string Process public string Process
{ {
get => process; get
set => SetField(ref process, value); {
try
{
var process = System.Diagnostics.Process.GetProcessById(processId);
return process.ProcessName + ":" + processId;
}
catch (Exception)
{
return string.Empty;
}
}
} }
public long ReceivedDataCount public long ReceivedDataCount
...@@ -80,13 +102,16 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -80,13 +102,16 @@ namespace Titanium.Web.Proxy.Examples.Wpf
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
protected void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null) protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{ {
if (!Equals(field, value)) if (!Equals(field, value))
{ {
field = value; field = value;
OnPropertyChanged(propertyName); OnPropertyChanged(propertyName);
return true;
} }
return false;
} }
[NotifyPropertyChangedInvocator] [NotifyPropertyChangedInvocator]
...@@ -132,20 +157,7 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -132,20 +157,7 @@ namespace Titanium.Web.Proxy.Examples.Wpf
BodySize = responseSize; BodySize = responseSize;
} }
Process = GetProcessDescription(HttpClient.ProcessId.Value); ProcessId = HttpClient.ProcessId.Value;
}
private string GetProcessDescription(int processId)
{
try
{
var process = System.Diagnostics.Process.GetProcessById(processId);
return process.ProcessName + ":" + processId;
}
catch (Exception)
{
return string.Empty;
}
} }
} }
} }
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj" />
</ItemGroup>
</Project>
\ No newline at end of file
...@@ -126,6 +126,7 @@ namespace Titanium.Web.Proxy ...@@ -126,6 +126,7 @@ namespace Titanium.Web.Proxy
bool isClientHello = clientHelloInfo != null; bool isClientHello = clientHelloInfo != null;
if (isClientHello) if (isClientHello)
{ {
connectRequest.TunnelType = TunnelType.Https;
connectRequest.ClientHelloInfo = clientHelloInfo; connectRequest.ClientHelloInfo = clientHelloInfo;
} }
...@@ -208,9 +209,9 @@ namespace Titanium.Web.Proxy ...@@ -208,9 +209,9 @@ namespace Titanium.Web.Proxy
} }
catch (Exception e) catch (Exception e)
{ {
var certname = certificate?.GetNameInfo(X509NameType.SimpleName, false); var certName = certificate?.GetNameInfo(X509NameType.SimpleName, false);
throw new ProxyConnectException( throw new ProxyConnectException(
$"Couldn't authenticate host '{connectHostname}' with certificate '{certname}'.", e, connectArgs); $"Couldn't authenticate host '{connectHostname}' with certificate '{certName}'.", e, connectArgs);
} }
if (await HttpHelper.IsConnectMethod(clientStream) == -1) if (await HttpHelper.IsConnectMethod(clientStream) == -1)
...@@ -233,6 +234,11 @@ namespace Titanium.Web.Proxy ...@@ -233,6 +234,11 @@ namespace Titanium.Web.Proxy
// Hostname is excluded or it is not an HTTPS connect // Hostname is excluded or it is not an HTTPS connect
if (!decryptSsl || !isClientHello) if (!decryptSsl || !isClientHello)
{ {
if (!isClientHello)
{
connectRequest.TunnelType = TunnelType.Websocket;
}
// create new connection to server. // create new connection to server.
// If we detected that client tunnel CONNECTs without SSL by checking for empty client hello then // If we detected that client tunnel CONNECTs without SSL by checking for empty client hello then
// this connection should not be HTTPS. // this connection should not be HTTPS.
...@@ -286,6 +292,8 @@ namespace Titanium.Web.Proxy ...@@ -286,6 +292,8 @@ namespace Titanium.Web.Proxy
string httpCmd = await clientStream.ReadLineAsync(cancellationToken); string httpCmd = await clientStream.ReadLineAsync(cancellationToken);
if (httpCmd == "PRI * HTTP/2.0") if (httpCmd == "PRI * HTTP/2.0")
{ {
connectArgs.HttpClient.ConnectRequest.TunnelType = TunnelType.Http2;
// HTTP/2 Connection Preface // HTTP/2 Connection Preface
string line = await clientStream.ReadLineAsync(cancellationToken); string line = await clientStream.ReadLineAsync(cancellationToken);
if (line != string.Empty) if (line != string.Empty)
...@@ -318,9 +326,16 @@ namespace Titanium.Web.Proxy ...@@ -318,9 +326,16 @@ namespace Titanium.Web.Proxy
await Http2Helper.SendHttp2(clientStream, connection.Stream, BufferSize, await Http2Helper.SendHttp2(clientStream, connection.Stream, BufferSize,
(buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); },
(buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); },
() => new SessionEventArgs(this, endPoint, cancellationTokenSource)
{
ProxyClient = { Connection = clientConnection },
HttpClient = { ConnectRequest = connectArgs?.HttpClient.ConnectRequest },
UserData = connectArgs?.UserData
},
async args => { await invokeBeforeRequest(args); },
async args => { await invokeBeforeResponse(args); },
connectArgs.CancellationTokenSource, clientConnection.Id, ExceptionFunc); connectArgs.CancellationTokenSource, clientConnection.Id, ExceptionFunc);
#endif #endif
} }
finally finally
{ {
......
...@@ -146,8 +146,8 @@ namespace Titanium.Web.Proxy.Helpers ...@@ -146,8 +146,8 @@ namespace Titanium.Web.Proxy.Helpers
private static async Task<int> startsWith(ICustomStreamReader clientStreamReader, string expectedStart) private static async Task<int> startsWith(ICustomStreamReader clientStreamReader, string expectedStart)
{ {
bool isExpected = true; bool isExpected = true;
int legthToCheck = 10; int lengthToCheck = 10;
for (int i = 0; i < legthToCheck; i++) for (int i = 0; i < lengthToCheck; i++)
{ {
int b = await clientStreamReader.PeekByteAsync(i); int b = await clientStreamReader.PeekByteAsync(i);
if (b == -1) if (b == -1)
......
...@@ -12,6 +12,8 @@ namespace Titanium.Web.Proxy.Http ...@@ -12,6 +12,8 @@ namespace Titanium.Web.Proxy.Http
Method = "CONNECT"; Method = "CONNECT";
} }
public TunnelType TunnelType { get; internal set; }
public ClientHelloInfo ClientHelloInfo { get; set; } public ClientHelloInfo ClientHelloInfo { get; set; }
} }
} }
...@@ -6,28 +6,28 @@ ...@@ -6,28 +6,28 @@
public static class KnownHeaders public static class KnownHeaders
{ {
// Both // Both
public const string Connection = "connection"; public const string Connection = "Connection";
public const string ConnectionClose = "close"; public const string ConnectionClose = "close";
public const string ConnectionKeepAlive = "keep-alive"; public const string ConnectionKeepAlive = "keep-alive";
public const string ContentLength = "content-length"; public const string ContentLength = "Content-Length";
public const string ContentType = "content-type"; public const string ContentType = "Content-Type";
public const string ContentTypeCharset = "charset"; public const string ContentTypeCharset = "charset";
public const string ContentTypeBoundary = "boundary"; public const string ContentTypeBoundary = "boundary";
public const string Upgrade = "upgrade"; public const string Upgrade = "Upgrade";
public const string UpgradeWebsocket = "websocket"; public const string UpgradeWebsocket = "websocket";
// Request headers // Request headers
public const string AcceptEncoding = "accept-encoding"; public const string AcceptEncoding = "Accept-Encoding";
public const string Authorization = "Authorization"; public const string Authorization = "Authorization";
public const string Expect = "expect"; public const string Expect = "Expect";
public const string Expect100Continue = "100-continue"; public const string Expect100Continue = "100-continue";
public const string Host = "host"; public const string Host = "Host";
public const string ProxyAuthorization = "Proxy-Authorization"; public const string ProxyAuthorization = "Proxy-Authorization";
public const string ProxyAuthorizationBasic = "basic"; public const string ProxyAuthorizationBasic = "basic";
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
public const string ProxyConnectionClose = "close"; public const string ProxyConnectionClose = "close";
// Response headers // Response headers
public const string ContentEncoding = "content-encoding"; public const string ContentEncoding = "Content-Encoding";
public const string ContentEncodingDeflate = "deflate"; public const string ContentEncodingDeflate = "deflate";
public const string ContentEncodingGzip = "gzip"; public const string ContentEncodingGzip = "gzip";
public const string ContentEncodingBrotli = "br"; public const string ContentEncodingBrotli = "br";
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
public const string ProxyAuthenticate = "Proxy-Authenticate"; public const string ProxyAuthenticate = "Proxy-Authenticate";
public const string TransferEncoding = "transfer-encoding"; public const string TransferEncoding = "Transfer-Encoding";
public const string TransferEncodingChunked = "chunked"; public const string TransferEncodingChunked = "chunked";
} }
} }
namespace Titanium.Web.Proxy.Http
{
public enum TunnelType
{
Unknown,
Https,
Websocket,
Http2,
}
}
...@@ -59,99 +59,99 @@ namespace Titanium.Web.Proxy.Http2.Hpack ...@@ -59,99 +59,99 @@ namespace Titanium.Web.Proxy.Http2.Hpack
/* 14 */ /* 14 */
new HttpHeader(":status", "500"), new HttpHeader(":status", "500"),
/* 15 */ /* 15 */
new HttpHeader("accept-charset", string.Empty), new HttpHeader("Accept-Charset", string.Empty),
/* 16 */ /* 16 */
new HttpHeader("accept-encoding", "gzip, deflate"), new HttpHeader("Accept-Encoding", "gzip, deflate"),
/* 17 */ /* 17 */
new HttpHeader("accept-language", string.Empty), new HttpHeader("Accept-Language", string.Empty),
/* 18 */ /* 18 */
new HttpHeader("accept-ranges", string.Empty), new HttpHeader("Accept-Ranges", string.Empty),
/* 19 */ /* 19 */
new HttpHeader("accept", string.Empty), new HttpHeader("Accept", string.Empty),
/* 20 */ /* 20 */
new HttpHeader("access-control-allow-origin", string.Empty), new HttpHeader("Access-Control-Allow-Origin", string.Empty),
/* 21 */ /* 21 */
new HttpHeader("age", string.Empty), new HttpHeader("Age", string.Empty),
/* 22 */ /* 22 */
new HttpHeader("allow", string.Empty), new HttpHeader("Allow", string.Empty),
/* 23 */ /* 23 */
new HttpHeader("authorization", string.Empty), new HttpHeader("Authorization", string.Empty),
/* 24 */ /* 24 */
new HttpHeader("cache-control", string.Empty), new HttpHeader("Cache-Control", string.Empty),
/* 25 */ /* 25 */
new HttpHeader("content-disposition", string.Empty), new HttpHeader("Content-Disposition", string.Empty),
/* 26 */ /* 26 */
new HttpHeader("content-encoding", string.Empty), new HttpHeader("Content-Encoding", string.Empty),
/* 27 */ /* 27 */
new HttpHeader("content-language", string.Empty), new HttpHeader("Content-Language", string.Empty),
/* 28 */ /* 28 */
new HttpHeader("content-length", string.Empty), new HttpHeader("Content-Length", string.Empty),
/* 29 */ /* 29 */
new HttpHeader("content-location", string.Empty), new HttpHeader("Content-Location", string.Empty),
/* 30 */ /* 30 */
new HttpHeader("content-range", string.Empty), new HttpHeader("Content-Range", string.Empty),
/* 31 */ /* 31 */
new HttpHeader("content-type", string.Empty), new HttpHeader("Content-Type", string.Empty),
/* 32 */ /* 32 */
new HttpHeader("cookie", string.Empty), new HttpHeader("Cookie", string.Empty),
/* 33 */ /* 33 */
new HttpHeader("date", string.Empty), new HttpHeader("Date", string.Empty),
/* 34 */ /* 34 */
new HttpHeader("etag", string.Empty), new HttpHeader("ETag", string.Empty),
/* 35 */ /* 35 */
new HttpHeader("expect", string.Empty), new HttpHeader("Expect", string.Empty),
/* 36 */ /* 36 */
new HttpHeader("expires", string.Empty), new HttpHeader("Expires", string.Empty),
/* 37 */ /* 37 */
new HttpHeader("from", string.Empty), new HttpHeader("From", string.Empty),
/* 38 */ /* 38 */
new HttpHeader("host", string.Empty), new HttpHeader("Host", string.Empty),
/* 39 */ /* 39 */
new HttpHeader("if-match", string.Empty), new HttpHeader("If-Match", string.Empty),
/* 40 */ /* 40 */
new HttpHeader("if-modified-since", string.Empty), new HttpHeader("If-Modified-Since", string.Empty),
/* 41 */ /* 41 */
new HttpHeader("if-none-match", string.Empty), new HttpHeader("If-None-Match", string.Empty),
/* 42 */ /* 42 */
new HttpHeader("if-range", string.Empty), new HttpHeader("If-Range", string.Empty),
/* 43 */ /* 43 */
new HttpHeader("if-unmodified-since", string.Empty), new HttpHeader("If-Unmodified-Since", string.Empty),
/* 44 */ /* 44 */
new HttpHeader("last-modified", string.Empty), new HttpHeader("Last-Modified", string.Empty),
/* 45 */ /* 45 */
new HttpHeader("link", string.Empty), new HttpHeader("Link", string.Empty),
/* 46 */ /* 46 */
new HttpHeader("location", string.Empty), new HttpHeader("Location", string.Empty),
/* 47 */ /* 47 */
new HttpHeader("max-forwards", string.Empty), new HttpHeader("Max-Forwards", string.Empty),
/* 48 */ /* 48 */
new HttpHeader("proxy-authenticate", string.Empty), new HttpHeader("Proxy-Authenticate", string.Empty),
/* 49 */ /* 49 */
new HttpHeader("proxy-authorization", string.Empty), new HttpHeader("Proxy-Authorization", string.Empty),
/* 50 */ /* 50 */
new HttpHeader("range", string.Empty), new HttpHeader("Range", string.Empty),
/* 51 */ /* 51 */
new HttpHeader("referer", string.Empty), new HttpHeader("Referer", string.Empty),
/* 52 */ /* 52 */
new HttpHeader("refresh", string.Empty), new HttpHeader("Refresh", string.Empty),
/* 53 */ /* 53 */
new HttpHeader("retry-after", string.Empty), new HttpHeader("Retry-After", string.Empty),
/* 54 */ /* 54 */
new HttpHeader("server", string.Empty), new HttpHeader("Server", string.Empty),
/* 55 */ /* 55 */
new HttpHeader("set-cookie", string.Empty), new HttpHeader("Set-Cookie", string.Empty),
/* 56 */ /* 56 */
new HttpHeader("strict-transport-security", string.Empty), new HttpHeader("Strict-Transport-Security", string.Empty),
/* 57 */ /* 57 */
new HttpHeader("transfer-encoding", string.Empty), new HttpHeader("Transfer-Encoding", string.Empty),
/* 58 */ /* 58 */
new HttpHeader("user-agent", string.Empty), new HttpHeader("User-Agent", string.Empty),
/* 59 */ /* 59 */
new HttpHeader("vary", string.Empty), new HttpHeader("Vary", string.Empty),
/* 60 */ /* 60 */
new HttpHeader("via", string.Empty), new HttpHeader("Via", string.Empty),
/* 61 */ /* 61 */
new HttpHeader("www-authenticate", string.Empty) new HttpHeader("WWW-Authenticate", string.Empty)
}; };
private static readonly Dictionary<string, int> staticIndexByName = CreateMap(); private static readonly Dictionary<string, int> staticIndexByName = CreateMap();
......
#if NETCOREAPP2_1 #if NETCOREAPP2_1
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Http2.Hpack; using Titanium.Web.Proxy.Http2.Hpack;
namespace Titanium.Web.Proxy.Http2 namespace Titanium.Web.Proxy.Http2
...@@ -25,27 +31,24 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -25,27 +31,24 @@ namespace Titanium.Web.Proxy.Http2
/// Useful for websocket requests /// Useful for websocket requests
/// Task-based Asynchronous Pattern /// Task-based Asynchronous Pattern
/// </summary> /// </summary>
/// <param name="clientStream"></param>
/// <param name="serverStream"></param>
/// <param name="bufferSize"></param>
/// <param name="onDataSend"></param>
/// <param name="onDataReceive"></param>
/// <param name="cancellationTokenSource"></param>
/// <param name="connectionId"></param>
/// <param name="exceptionFunc"></param>
/// <returns></returns> /// <returns></returns>
internal static async Task SendHttp2(Stream clientStream, Stream serverStream, int bufferSize, internal static async Task SendHttp2(Stream clientStream, Stream serverStream, int bufferSize,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive, Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive,
Func<SessionEventArgs> sessionFactory,
Func<SessionEventArgs, Task> onBeforeRequest, Func<SessionEventArgs, Task> onBeforeResponse,
CancellationTokenSource cancellationTokenSource, Guid connectionId, CancellationTokenSource cancellationTokenSource, Guid connectionId,
ExceptionHandler exceptionFunc) ExceptionHandler exceptionFunc)
{ {
var decoder = new Decoder(8192, 4096 * 16);
var sessions = new ConcurrentDictionary<int, SessionEventArgs>();
// Now async relay all server=>client & client=>server data // Now async relay all server=>client & client=>server data
var sendRelay = var sendRelay =
CopyHttp2FrameAsync(clientStream, serverStream, onDataSend, bufferSize, connectionId, copyHttp2FrameAsync(clientStream, serverStream, onDataSend, sessionFactory, decoder, sessions, onBeforeRequest,
true, cancellationTokenSource.Token); bufferSize, connectionId, true, cancellationTokenSource.Token, exceptionFunc);
var receiveRelay = var receiveRelay =
CopyHttp2FrameAsync(serverStream, clientStream, onDataReceive, bufferSize, connectionId, copyHttp2FrameAsync(serverStream, clientStream, onDataReceive, sessionFactory, decoder, sessions, onBeforeResponse,
false, cancellationTokenSource.Token); bufferSize, connectionId, false, cancellationTokenSource.Token, exceptionFunc);
await Task.WhenAny(sendRelay, receiveRelay); await Task.WhenAny(sendRelay, receiveRelay);
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
...@@ -53,16 +56,17 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -53,16 +56,17 @@ namespace Titanium.Web.Proxy.Http2
await Task.WhenAll(sendRelay, receiveRelay); await Task.WhenAll(sendRelay, receiveRelay);
} }
private static async Task CopyHttp2FrameAsync(Stream input, Stream output, Action<byte[], int, int> onCopy, private static async Task copyHttp2FrameAsync(Stream input, Stream output, Action<byte[], int, int> onCopy,
int bufferSize, Guid connectionId, bool isClient, CancellationToken cancellationToken) Func<SessionEventArgs> sessionFactory, Decoder decoder, ConcurrentDictionary<int, SessionEventArgs> sessions,
Func<SessionEventArgs, Task> onBeforeRequestResponse,
int bufferSize, Guid connectionId, bool isClient, CancellationToken cancellationToken,
ExceptionHandler exceptionFunc)
{ {
var decoder = new Decoder(8192, 4096);
var headerBuffer = new byte[9]; var headerBuffer = new byte[9];
var buffer = new byte[32768]; var buffer = new byte[32768];
while (true) while (true)
{ {
int read = await ForceRead(input, headerBuffer, 0, 9, cancellationToken); int read = await forceRead(input, headerBuffer, 0, 9, cancellationToken);
onCopy(headerBuffer, 0, read); onCopy(headerBuffer, 0, read);
if (read != 9) if (read != 9)
{ {
...@@ -75,22 +79,34 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -75,22 +79,34 @@ namespace Titanium.Web.Proxy.Http2
int streamId = ((headerBuffer[5] & 0x7f) << 24) + (headerBuffer[6] << 16) + (headerBuffer[7] << 8) + int streamId = ((headerBuffer[5] & 0x7f) << 24) + (headerBuffer[6] << 16) + (headerBuffer[7] << 8) +
headerBuffer[8]; headerBuffer[8];
read = await ForceRead(input, buffer, 0, length, cancellationToken); read = await forceRead(input, buffer, 0, length, cancellationToken);
onCopy(buffer, 0, read); onCopy(buffer, 0, read);
if (read != length) if (read != length)
{ {
return; return;
} }
if (isClient) bool endStream = false;
//System.Diagnostics.Debug.WriteLine("CLIENT: " + isClient + ", STREAM: " + streamId + ", TYPE: " + type);
if (type == 0 /* data */)
{
bool endStreamFlag = (flags & (int)Http2FrameFlag.EndStream) != 0;
if (endStreamFlag)
{ {
if (type == 1 /*headers*/) endStream = true;
}
}
else if (type == 1 /*headers*/)
{ {
bool endHeaders = (flags & (int)Http2FrameFlag.EndHeaders) != 0; bool endHeaders = (flags & (int)Http2FrameFlag.EndHeaders) != 0;
bool padded = (flags & (int)Http2FrameFlag.Padded) != 0; bool padded = (flags & (int)Http2FrameFlag.Padded) != 0;
bool priority = (flags & (int)Http2FrameFlag.Priority) != 0; bool priority = (flags & (int)Http2FrameFlag.Priority) != 0;
bool endStreamFlag = (flags & (int)Http2FrameFlag.EndStream) != 0;
System.Diagnostics.Debug.WriteLine("HEADER: " + streamId + " end: " + endHeaders); if (endStreamFlag)
{
endStream = true;
}
int offset = 0; int offset = 0;
if (padded) if (padded)
...@@ -109,21 +125,66 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -109,21 +125,66 @@ namespace Titanium.Web.Proxy.Http2
dataLength -= buffer[0]; dataLength -= buffer[0];
} }
var headerListener = new MyHeaderListener(); if (!sessions.TryGetValue(streamId, out var args))
{
// todo: remove sessions when finished, otherwise it will be a "memory leak"
args = sessionFactory();
sessions.TryAdd(streamId, args);
}
var headerListener = new MyHeaderListener(
(name, value) =>
{
var headers = isClient ? args.HttpClient.Request.Headers : args.HttpClient.Response.Headers;
headers.AddHeader(name, value);
});
try try
{
lock (decoder)
{ {
decoder.Decode(new BinaryReader(new MemoryStream(buffer, offset, dataLength)), decoder.Decode(new BinaryReader(new MemoryStream(buffer, offset, dataLength)),
headerListener); headerListener);
decoder.EndHeaderBlock(); decoder.EndHeaderBlock();
} }
catch (Exception)
if (isClient)
{
args.HttpClient.Request.HttpVersion = HttpVersion.Version20;
args.HttpClient.Request.Method = headerListener.Method;
args.HttpClient.Request.OriginalUrl = headerListener.Status;
args.HttpClient.Request.RequestUri = headerListener.GetUri();
}
else
{
args.HttpClient.Response.HttpVersion = HttpVersion.Version20;
int.TryParse(headerListener.Status, out int statusCode);
args.HttpClient.Response.StatusCode = statusCode;
}
}
catch (Exception ex)
{ {
exceptionFunc(new ProxyHttpException("Failed to decode HTTP/2 headers", ex, args));
} }
if (endHeaders)
{
await onBeforeRequestResponse(args);
} }
} }
await output.WriteAsync(headerBuffer, 0, headerBuffer.Length, cancellationToken); if (!isClient && endStream)
await output.WriteAsync(buffer, 0, length, cancellationToken); {
sessions.TryRemove(streamId, out _);
}
// do not cancel the write operation
await output.WriteAsync(headerBuffer, 0, headerBuffer.Length/*, cancellationToken*/);
await output.WriteAsync(buffer, 0, length/*, cancellationToken*/);
if (cancellationToken.IsCancellationRequested)
{
return;
}
/*using (var fs = new System.IO.FileStream($@"c:\11\{connectionId}.{streamId}.dat", FileMode.Append)) /*using (var fs = new System.IO.FileStream($@"c:\11\{connectionId}.{streamId}.dat", FileMode.Append))
{ {
...@@ -133,7 +194,7 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -133,7 +194,7 @@ namespace Titanium.Web.Proxy.Http2
} }
} }
private static async Task<int> ForceRead(Stream input, byte[] buffer, int offset, int bytesToRead, private static async Task<int> forceRead(Stream input, byte[] buffer, int offset, int bytesToRead,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
int totalRead = 0; int totalRead = 0;
...@@ -155,9 +216,59 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -155,9 +216,59 @@ namespace Titanium.Web.Proxy.Http2
class MyHeaderListener : IHeaderListener class MyHeaderListener : IHeaderListener
{ {
private readonly Action<string, string> addHeaderFunc;
public string Method { get; private set; }
public string Status { get; private set; }
private string authority;
private string scheme;
public string Path { get; private set; }
public MyHeaderListener(Action<string, string> addHeaderFunc)
{
this.addHeaderFunc = addHeaderFunc;
}
public void AddHeader(string name, string value, bool sensitive) public void AddHeader(string name, string value, bool sensitive)
{ {
Console.WriteLine(name + ": " + value + " " + sensitive); if (name[0] == ':')
{
switch (name)
{
case ":method":
Method = value;
return;
case ":authority":
authority = value;
return;
case ":scheme":
scheme = value;
return;
case ":path":
Path = value;
return;
case ":status":
Status = value;
return;
}
}
addHeaderFunc(name, value);
}
public Uri GetUri()
{
if (authority == null)
{
// todo
authority = "abc.abc";
}
return new Uri(scheme + "://" + authority + Path);
} }
} }
} }
......
...@@ -146,10 +146,13 @@ namespace Titanium.Web.Proxy ...@@ -146,10 +146,13 @@ namespace Titanium.Web.Proxy
public bool EnableWinAuth { get; set; } public bool EnableWinAuth { get; set; }
/// <summary> /// <summary>
/// Enable disable HTTP/2 support. This setting is internal, /// Enable disable HTTP/2 support.
/// because the implementation is not finished /// Warning: HTTP/2 support is very limited
/// - only enabled when both client and server supports it (no protocol changing in proxy)
/// - GetRequest/ResponseBody(AsString) methods are not supported
/// - cannot modify the request/response (e.g header modifications in BeforeRequest/Response events are ignored)
/// </summary> /// </summary>
internal bool EnableHttp2 { get; set; } = false; public bool EnableHttp2 { get; set; } = false;
/// <summary> /// <summary>
/// Should we check for certificate revocation during SSL authentication to servers /// Should we check for certificate revocation during SSL authentication to servers
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment