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 @@ ...@@ -36,13 +36,20 @@
</ListView> </ListView>
<TabControl Grid.Column="2" Grid.Row="0"> <TabControl Grid.Column="2" Grid.Row="0">
<TabItem Header="Session"> <TabItem Header="Session">
<Grid Background="Red" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />
<RowDefinition /> <RowDefinition />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBox x:Name="TextBoxRequest" Grid.Row="0" /> <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> </Grid>
</TabItem> </TabItem>
</TabControl> </TabControl>
......
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
...@@ -8,6 +9,7 @@ using System.Threading.Tasks; ...@@ -8,6 +9,7 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media.Imaging;
using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Models;
...@@ -99,7 +101,7 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -99,7 +101,7 @@ namespace Titanium.Web.Proxy.Examples.Wpf
InitializeComponent(); InitializeComponent();
} }
public ObservableCollection<SessionListItem> Sessions { get; } = new ObservableCollection<SessionListItem>(); public ObservableCollectionEx<SessionListItem> Sessions { get; } = new ObservableCollectionEx<SessionListItem>();
public SessionListItem SelectedSession public SessionListItem SelectedSession
{ {
...@@ -278,11 +280,14 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -278,11 +280,14 @@ namespace Titanium.Web.Proxy.Examples.Wpf
if (e.Key == Key.Delete) if (e.Key == Key.Delete)
{ {
var selectedItems = ((ListView)sender).SelectedItems; var selectedItems = ((ListView)sender).SelectedItems;
Sessions.SuppressNotification = true;
foreach (var item in selectedItems.Cast<SessionListItem>().ToArray()) foreach (var item in selectedItems.Cast<SessionListItem>().ToArray())
{ {
Sessions.Remove(item); Sessions.Remove(item);
sessionDictionary.Remove(item.HttpClient); sessionDictionary.Remove(item.HttpClient);
} }
Sessions.SuppressNotification = false;
} }
} }
...@@ -297,7 +302,8 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -297,7 +302,8 @@ namespace Titanium.Web.Proxy.Examples.Wpf
var session = SelectedSession.HttpClient; var session = SelectedSession.HttpClient;
var request = session.Request; 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; bool truncated = data.Length > truncateLimit;
if (truncated) if (truncated)
{ {
...@@ -313,7 +319,8 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -313,7 +319,8 @@ namespace Titanium.Web.Proxy.Examples.Wpf
TextBoxRequest.Text = sb.ToString(); TextBoxRequest.Text = sb.ToString();
var response = session.Response; 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; truncated = data.Length > truncateLimit;
if (truncated) if (truncated)
{ {
...@@ -333,6 +340,19 @@ namespace Titanium.Web.Proxy.Examples.Wpf ...@@ -333,6 +340,19 @@ namespace Titanium.Web.Proxy.Examples.Wpf
} }
TextBoxResponse.Text = sb.ToString(); 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 @@ ...@@ -92,6 +92,7 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</ApplicationDefinition> </ApplicationDefinition>
<Compile Include="ObservableCollectionEx.cs" />
<Compile Include="Properties\Annotations.cs" /> <Compile Include="Properties\Annotations.cs" />
<Compile Include="SessionListItem.cs" /> <Compile Include="SessionListItem.cs" />
<Page Include="MainWindow.xaml"> <Page Include="MainWindow.xaml">
......
...@@ -245,8 +245,7 @@ namespace StreamExtended.Network ...@@ -245,8 +245,7 @@ namespace StreamExtended.Network
{ {
return -1; return -1;
} }
return streamBuffer[bufferPos + index]; return streamBuffer[bufferPos + index];
} }
...@@ -491,7 +490,7 @@ namespace StreamExtended.Network ...@@ -491,7 +490,7 @@ namespace StreamExtended.Network
if (bufferLength > 0) if (bufferLength > 0)
{ {
//normally we fill the buffer only when it is empty, but sometimes we need more data //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); Buffer.BlockCopy(streamBuffer, bufferPos, streamBuffer, 0, bufferLength);
} }
...@@ -516,7 +515,6 @@ namespace StreamExtended.Network ...@@ -516,7 +515,6 @@ namespace StreamExtended.Network
} }
return result; return result;
} }
/// <summary> /// <summary>
......
...@@ -4,6 +4,7 @@ using System.IO; ...@@ -4,6 +4,7 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using StreamExtended; using StreamExtended;
using StreamExtended.Network; using StreamExtended.Network;
using Titanium.Web.Proxy.Exceptions;
namespace Titanium.Web.Proxy.EventArguments namespace Titanium.Web.Proxy.EventArguments
{ {
...@@ -60,7 +61,11 @@ namespace Titanium.Web.Proxy.EventArguments ...@@ -60,7 +61,11 @@ namespace Titanium.Web.Proxy.EventArguments
chunkHead = chunkHead.Substring(0, idx); 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; bytesRemaining = chunkSize;
if (chunkSize == 0) if (chunkSize == 0)
......
...@@ -47,8 +47,7 @@ namespace Titanium.Web.Proxy ...@@ -47,8 +47,7 @@ namespace Titanium.Web.Proxy
{ {
string connectHostname = null; string connectHostname = null;
TunnelConnectSessionEventArgs connectArgs = null; TunnelConnectSessionEventArgs connectArgs = null;
// Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request) // Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request)
if (await HttpHelper.IsConnectMethod(clientStream) == 1) if (await HttpHelper.IsConnectMethod(clientStream) == 1)
{ {
...@@ -142,15 +141,22 @@ namespace Titanium.Web.Proxy ...@@ -142,15 +141,22 @@ namespace Titanium.Web.Proxy
if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2)) if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2))
{ {
// test server HTTP/2 support // test server HTTP/2 support
// todo: this is a hack, because Titanium does not support HTTP protocol changing currently try
var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs, {
isConnect: true, applicationProtocols: SslExtensions.Http2ProtocolAsList, // todo: this is a hack, because Titanium does not support HTTP protocol changing currently
noCache: true, cancellationToken: cancellationToken); var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs,
isConnect: true, applicationProtocols: SslExtensions.Http2ProtocolAsList,
http2Supported = connection.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2; noCache: true, cancellationToken: cancellationToken);
//release connection back to pool instead of closing when connection pool is enabled. http2Supported = connection.NegotiatedApplicationProtocol ==
await tcpConnectionFactory.Release(connection, true); 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) if (EnableTcpServerConnectionPrefetch)
......
...@@ -23,7 +23,7 @@ namespace Titanium.Web.Proxy.Helpers ...@@ -23,7 +23,7 @@ namespace Titanium.Web.Proxy.Helpers
internal async Task WriteRequestAsync(Request request, bool flush = true, internal async Task WriteRequestAsync(Request request, bool flush = true,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await WriteLineAsync(Request.CreateRequestLine(request.Method, request.OriginalUrl, request.HttpVersion), await WriteLineAsync(Request.CreateRequestLine(request.Method, request.RequestUriString, request.HttpVersion),
cancellationToken); cancellationToken);
await WriteAsync(request, flush, cancellationToken); await WriteAsync(request, flush, cancellationToken);
} }
......
...@@ -99,7 +99,7 @@ namespace Titanium.Web.Proxy.Http ...@@ -99,7 +99,7 @@ namespace Titanium.Web.Proxy.Http
// prepare the request & headers // prepare the request & headers
await writer.WriteLineAsync(Request.CreateRequestLine(Request.Method, await writer.WriteLineAsync(Request.CreateRequestLine(Request.Method,
useUpstreamProxy || isTransparent ? Request.OriginalUrl : Request.RequestUri.PathAndQuery, useUpstreamProxy || isTransparent ? Request.RequestUriString : Request.RequestUri.PathAndQuery,
Request.HttpVersion), cancellationToken); Request.HttpVersion), cancellationToken);
var headerBuilder = new StringBuilder(); var headerBuilder = new StringBuilder();
......
...@@ -14,6 +14,8 @@ namespace Titanium.Web.Proxy.Http ...@@ -14,6 +14,8 @@ namespace Titanium.Web.Proxy.Http
[TypeConverter(typeof(ExpandableObjectConverter))] [TypeConverter(typeof(ExpandableObjectConverter))]
public class Request : RequestResponseBase public class Request : RequestResponseBase
{ {
private string originalUrl;
/// <summary> /// <summary>
/// Request Method. /// Request Method.
/// </summary> /// </summary>
...@@ -32,7 +34,20 @@ namespace Titanium.Web.Proxy.Http ...@@ -32,7 +34,20 @@ namespace Titanium.Web.Proxy.Http
/// <summary> /// <summary>
/// The original request Url. /// The original request Url.
/// </summary> /// </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> /// <summary>
/// Has request body? /// Has request body?
...@@ -140,7 +155,7 @@ namespace Titanium.Web.Proxy.Http ...@@ -140,7 +155,7 @@ namespace Titanium.Web.Proxy.Http
get get
{ {
var sb = new StringBuilder(); 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) foreach (var header in Headers)
{ {
sb.Append($"{header.ToString()}{ProxyConstants.NewLine}"); sb.Append($"{header.ToString()}{ProxyConstants.NewLine}");
......
...@@ -39,14 +39,19 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -39,14 +39,19 @@ namespace Titanium.Web.Proxy.Http2
CancellationTokenSource cancellationTokenSource, Guid connectionId, CancellationTokenSource cancellationTokenSource, Guid connectionId,
ExceptionHandler exceptionFunc) ExceptionHandler exceptionFunc)
{ {
var clientSettings = new Http2Settings();
var serverSettings = new Http2Settings();
var sessions = new ConcurrentDictionary<int, SessionEventArgs>(); 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, sessionFactory, sessions, onBeforeRequest, copyHttp2FrameAsync(clientStream, serverStream, onDataSend, clientSettings, serverSettings,
sessionFactory, sessions, onBeforeRequest,
bufferSize, connectionId, true, cancellationTokenSource.Token, exceptionFunc); bufferSize, connectionId, true, cancellationTokenSource.Token, exceptionFunc);
var receiveRelay = var receiveRelay =
copyHttp2FrameAsync(serverStream, clientStream, onDataReceive, sessionFactory, sessions, onBeforeResponse, copyHttp2FrameAsync(serverStream, clientStream, onDataReceive, serverSettings, clientSettings,
sessionFactory, sessions, onBeforeResponse,
bufferSize, connectionId, false, cancellationTokenSource.Token, exceptionFunc); bufferSize, connectionId, false, cancellationTokenSource.Token, exceptionFunc);
await Task.WhenAny(sendRelay, receiveRelay); await Task.WhenAny(sendRelay, receiveRelay);
...@@ -56,15 +61,17 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -56,15 +61,17 @@ namespace Titanium.Web.Proxy.Http2
} }
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,
Http2Settings localSettings, Http2Settings remoteSettings,
Func<SessionEventArgs> sessionFactory, ConcurrentDictionary<int, SessionEventArgs> sessions, Func<SessionEventArgs> sessionFactory, ConcurrentDictionary<int, SessionEventArgs> sessions,
Func<SessionEventArgs, Task> onBeforeRequestResponse, Func<SessionEventArgs, Task> onBeforeRequestResponse,
int bufferSize, Guid connectionId, bool isClient, CancellationToken cancellationToken, int bufferSize, Guid connectionId, bool isClient, CancellationToken cancellationToken,
ExceptionHandler exceptionFunc) ExceptionHandler exceptionFunc)
{ {
var decoder = new Decoder(8192, 4096 * 16); int headerTableSize = 0;
Decoder decoder = null;
var headerBuffer = new byte[9]; var headerBuffer = new byte[9];
var buffer = new byte[32768]; byte[] buffer = null;
while (true) while (true)
{ {
int read = await forceRead(input, headerBuffer, 0, 9, cancellationToken); int read = await forceRead(input, headerBuffer, 0, 9, cancellationToken);
...@@ -80,6 +87,11 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -80,6 +87,11 @@ 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];
if (buffer == null || buffer.Length < localSettings.MaxFrameSize)
{
buffer = new byte[localSettings.MaxFrameSize];
}
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)
...@@ -145,7 +157,7 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -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 endHeaders = (flags & (int)Http2FrameFlag.EndHeaders) != 0;
bool padded = (flags & (int)Http2FrameFlag.Padded) != 0; bool padded = (flags & (int)Http2FrameFlag.Padded) != 0;
...@@ -181,13 +193,18 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -181,13 +193,18 @@ namespace Titanium.Web.Proxy.Http2
}); });
try 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)), headerTableSize = localSettings.HeaderTableSize;
headerListener); decoder = new Decoder(8192, headerTableSize);
decoder.EndHeaderBlock();
} }
decoder.Decode(new BinaryReader(new MemoryStream(buffer, offset, dataLength)),
headerListener);
decoder.EndHeaderBlock();
if (isClient) if (isClient)
{ {
var request = args.HttpClient.Request; var request = args.HttpClient.Request;
...@@ -225,6 +242,53 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -225,6 +242,53 @@ namespace Titanium.Web.Proxy.Http2
rr.Locked = true; 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) if (!isClient && endStream)
{ {
...@@ -269,6 +333,14 @@ namespace Titanium.Web.Proxy.Http2 ...@@ -269,6 +333,14 @@ namespace Titanium.Web.Proxy.Http2
return totalRead; return totalRead;
} }
class Http2Settings
{
public int HeaderTableSize { get; set; } = 4096;
public int MaxFrameSize { get; set; } = 16384;
}
class MyHeaderListener : IHeaderListener class MyHeaderListener : IHeaderListener
{ {
private readonly Action<string, string> addHeaderFunc; private readonly Action<string, string> addHeaderFunc;
......
...@@ -92,7 +92,7 @@ namespace Titanium.Web.Proxy ...@@ -92,7 +92,7 @@ namespace Titanium.Web.Proxy
// clear current response // clear current response
await args.ClearResponse(cancellationToken); await args.ClearResponse(cancellationToken);
var httpCmd = Request.CreateRequestLine(args.HttpClient.Request.Method, 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, await handleHttpSessionRequest(httpCmd, args, null, args.ClientConnection.NegotiatedApplicationProtocol,
cancellationToken, args.CancellationTokenSource); cancellationToken, args.CancellationTokenSource);
return; return;
......
...@@ -20,7 +20,7 @@ namespace Titanium.Web.Proxy.IntegrationTests.Helpers ...@@ -20,7 +20,7 @@ namespace Titanium.Web.Proxy.IntegrationTests.Helpers
var request = new Request var request = new Request
{ {
Method = "POST", Method = "POST",
OriginalUrl = "/", RequestUriString = "/",
HttpVersion = new Version(1, 1) HttpVersion = new Version(1, 1)
}; };
request.Headers.AddHeader(KnownHeaders.Host, server); request.Headers.AddHeader(KnownHeaders.Host, server);
......
...@@ -26,7 +26,7 @@ namespace Titanium.Web.Proxy.IntegrationTests.Helpers ...@@ -26,7 +26,7 @@ namespace Titanium.Web.Proxy.IntegrationTests.Helpers
RequestResponseBase request = new Request() RequestResponseBase request = new Request()
{ {
Method = method, Method = method,
OriginalUrl = url, RequestUriString = url,
HttpVersion = version HttpVersion = version
}; };
while (!string.IsNullOrEmpty(line = reader.ReadLine())) 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