Unverified Commit a31938f4 authored by honfika's avatar honfika Committed by GitHub

Merge pull request #585 from honfika/master

Small HTTP/2 and other improvements
parents 66c41133 9daed0e5
......@@ -36,13 +36,20 @@
</ListView>
<TabControl Grid.Column="2" Grid.Row="0">
<TabItem Header="Session">
<Grid Background="Red" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBox x:Name="TextBoxRequest" Grid.Row="0" />
<TextBox x:Name="TextBoxResponse" Grid.Row="1" />
<TabControl Grid.Row="1">
<TabItem Header="Text">
<TextBox x:Name="TextBoxResponse" />
</TabItem>
<TabItem Header="Image">
<Image x:Name="ImageResponse" HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="None" />
</TabItem>
</TabControl>
</Grid>
</TabItem>
</TabControl>
......
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.Threading.Tasks;
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 @@ namespace Titanium.Web.Proxy.Examples.Wpf
InitializeComponent();
}
public ObservableCollection<SessionListItem> Sessions { get; } = new ObservableCollection<SessionListItem>();
public ObservableCollectionEx<SessionListItem> Sessions { get; } = new ObservableCollectionEx<SessionListItem>();
public SessionListItem SelectedSession
{
......@@ -278,11 +280,14 @@ namespace Titanium.Web.Proxy.Examples.Wpf
if (e.Key == Key.Delete)
{
var selectedItems = ((ListView)sender).SelectedItems;
Sessions.SuppressNotification = true;
foreach (var item in selectedItems.Cast<SessionListItem>().ToArray())
{
Sessions.Remove(item);
sessionDictionary.Remove(item.HttpClient);
}
Sessions.SuppressNotification = false;
}
}
......@@ -297,7 +302,8 @@ namespace Titanium.Web.Proxy.Examples.Wpf
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 @@ namespace Titanium.Web.Proxy.Examples.Wpf
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 @@ namespace Titanium.Web.Proxy.Examples.Wpf
}
TextBoxResponse.Text = sb.ToString();
try
{
using (MemoryStream stream = new MemoryStream(fullData))
{
ImageResponse.Source =
BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
}
catch
{
ImageResponse.Source = null;
}
}
}
}
using System.Collections.ObjectModel;
using System.Collections.Specialized;
namespace Titanium.Web.Proxy.Examples.Wpf
{
public class ObservableCollectionEx<T> : ObservableCollection<T>
{
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
......@@ -92,6 +92,7 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="ObservableCollectionEx.cs" />
<Compile Include="Properties\Annotations.cs" />
<Compile Include="SessionListItem.cs" />
<Page Include="MainWindow.xaml">
......
......@@ -245,8 +245,7 @@ namespace StreamExtended.Network
{
return -1;
}
return streamBuffer[bufferPos + index];
}
......@@ -491,7 +490,7 @@ namespace StreamExtended.Network
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 @@ namespace StreamExtended.Network
}
return result;
}
/// <summary>
......
......@@ -4,6 +4,7 @@ using System.IO;
using System.Threading.Tasks;
using StreamExtended;
using StreamExtended.Network;
using Titanium.Web.Proxy.Exceptions;
namespace Titanium.Web.Proxy.EventArguments
{
......@@ -60,7 +61,11 @@ namespace Titanium.Web.Proxy.EventArguments
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)
......
......@@ -47,8 +47,7 @@ namespace Titanium.Web.Proxy
{
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 @@ namespace Titanium.Web.Proxy
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)
......
......@@ -23,7 +23,7 @@ namespace Titanium.Web.Proxy.Helpers
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);
}
......
......@@ -99,7 +99,7 @@ namespace Titanium.Web.Proxy.Http
// 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();
......
......@@ -14,6 +14,8 @@ namespace Titanium.Web.Proxy.Http
[TypeConverter(typeof(ExpandableObjectConverter))]
public class Request : RequestResponseBase
{
private string originalUrl;
/// <summary>
/// Request Method.
/// </summary>
......@@ -32,7 +34,20 @@ namespace Titanium.Web.Proxy.Http
/// <summary>
/// The original request Url.
/// </summary>
public string OriginalUrl { get; set; }
public string OriginalUrl
{
get => originalUrl;
internal set
{
originalUrl = value;
RequestUriString = value;
}
}
/// <summary>
/// The request uri as it is in the HTTP header
/// </summary>
public string RequestUriString { get; set; }
/// <summary>
/// Has request body?
......@@ -140,7 +155,7 @@ namespace Titanium.Web.Proxy.Http
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}");
......
......@@ -39,14 +39,19 @@ namespace Titanium.Web.Proxy.Http2
CancellationTokenSource cancellationTokenSource, Guid connectionId,
ExceptionHandler exceptionFunc)
{
var clientSettings = new Http2Settings();
var serverSettings = new Http2Settings();
var sessions = new ConcurrentDictionary<int, SessionEventArgs>();
// 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 @@ namespace Titanium.Web.Proxy.Http2
}
private static async Task copyHttp2FrameAsync(Stream input, Stream output, Action<byte[], int, int> onCopy,
Http2Settings localSettings, Http2Settings remoteSettings,
Func<SessionEventArgs> sessionFactory, ConcurrentDictionary<int, SessionEventArgs> sessions,
Func<SessionEventArgs, Task> 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 @@ namespace Titanium.Web.Proxy.Http2
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 @@ namespace Titanium.Web.Proxy.Http2
}
}
}
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 @@ namespace Titanium.Web.Proxy.Http2
});
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 @@ namespace Titanium.Web.Proxy.Http2
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 @@ namespace Titanium.Web.Proxy.Http2
return totalRead;
}
class Http2Settings
{
public int HeaderTableSize { get; set; } = 4096;
public int MaxFrameSize { get; set; } = 16384;
}
class MyHeaderListener : IHeaderListener
{
private readonly Action<string, string> addHeaderFunc;
......
......@@ -92,7 +92,7 @@ namespace Titanium.Web.Proxy
// 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;
......
......@@ -20,7 +20,7 @@ namespace Titanium.Web.Proxy.IntegrationTests.Helpers
var request = new Request
{
Method = "POST",
OriginalUrl = "/",
RequestUriString = "/",
HttpVersion = new Version(1, 1)
};
request.Headers.AddHeader(KnownHeaders.Host, server);
......
......@@ -26,7 +26,7 @@ namespace Titanium.Web.Proxy.IntegrationTests.Helpers
RequestResponseBase request = new Request()
{
Method = method,
OriginalUrl = url,
RequestUriString = url,
HttpVersion = version
};
while (!string.IsNullOrEmpty(line = reader.ReadLine()))
......
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