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()))