From b37d7c8ff426f2ef51741638629c5802ac05d702 Mon Sep 17 00:00:00 2001 From: Honfika Date: Thu, 25 Apr 2019 16:20:20 +0200 Subject: [PATCH 1/4] 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) --- .../MainWindow.xaml.cs | 39 ++++ .../SessionListItem.cs | 48 +++-- ...nium.Web.Proxy.Examples.Wpf.NetCore.csproj | 17 ++ .../ExplicitClientHandler.cs | 21 +- src/Titanium.Web.Proxy/Helpers/HttpHelper.cs | 4 +- src/Titanium.Web.Proxy/Http/ConnectRequest.cs | 2 + src/Titanium.Web.Proxy/Http/KnownHeaders.cs | 18 +- src/Titanium.Web.Proxy/Http/TunnelType.cs | 10 + .../Http2/Hpack/StaticTable.cs | 96 ++++----- src/Titanium.Web.Proxy/Http2/Http2Helper.cs | 201 ++++++++++++++---- src/Titanium.Web.Proxy/ProxyServer.cs | 9 +- 11 files changed, 337 insertions(+), 128 deletions(-) create mode 100644 examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf.NetCore.csproj create mode 100644 src/Titanium.Web.Proxy/Http/TunnelType.cs diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs b/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs index 94b592fe6..3999af1e6 100644 --- a/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs @@ -150,6 +150,12 @@ private async Task ProxyServer_BeforeRequest(object sender, SessionEventArgs e) SessionListItem item = null; 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) { e.HttpClient.Request.KeepBody = true; @@ -168,6 +174,12 @@ await Dispatcher.InvokeAsync(() => } }); + if (e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Http2) + { + // GetRequestBody for HTTP/2 currently not supported + return; + } + if (item != null) { if (e.HttpClient.Response.HasBody) @@ -217,6 +229,12 @@ private SessionListItem createSessionListItem(SessionEventArgsBase e) var session = (SessionEventArgsBase)sender; 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; } }; @@ -226,6 +244,12 @@ private SessionListItem createSessionListItem(SessionEventArgsBase e) var session = (SessionEventArgsBase)sender; 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; } }; @@ -235,6 +259,21 @@ private SessionListItem createSessionListItem(SessionEventArgsBase e) 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) { if (e.Key == Key.Delete) diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/SessionListItem.cs b/examples/Titanium.Web.Proxy.Examples.Wpf/SessionListItem.cs index c3f54b0fe..7cfd456b2 100644 --- a/examples/Titanium.Web.Proxy.Examples.Wpf/SessionListItem.cs +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/SessionListItem.cs @@ -11,7 +11,7 @@ public class SessionListItem : INotifyPropertyChanged private long? bodySize; private Exception exception; private string host; - private string process; + private int processId; private string protocol; private long receivedDataCount; private long sentDataCount; @@ -54,10 +54,32 @@ public long? BodySize set => SetField(ref bodySize, value); } + public int ProcessId + { + get => processId; + set + { + if (SetField(ref processId, value)) + { + OnPropertyChanged(nameof(Process)); + } + } + } + public string Process { - get => process; - set => SetField(ref process, value); + get + { + try + { + var process = System.Diagnostics.Process.GetProcessById(processId); + return process.ProcessName + ":" + processId; + } + catch (Exception) + { + return string.Empty; + } + } } public long ReceivedDataCount @@ -80,13 +102,16 @@ public Exception Exception public event PropertyChangedEventHandler PropertyChanged; - protected void SetField(ref T field, T value, [CallerMemberName] string propertyName = null) + protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) { if (!Equals(field, value)) { field = value; OnPropertyChanged(propertyName); + return true; } + + return false; } [NotifyPropertyChangedInvocator] @@ -132,20 +157,7 @@ public void Update() BodySize = responseSize; } - Process = GetProcessDescription(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; - } + ProcessId = HttpClient.ProcessId.Value; } } } diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf.NetCore.csproj b/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf.NetCore.csproj new file mode 100644 index 000000000..5619d9464 --- /dev/null +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf.NetCore.csproj @@ -0,0 +1,17 @@ + + + + WinExe + netcoreapp3.0 + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs index d47ab5e40..a9318e56f 100644 --- a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs @@ -126,6 +126,7 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, bool isClientHello = clientHelloInfo != null; if (isClientHello) { + connectRequest.TunnelType = TunnelType.Https; connectRequest.ClientHelloInfo = clientHelloInfo; } @@ -208,9 +209,9 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, } catch (Exception e) { - var certname = certificate?.GetNameInfo(X509NameType.SimpleName, false); + var certName = certificate?.GetNameInfo(X509NameType.SimpleName, false); 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) @@ -233,6 +234,11 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, // Hostname is excluded or it is not an HTTPS connect if (!decryptSsl || !isClientHello) { + if (!isClientHello) + { + connectRequest.TunnelType = TunnelType.Websocket; + } + // create new connection to server. // If we detected that client tunnel CONNECTs without SSL by checking for empty client hello then // this connection should not be HTTPS. @@ -286,6 +292,8 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, BufferSize, string httpCmd = await clientStream.ReadLineAsync(cancellationToken); if (httpCmd == "PRI * HTTP/2.0") { + connectArgs.HttpClient.ConnectRequest.TunnelType = TunnelType.Http2; + // HTTP/2 Connection Preface string line = await clientStream.ReadLineAsync(cancellationToken); if (line != string.Empty) @@ -318,9 +326,16 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, BufferSize, await Http2Helper.SendHttp2(clientStream, connection.Stream, BufferSize, (buffer, offset, count) => { connectArgs.OnDataSent(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); #endif - } finally { diff --git a/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs b/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs index bfcc6c86a..c13dc0c07 100644 --- a/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs +++ b/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs @@ -146,8 +146,8 @@ internal static Task IsPriMethod(ICustomStreamReader clientStreamReader) private static async Task startsWith(ICustomStreamReader clientStreamReader, string expectedStart) { bool isExpected = true; - int legthToCheck = 10; - for (int i = 0; i < legthToCheck; i++) + int lengthToCheck = 10; + for (int i = 0; i < lengthToCheck; i++) { int b = await clientStreamReader.PeekByteAsync(i); if (b == -1) diff --git a/src/Titanium.Web.Proxy/Http/ConnectRequest.cs b/src/Titanium.Web.Proxy/Http/ConnectRequest.cs index 25496a0bd..9b1400df1 100644 --- a/src/Titanium.Web.Proxy/Http/ConnectRequest.cs +++ b/src/Titanium.Web.Proxy/Http/ConnectRequest.cs @@ -12,6 +12,8 @@ public ConnectRequest() Method = "CONNECT"; } + public TunnelType TunnelType { get; internal set; } + public ClientHelloInfo ClientHelloInfo { get; set; } } } diff --git a/src/Titanium.Web.Proxy/Http/KnownHeaders.cs b/src/Titanium.Web.Proxy/Http/KnownHeaders.cs index 9636a1fdd..79bf37a9b 100644 --- a/src/Titanium.Web.Proxy/Http/KnownHeaders.cs +++ b/src/Titanium.Web.Proxy/Http/KnownHeaders.cs @@ -6,28 +6,28 @@ public static class KnownHeaders { // Both - public const string Connection = "connection"; + public const string Connection = "Connection"; public const string ConnectionClose = "close"; 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 ContentTypeBoundary = "boundary"; - public const string Upgrade = "upgrade"; + public const string Upgrade = "Upgrade"; public const string UpgradeWebsocket = "websocket"; // Request headers - public const string AcceptEncoding = "accept-encoding"; + public const string AcceptEncoding = "Accept-Encoding"; public const string Authorization = "Authorization"; - public const string Expect = "expect"; + public const string Expect = "Expect"; 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 ProxyAuthorizationBasic = "basic"; @@ -36,7 +36,7 @@ public static class KnownHeaders public const string ProxyConnectionClose = "close"; // Response headers - public const string ContentEncoding = "content-encoding"; + public const string ContentEncoding = "Content-Encoding"; public const string ContentEncodingDeflate = "deflate"; public const string ContentEncodingGzip = "gzip"; public const string ContentEncodingBrotli = "br"; @@ -45,7 +45,7 @@ public static class KnownHeaders public const string ProxyAuthenticate = "Proxy-Authenticate"; - public const string TransferEncoding = "transfer-encoding"; + public const string TransferEncoding = "Transfer-Encoding"; public const string TransferEncodingChunked = "chunked"; } } diff --git a/src/Titanium.Web.Proxy/Http/TunnelType.cs b/src/Titanium.Web.Proxy/Http/TunnelType.cs new file mode 100644 index 000000000..d907914b0 --- /dev/null +++ b/src/Titanium.Web.Proxy/Http/TunnelType.cs @@ -0,0 +1,10 @@ +namespace Titanium.Web.Proxy.Http +{ + public enum TunnelType + { + Unknown, + Https, + Websocket, + Http2, + } +} diff --git a/src/Titanium.Web.Proxy/Http2/Hpack/StaticTable.cs b/src/Titanium.Web.Proxy/Http2/Hpack/StaticTable.cs index 3d3f6c78f..771574f59 100644 --- a/src/Titanium.Web.Proxy/Http2/Hpack/StaticTable.cs +++ b/src/Titanium.Web.Proxy/Http2/Hpack/StaticTable.cs @@ -59,99 +59,99 @@ public static class StaticTable /* 14 */ new HttpHeader(":status", "500"), /* 15 */ - new HttpHeader("accept-charset", string.Empty), + new HttpHeader("Accept-Charset", string.Empty), /* 16 */ - new HttpHeader("accept-encoding", "gzip, deflate"), + new HttpHeader("Accept-Encoding", "gzip, deflate"), /* 17 */ - new HttpHeader("accept-language", string.Empty), + new HttpHeader("Accept-Language", string.Empty), /* 18 */ - new HttpHeader("accept-ranges", string.Empty), + new HttpHeader("Accept-Ranges", string.Empty), /* 19 */ - new HttpHeader("accept", string.Empty), + new HttpHeader("Accept", string.Empty), /* 20 */ - new HttpHeader("access-control-allow-origin", string.Empty), + new HttpHeader("Access-Control-Allow-Origin", string.Empty), /* 21 */ - new HttpHeader("age", string.Empty), + new HttpHeader("Age", string.Empty), /* 22 */ - new HttpHeader("allow", string.Empty), + new HttpHeader("Allow", string.Empty), /* 23 */ - new HttpHeader("authorization", string.Empty), + new HttpHeader("Authorization", string.Empty), /* 24 */ - new HttpHeader("cache-control", string.Empty), + new HttpHeader("Cache-Control", string.Empty), /* 25 */ - new HttpHeader("content-disposition", string.Empty), + new HttpHeader("Content-Disposition", string.Empty), /* 26 */ - new HttpHeader("content-encoding", string.Empty), + new HttpHeader("Content-Encoding", string.Empty), /* 27 */ - new HttpHeader("content-language", string.Empty), + new HttpHeader("Content-Language", string.Empty), /* 28 */ - new HttpHeader("content-length", string.Empty), + new HttpHeader("Content-Length", string.Empty), /* 29 */ - new HttpHeader("content-location", string.Empty), + new HttpHeader("Content-Location", string.Empty), /* 30 */ - new HttpHeader("content-range", string.Empty), + new HttpHeader("Content-Range", string.Empty), /* 31 */ - new HttpHeader("content-type", string.Empty), + new HttpHeader("Content-Type", string.Empty), /* 32 */ - new HttpHeader("cookie", string.Empty), + new HttpHeader("Cookie", string.Empty), /* 33 */ - new HttpHeader("date", string.Empty), + new HttpHeader("Date", string.Empty), /* 34 */ - new HttpHeader("etag", string.Empty), + new HttpHeader("ETag", string.Empty), /* 35 */ - new HttpHeader("expect", string.Empty), + new HttpHeader("Expect", string.Empty), /* 36 */ - new HttpHeader("expires", string.Empty), + new HttpHeader("Expires", string.Empty), /* 37 */ - new HttpHeader("from", string.Empty), + new HttpHeader("From", string.Empty), /* 38 */ - new HttpHeader("host", string.Empty), + new HttpHeader("Host", string.Empty), /* 39 */ - new HttpHeader("if-match", string.Empty), + new HttpHeader("If-Match", string.Empty), /* 40 */ - new HttpHeader("if-modified-since", string.Empty), + new HttpHeader("If-Modified-Since", string.Empty), /* 41 */ - new HttpHeader("if-none-match", string.Empty), + new HttpHeader("If-None-Match", string.Empty), /* 42 */ - new HttpHeader("if-range", string.Empty), + new HttpHeader("If-Range", string.Empty), /* 43 */ - new HttpHeader("if-unmodified-since", string.Empty), + new HttpHeader("If-Unmodified-Since", string.Empty), /* 44 */ - new HttpHeader("last-modified", string.Empty), + new HttpHeader("Last-Modified", string.Empty), /* 45 */ - new HttpHeader("link", string.Empty), + new HttpHeader("Link", string.Empty), /* 46 */ - new HttpHeader("location", string.Empty), + new HttpHeader("Location", string.Empty), /* 47 */ - new HttpHeader("max-forwards", string.Empty), + new HttpHeader("Max-Forwards", string.Empty), /* 48 */ - new HttpHeader("proxy-authenticate", string.Empty), + new HttpHeader("Proxy-Authenticate", string.Empty), /* 49 */ - new HttpHeader("proxy-authorization", string.Empty), + new HttpHeader("Proxy-Authorization", string.Empty), /* 50 */ - new HttpHeader("range", string.Empty), + new HttpHeader("Range", string.Empty), /* 51 */ - new HttpHeader("referer", string.Empty), + new HttpHeader("Referer", string.Empty), /* 52 */ - new HttpHeader("refresh", string.Empty), + new HttpHeader("Refresh", string.Empty), /* 53 */ - new HttpHeader("retry-after", string.Empty), + new HttpHeader("Retry-After", string.Empty), /* 54 */ - new HttpHeader("server", string.Empty), + new HttpHeader("Server", string.Empty), /* 55 */ - new HttpHeader("set-cookie", string.Empty), + new HttpHeader("Set-Cookie", string.Empty), /* 56 */ - new HttpHeader("strict-transport-security", string.Empty), + new HttpHeader("Strict-Transport-Security", string.Empty), /* 57 */ - new HttpHeader("transfer-encoding", string.Empty), + new HttpHeader("Transfer-Encoding", string.Empty), /* 58 */ - new HttpHeader("user-agent", string.Empty), + new HttpHeader("User-Agent", string.Empty), /* 59 */ - new HttpHeader("vary", string.Empty), + new HttpHeader("Vary", string.Empty), /* 60 */ - new HttpHeader("via", string.Empty), + new HttpHeader("Via", string.Empty), /* 61 */ - new HttpHeader("www-authenticate", string.Empty) + new HttpHeader("WWW-Authenticate", string.Empty) }; private static readonly Dictionary staticIndexByName = CreateMap(); @@ -244,4 +244,4 @@ private static Dictionary CreateMap() return ret; } } -} \ No newline at end of file +} diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs index a5c6af8f2..f86c1f1a1 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs @@ -1,8 +1,14 @@ #if NETCOREAPP2_1 using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; +using System.Net; using System.Threading; 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; namespace Titanium.Web.Proxy.Http2 @@ -25,27 +31,24 @@ internal class Http2Helper /// Useful for websocket requests /// Task-based Asynchronous Pattern /// - /// - /// - /// - /// - /// - /// - /// - /// /// internal static async Task SendHttp2(Stream clientStream, Stream serverStream, int bufferSize, Action onDataSend, Action onDataReceive, + Func sessionFactory, + Func onBeforeRequest, Func onBeforeResponse, CancellationTokenSource cancellationTokenSource, Guid connectionId, ExceptionHandler exceptionFunc) { + var decoder = new Decoder(8192, 4096 * 16); + var sessions = new ConcurrentDictionary(); + // Now async relay all server=>client & client=>server data var sendRelay = - CopyHttp2FrameAsync(clientStream, serverStream, onDataSend, bufferSize, connectionId, - true, cancellationTokenSource.Token); + copyHttp2FrameAsync(clientStream, serverStream, onDataSend, sessionFactory, decoder, sessions, onBeforeRequest, + bufferSize, connectionId, true, cancellationTokenSource.Token, exceptionFunc); var receiveRelay = - CopyHttp2FrameAsync(serverStream, clientStream, onDataReceive, bufferSize, connectionId, - false, cancellationTokenSource.Token); + copyHttp2FrameAsync(serverStream, clientStream, onDataReceive, sessionFactory, decoder, sessions, onBeforeResponse, + bufferSize, connectionId, false, cancellationTokenSource.Token, exceptionFunc); await Task.WhenAny(sendRelay, receiveRelay); cancellationTokenSource.Cancel(); @@ -53,16 +56,17 @@ internal static async Task SendHttp2(Stream clientStream, Stream serverStream, i await Task.WhenAll(sendRelay, receiveRelay); } - private static async Task CopyHttp2FrameAsync(Stream input, Stream output, Action onCopy, - int bufferSize, Guid connectionId, bool isClient, CancellationToken cancellationToken) + private static async Task copyHttp2FrameAsync(Stream input, Stream output, Action onCopy, + Func sessionFactory, Decoder decoder, ConcurrentDictionary sessions, + Func onBeforeRequestResponse, + int bufferSize, Guid connectionId, bool isClient, CancellationToken cancellationToken, + ExceptionHandler exceptionFunc) { - var decoder = new Decoder(8192, 4096); - var headerBuffer = new byte[9]; var buffer = new byte[32768]; while (true) { - int read = await ForceRead(input, headerBuffer, 0, 9, cancellationToken); + int read = await forceRead(input, headerBuffer, 0, 9, cancellationToken); onCopy(headerBuffer, 0, read); if (read != 9) { @@ -75,55 +79,112 @@ private static async Task CopyHttp2FrameAsync(Stream input, Stream output, Actio int streamId = ((headerBuffer[5] & 0x7f) << 24) + (headerBuffer[6] << 16) + (headerBuffer[7] << 8) + headerBuffer[8]; - read = await ForceRead(input, buffer, 0, length, cancellationToken); + read = await forceRead(input, buffer, 0, length, cancellationToken); onCopy(buffer, 0, read); if (read != length) { 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) + { + endStream = true; + } + } + else if (type == 1 /*headers*/) { - if (type == 1 /*headers*/) + bool endHeaders = (flags & (int)Http2FrameFlag.EndHeaders) != 0; + bool padded = (flags & (int)Http2FrameFlag.Padded) != 0; + bool priority = (flags & (int)Http2FrameFlag.Priority) != 0; + bool endStreamFlag = (flags & (int)Http2FrameFlag.EndStream) != 0; + if (endStreamFlag) { - bool endHeaders = (flags & (int)Http2FrameFlag.EndHeaders) != 0; - bool padded = (flags & (int)Http2FrameFlag.Padded) != 0; - bool priority = (flags & (int)Http2FrameFlag.Priority) != 0; + endStream = true; + } - System.Diagnostics.Debug.WriteLine("HEADER: " + streamId + " end: " + endHeaders); + int offset = 0; + if (padded) + { + offset = 1; + } + + if (priority) + { + offset += 5; + } - int offset = 0; - if (padded) - { - offset = 1; - } + int dataLength = length - offset; + if (padded) + { + dataLength -= buffer[0]; + } - if (priority) - { - offset += 5; - } + 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); + } - int dataLength = length - offset; - if (padded) + var headerListener = new MyHeaderListener( + (name, value) => { - dataLength -= buffer[0]; - } - - var headerListener = new MyHeaderListener(); - try + var headers = isClient ? args.HttpClient.Request.Headers : args.HttpClient.Response.Headers; + headers.AddHeader(name, value); + }); + try + { + lock (decoder) { decoder.Decode(new BinaryReader(new MemoryStream(buffer, offset, dataLength)), headerListener); 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); + } + } + + if (!isClient && endStream) + { + sessions.TryRemove(streamId, out _); } - await output.WriteAsync(headerBuffer, 0, headerBuffer.Length, cancellationToken); - await output.WriteAsync(buffer, 0, length, cancellationToken); + // 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)) { @@ -133,7 +194,7 @@ private static async Task CopyHttp2FrameAsync(Stream input, Stream output, Actio } } - private static async Task ForceRead(Stream input, byte[] buffer, int offset, int bytesToRead, + private static async Task forceRead(Stream input, byte[] buffer, int offset, int bytesToRead, CancellationToken cancellationToken) { int totalRead = 0; @@ -155,9 +216,59 @@ private static async Task ForceRead(Stream input, byte[] buffer, int offset class MyHeaderListener : IHeaderListener { + private readonly Action 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 addHeaderFunc) + { + this.addHeaderFunc = addHeaderFunc; + } + 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); } } } diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index d8de74d8f..f20fc8ce3 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -146,10 +146,13 @@ public ProxyServer(string rootCertificateName, string rootCertificateIssuerName, public bool EnableWinAuth { get; set; } /// - /// Enable disable HTTP/2 support. This setting is internal, - /// because the implementation is not finished + /// Enable disable HTTP/2 support. + /// 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) /// - internal bool EnableHttp2 { get; set; } = false; + public bool EnableHttp2 { get; set; } = false; /// /// Should we check for certificate revocation during SSL authentication to servers From b87e57c2270bc0809cf6c650df9d203512854bb0 Mon Sep 17 00:00:00 2001 From: Honfika Date: Thu, 25 Apr 2019 16:37:01 +0200 Subject: [PATCH 2/4] this is also a websocket --- src/Titanium.Web.Proxy/RequestHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Titanium.Web.Proxy/RequestHandler.cs b/src/Titanium.Web.Proxy/RequestHandler.cs index e76657ade..785639f28 100644 --- a/src/Titanium.Web.Proxy/RequestHandler.cs +++ b/src/Titanium.Web.Proxy/RequestHandler.cs @@ -312,6 +312,8 @@ private async Task handleHttpSessionRequest(string httpCmd, Session if (args.HttpClient.Request.UpgradeToWebSocket) { + args.HttpClient.ConnectRequest.TunnelType = TunnelType.Websocket; + // if upgrading to websocket then relay the request without reading the contents await handleWebSocketUpgrade(httpCmd, args, args.HttpClient.Request, args.HttpClient.Response, args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamWriter, From 4b8b59c7375ac0a473790624ad961731cc1f41e2 Mon Sep 17 00:00:00 2001 From: Honfika Date: Thu, 25 Apr 2019 16:57:23 +0200 Subject: [PATCH 3/4] make some properties get only --- src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs | 2 +- src/Titanium.Web.Proxy/Models/HttpHeader.cs | 2 +- src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs index 0359c5eaf..f7909cf13 100644 --- a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs +++ b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs @@ -31,7 +31,7 @@ public abstract class SessionEventArgsBase : EventArgs, IDisposable /// /// Relative milliseconds for various events. /// - public Dictionary TimeLine { get; set; } = new Dictionary(); + public Dictionary TimeLine { get; } = new Dictionary(); /// /// Initializes a new instance of the class. diff --git a/src/Titanium.Web.Proxy/Models/HttpHeader.cs b/src/Titanium.Web.Proxy/Models/HttpHeader.cs index d9612b55c..030008c60 100644 --- a/src/Titanium.Web.Proxy/Models/HttpHeader.cs +++ b/src/Titanium.Web.Proxy/Models/HttpHeader.cs @@ -45,7 +45,7 @@ public HttpHeader(string name, string value) /// /// Header Name. /// - public string Name { get; set; } + public string Name { get; } /// /// Header Value. diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs index cea5e99f6..c6b20dc53 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs @@ -42,7 +42,7 @@ internal TcpConnectionFactory(ProxyServer server) Task.Run(async () => await clearOutdatedConnections()); } - internal ProxyServer Server { get; set; } + internal ProxyServer Server { get; } internal string GetConnectionCacheKey(string remoteHostName, int remotePort, bool isHttps, List applicationProtocols, From 2d4dc4e020d605539660a4196ee0ef8c62c3988c Mon Sep 17 00:00:00 2001 From: Honfika Date: Thu, 25 Apr 2019 16:58:51 +0200 Subject: [PATCH 4/4] Ref class was added by me earlier. I don't know why, but it is not needed anymore, so I remove it. --- src/Titanium.Web.Proxy/Helpers/Ref.cs | 32 --------------------------- 1 file changed, 32 deletions(-) delete mode 100644 src/Titanium.Web.Proxy/Helpers/Ref.cs diff --git a/src/Titanium.Web.Proxy/Helpers/Ref.cs b/src/Titanium.Web.Proxy/Helpers/Ref.cs deleted file mode 100644 index 0bd3e6ac9..000000000 --- a/src/Titanium.Web.Proxy/Helpers/Ref.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Titanium.Web.Proxy.Helpers -{ - internal class Ref - { - internal Ref() - { - } - - internal Ref(T value) - { - Value = value; - } - - internal T Value { get; set; } - - public override string ToString() - { - var value = Value; - return value == null ? string.Empty : value.ToString(); - } - - public static implicit operator T(Ref r) - { - return r.Value; - } - - public static implicit operator Ref(T value) - { - return new Ref(value); - } - } -}