diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml b/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml index c13acfd45..1739dedcf 100644 --- a/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml @@ -36,13 +36,20 @@ - + - + + + + + + + + diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs b/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs index 51b1bcbe0..0e2cb80e9 100644 --- a/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Net; using System.Text; @@ -8,6 +9,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; +using System.Windows.Media.Imaging; using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Models; @@ -99,7 +101,7 @@ public MainWindow() InitializeComponent(); } - public ObservableCollection Sessions { get; } = new ObservableCollection(); + public ObservableCollectionEx Sessions { get; } = new ObservableCollectionEx(); public SessionListItem SelectedSession { @@ -278,11 +280,14 @@ private void ListViewSessions_OnKeyDown(object sender, KeyEventArgs e) if (e.Key == Key.Delete) { var selectedItems = ((ListView)sender).SelectedItems; + Sessions.SuppressNotification = true; foreach (var item in selectedItems.Cast().ToArray()) { Sessions.Remove(item); sessionDictionary.Remove(item.HttpClient); } + + Sessions.SuppressNotification = false; } } @@ -297,7 +302,8 @@ private void selectedSessionChanged() var session = SelectedSession.HttpClient; var request = session.Request; - var data = (request.IsBodyRead ? request.Body : null) ?? new byte[0]; + var fullData = (request.IsBodyRead ? request.Body : null) ?? new byte[0]; + var data = fullData; bool truncated = data.Length > truncateLimit; if (truncated) { @@ -313,7 +319,8 @@ private void selectedSessionChanged() TextBoxRequest.Text = sb.ToString(); var response = session.Response; - data = (response.IsBodyRead ? response.Body : null) ?? new byte[0]; + fullData = (response.IsBodyRead ? response.Body : null) ?? new byte[0]; + data = fullData; truncated = data.Length > truncateLimit; if (truncated) { @@ -333,6 +340,19 @@ private void selectedSessionChanged() } TextBoxResponse.Text = sb.ToString(); + + try + { + using (MemoryStream stream = new MemoryStream(fullData)) + { + ImageResponse.Source = + BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + } + } + catch + { + ImageResponse.Source = null; + } } } } diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/ObservableCollectionEx.cs b/examples/Titanium.Web.Proxy.Examples.Wpf/ObservableCollectionEx.cs new file mode 100644 index 000000000..e93175e59 --- /dev/null +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/ObservableCollectionEx.cs @@ -0,0 +1,36 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Titanium.Web.Proxy.Examples.Wpf +{ + public class ObservableCollectionEx : ObservableCollection + { + private bool notificationSuppressed; + private bool suppressNotification; + + public bool SuppressNotification + { + get => suppressNotification; + set + { + suppressNotification = value; + if (suppressNotification == false && notificationSuppressed) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + notificationSuppressed = false; + } + } + } + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (SuppressNotification) + { + notificationSuppressed = true; + return; + } + + base.OnCollectionChanged(e); + } + } +} \ No newline at end of file diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf.csproj b/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf.csproj index 96838a076..f9d14d6ce 100644 --- a/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf.csproj +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/Titanium.Web.Proxy.Examples.Wpf.csproj @@ -92,6 +92,7 @@ MSBuild:Compile Designer + diff --git a/src/StreamExtended/Network/CustomBufferedStream.cs b/src/StreamExtended/Network/CustomBufferedStream.cs index 10e34490b..6ef058a03 100644 --- a/src/StreamExtended/Network/CustomBufferedStream.cs +++ b/src/StreamExtended/Network/CustomBufferedStream.cs @@ -245,8 +245,7 @@ public override int ReadByte() { return -1; } - - + return streamBuffer[bufferPos + index]; } @@ -491,7 +490,7 @@ public bool FillBuffer() if (bufferLength > 0) { //normally we fill the buffer only when it is empty, but sometimes we need more data - //move the remanining data to the beginning of the buffer + //move the remaining data to the beginning of the buffer Buffer.BlockCopy(streamBuffer, bufferPos, streamBuffer, 0, bufferLength); } @@ -516,7 +515,6 @@ public bool FillBuffer() } return result; - } /// diff --git a/src/Titanium.Web.Proxy/EventArguments/LimitedStream.cs b/src/Titanium.Web.Proxy/EventArguments/LimitedStream.cs index 175b0974b..cd4d7ae9b 100644 --- a/src/Titanium.Web.Proxy/EventArguments/LimitedStream.cs +++ b/src/Titanium.Web.Proxy/EventArguments/LimitedStream.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using StreamExtended; using StreamExtended.Network; +using Titanium.Web.Proxy.Exceptions; namespace Titanium.Web.Proxy.EventArguments { @@ -60,7 +61,11 @@ private void getNextChunk() chunkHead = chunkHead.Substring(0, idx); } - int chunkSize = int.Parse(chunkHead, NumberStyles.HexNumber); + if (!int.TryParse(chunkHead, NumberStyles.HexNumber, null, out int chunkSize)) + { + throw new ProxyHttpException($"Invalid chunk length: '{chunkHead}'", null, null); + } + bytesRemaining = chunkSize; if (chunkSize == 0) diff --git a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs index a9318e56f..3cbab765b 100644 --- a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs @@ -47,8 +47,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect { string connectHostname = null; TunnelConnectSessionEventArgs connectArgs = null; - - + // Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) if (await HttpHelper.IsConnectMethod(clientStream) == 1) { @@ -142,15 +141,22 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2)) { // test server HTTP/2 support - // todo: this is a hack, because Titanium does not support HTTP protocol changing currently - var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, - isConnect: true, applicationProtocols: SslExtensions.Http2ProtocolAsList, - noCache: true, cancellationToken: cancellationToken); - - http2Supported = connection.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2; - - //release connection back to pool instead of closing when connection pool is enabled. - await tcpConnectionFactory.Release(connection, true); + try + { + // todo: this is a hack, because Titanium does not support HTTP protocol changing currently + var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, + isConnect: true, applicationProtocols: SslExtensions.Http2ProtocolAsList, + noCache: true, cancellationToken: cancellationToken); + + http2Supported = connection.NegotiatedApplicationProtocol == + SslApplicationProtocol.Http2; + //release connection back to pool instead of closing when connection pool is enabled. + await tcpConnectionFactory.Release(connection, true); + } + catch (Exception) + { + // ignore + } } if (EnableTcpServerConnectionPrefetch) diff --git a/src/Titanium.Web.Proxy/Helpers/HttpRequestWriter.cs b/src/Titanium.Web.Proxy/Helpers/HttpRequestWriter.cs index f9e5b80ab..5883761bc 100644 --- a/src/Titanium.Web.Proxy/Helpers/HttpRequestWriter.cs +++ b/src/Titanium.Web.Proxy/Helpers/HttpRequestWriter.cs @@ -23,7 +23,7 @@ internal HttpRequestWriter(Stream stream, IBufferPool bufferPool, int bufferSize internal async Task WriteRequestAsync(Request request, bool flush = true, CancellationToken cancellationToken = default) { - await WriteLineAsync(Request.CreateRequestLine(request.Method, request.OriginalUrl, request.HttpVersion), + await WriteLineAsync(Request.CreateRequestLine(request.Method, request.RequestUriString, request.HttpVersion), cancellationToken); await WriteAsync(request, flush, cancellationToken); } diff --git a/src/Titanium.Web.Proxy/Http/HttpWebClient.cs b/src/Titanium.Web.Proxy/Http/HttpWebClient.cs index 129380b9d..c22c9f218 100644 --- a/src/Titanium.Web.Proxy/Http/HttpWebClient.cs +++ b/src/Titanium.Web.Proxy/Http/HttpWebClient.cs @@ -99,7 +99,7 @@ internal async Task SendRequest(bool enable100ContinueBehaviour, bool isTranspar // prepare the request & headers await writer.WriteLineAsync(Request.CreateRequestLine(Request.Method, - useUpstreamProxy || isTransparent ? Request.OriginalUrl : Request.RequestUri.PathAndQuery, + useUpstreamProxy || isTransparent ? Request.RequestUriString : Request.RequestUri.PathAndQuery, Request.HttpVersion), cancellationToken); var headerBuilder = new StringBuilder(); diff --git a/src/Titanium.Web.Proxy/Http/Request.cs b/src/Titanium.Web.Proxy/Http/Request.cs index e5965b532..6a80ede9d 100644 --- a/src/Titanium.Web.Proxy/Http/Request.cs +++ b/src/Titanium.Web.Proxy/Http/Request.cs @@ -14,6 +14,8 @@ namespace Titanium.Web.Proxy.Http [TypeConverter(typeof(ExpandableObjectConverter))] public class Request : RequestResponseBase { + private string originalUrl; + /// /// Request Method. /// @@ -32,7 +34,20 @@ public class Request : RequestResponseBase /// /// The original request Url. /// - public string OriginalUrl { get; set; } + public string OriginalUrl + { + get => originalUrl; + internal set + { + originalUrl = value; + RequestUriString = value; + } + } + + /// + /// The request uri as it is in the HTTP header + /// + public string RequestUriString { get; set; } /// /// Has request body? @@ -140,7 +155,7 @@ public override string HeaderText get { var sb = new StringBuilder(); - sb.Append($"{CreateRequestLine(Method, OriginalUrl, HttpVersion)}{ProxyConstants.NewLine}"); + sb.Append($"{CreateRequestLine(Method, RequestUriString, HttpVersion)}{ProxyConstants.NewLine}"); foreach (var header in Headers) { sb.Append($"{header.ToString()}{ProxyConstants.NewLine}"); diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs index e327fbe92..df31c6e08 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs @@ -39,14 +39,19 @@ internal static async Task SendHttp2(Stream clientStream, Stream serverStream, i CancellationTokenSource cancellationTokenSource, Guid connectionId, ExceptionHandler exceptionFunc) { + var clientSettings = new Http2Settings(); + var serverSettings = new Http2Settings(); + var sessions = new ConcurrentDictionary(); // Now async relay all server=>client & client=>server data var sendRelay = - copyHttp2FrameAsync(clientStream, serverStream, onDataSend, sessionFactory, sessions, onBeforeRequest, + copyHttp2FrameAsync(clientStream, serverStream, onDataSend, clientSettings, serverSettings, + sessionFactory, sessions, onBeforeRequest, bufferSize, connectionId, true, cancellationTokenSource.Token, exceptionFunc); var receiveRelay = - copyHttp2FrameAsync(serverStream, clientStream, onDataReceive, sessionFactory, sessions, onBeforeResponse, + copyHttp2FrameAsync(serverStream, clientStream, onDataReceive, serverSettings, clientSettings, + sessionFactory, sessions, onBeforeResponse, bufferSize, connectionId, false, cancellationTokenSource.Token, exceptionFunc); await Task.WhenAny(sendRelay, receiveRelay); @@ -56,15 +61,17 @@ internal static async Task SendHttp2(Stream clientStream, Stream serverStream, i } private static async Task copyHttp2FrameAsync(Stream input, Stream output, Action onCopy, + Http2Settings localSettings, Http2Settings remoteSettings, Func sessionFactory, ConcurrentDictionary sessions, Func onBeforeRequestResponse, int bufferSize, Guid connectionId, bool isClient, CancellationToken cancellationToken, ExceptionHandler exceptionFunc) { - var decoder = new Decoder(8192, 4096 * 16); + int headerTableSize = 0; + Decoder decoder = null; var headerBuffer = new byte[9]; - var buffer = new byte[32768]; + byte[] buffer = null; while (true) { int read = await forceRead(input, headerBuffer, 0, 9, cancellationToken); @@ -80,6 +87,11 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio int streamId = ((headerBuffer[5] & 0x7f) << 24) + (headerBuffer[6] << 16) + (headerBuffer[7] << 8) + headerBuffer[8]; + if (buffer == null || buffer.Length < localSettings.MaxFrameSize) + { + buffer = new byte[localSettings.MaxFrameSize]; + } + read = await forceRead(input, buffer, 0, length, cancellationToken); onCopy(buffer, 0, read); if (read != length) @@ -145,7 +157,7 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio } } } - else if (type == 1 /*headers*/) + else if (type == 1 /* headers */) { bool endHeaders = (flags & (int)Http2FrameFlag.EndHeaders) != 0; bool padded = (flags & (int)Http2FrameFlag.Padded) != 0; @@ -181,13 +193,18 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio }); try { - lock (decoder) + // recreate the decoder when new value is bigger + // should we recreate when smaller, too? + if (decoder == null || headerTableSize < localSettings.HeaderTableSize) { - decoder.Decode(new BinaryReader(new MemoryStream(buffer, offset, dataLength)), - headerListener); - decoder.EndHeaderBlock(); + headerTableSize = localSettings.HeaderTableSize; + decoder = new Decoder(8192, headerTableSize); } + decoder.Decode(new BinaryReader(new MemoryStream(buffer, offset, dataLength)), + headerListener); + decoder.EndHeaderBlock(); + if (isClient) { var request = args.HttpClient.Request; @@ -225,6 +242,53 @@ private static async Task copyHttp2FrameAsync(Stream input, Stream output, Actio rr.Locked = true; } } + else if (type == 4 /* settings */) + { + if (length % 6 != 0) + { + // https://httpwg.org/specs/rfc7540.html#SETTINGS + // 6.5. SETTINGS + // A SETTINGS frame with a length other than a multiple of 6 octets MUST be treated as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR + throw new ProxyHttpException("Invalid settings length", null, null); + } + + int pos = 0; + while (pos < length) + { + int identifier = (buffer[pos++] << 8) + buffer[pos++]; + int value = (buffer[pos++] << 24) + (buffer[pos++] << 16) + (buffer[pos++] << 8) + buffer[pos++]; + if (identifier == 1 /*SETTINGS_HEADER_TABLE_SIZE*/) + { + //System.Diagnostics.Debug.WriteLine("HEADER SIZE CONN: " + connectionId + ", CLIENT: " + isClient + ", value: " + value); + remoteSettings.HeaderTableSize = value; + } + else if (identifier == 5 /*SETTINGS_MAX_FRAME_SIZE*/) + { + remoteSettings.MaxFrameSize = value; + } + } + } + + if (type == 3 /* rst_stream */) + { + int errorCode = (buffer[0] << 24) + (buffer[1] << 16) + (buffer[2] << 8) + buffer[3]; + if (streamId == 0) + { + // connection error + exceptionFunc(new ProxyHttpException("HTTP/2 connection error. Error code: " + errorCode, null, args)); + return; + } + else + { + // stream error + sessions.TryRemove(streamId, out _); + + if (errorCode != 8 /*cancel*/) + { + exceptionFunc(new ProxyHttpException("HTTP/2 stream error. Error code: " + errorCode, null, args)); + } + } + } if (!isClient && endStream) { @@ -269,6 +333,14 @@ private static async Task forceRead(Stream input, byte[] buffer, int offset return totalRead; } + + class Http2Settings + { + public int HeaderTableSize { get; set; } = 4096; + + public int MaxFrameSize { get; set; } = 16384; + } + class MyHeaderListener : IHeaderListener { private readonly Action addHeaderFunc; diff --git a/src/Titanium.Web.Proxy/ResponseHandler.cs b/src/Titanium.Web.Proxy/ResponseHandler.cs index ba9110f3c..f727f01a4 100644 --- a/src/Titanium.Web.Proxy/ResponseHandler.cs +++ b/src/Titanium.Web.Proxy/ResponseHandler.cs @@ -92,7 +92,7 @@ private async Task handleHttpSessionResponse(SessionEventArgs args) // clear current response await args.ClearResponse(cancellationToken); var httpCmd = Request.CreateRequestLine(args.HttpClient.Request.Method, - args.HttpClient.Request.OriginalUrl, args.HttpClient.Request.HttpVersion); + args.HttpClient.Request.RequestUriString, args.HttpClient.Request.HttpVersion); await handleHttpSessionRequest(httpCmd, args, null, args.ClientConnection.NegotiatedApplicationProtocol, cancellationToken, args.CancellationTokenSource); return; diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/Helpers/HttpContinueClient.cs b/tests/Titanium.Web.Proxy.IntegrationTests/Helpers/HttpContinueClient.cs index cbcf7a3b6..701cccca2 100644 --- a/tests/Titanium.Web.Proxy.IntegrationTests/Helpers/HttpContinueClient.cs +++ b/tests/Titanium.Web.Proxy.IntegrationTests/Helpers/HttpContinueClient.cs @@ -20,7 +20,7 @@ public async Task Post(string server, int port, string content) var request = new Request { Method = "POST", - OriginalUrl = "/", + RequestUriString = "/", HttpVersion = new Version(1, 1) }; request.Headers.AddHeader(KnownHeaders.Host, server); diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/Helpers/HttpMessageParsing.cs b/tests/Titanium.Web.Proxy.IntegrationTests/Helpers/HttpMessageParsing.cs index e4cc05c53..9733568fc 100644 --- a/tests/Titanium.Web.Proxy.IntegrationTests/Helpers/HttpMessageParsing.cs +++ b/tests/Titanium.Web.Proxy.IntegrationTests/Helpers/HttpMessageParsing.cs @@ -26,7 +26,7 @@ internal static Request ParseRequest(string messageText, bool requireBody) RequestResponseBase request = new Request() { Method = method, - OriginalUrl = url, + RequestUriString = url, HttpVersion = version }; while (!string.IsNullOrEmpty(line = reader.ReadLine()))