Commit 0fe5a400 authored by Honfika's avatar Honfika

HTTP/2 => HTTP/2 support in "readonly" mode without user events (HTTP 1.x <=>...

HTTP/2 => HTTP/2 support in "readonly" mode without user events (HTTP 1.x <=> HTTP/2 fails currently)
parent 4dd7f91f
......@@ -13,6 +13,7 @@ namespace Titanium.Web.Proxy.EventArguments
CancellationTokenSource cancellationTokenSource, ExceptionHandler exceptionFunc)
: base(bufferSize, endPoint, cancellationTokenSource, connectRequest, exceptionFunc)
{
WebSession.ConnectRequest = connectRequest;
}
public bool DecryptSsl { get; set; } = true;
......
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
......@@ -40,7 +41,7 @@ namespace Titanium.Web.Proxy
try
{
string connectHostname = null;
ConnectRequest connectRequest = 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)
......@@ -57,7 +58,7 @@ namespace Titanium.Web.Proxy
var httpRemoteUri = new Uri("http://" + httpUrl);
connectHostname = httpRemoteUri.Host;
connectRequest = new ConnectRequest
var connectRequest = new ConnectRequest
{
RequestUri = httpRemoteUri,
OriginalUrl = httpUrl,
......@@ -66,7 +67,7 @@ namespace Titanium.Web.Proxy
await HeaderParser.ReadHeaders(clientStreamReader, connectRequest.Headers, cancellationToken);
var connectArgs = new TunnelConnectSessionEventArgs(BufferSize, endPoint, connectRequest,
connectArgs = new TunnelConnectSessionEventArgs(BufferSize, endPoint, connectRequest,
cancellationTokenSource, ExceptionFunc);
connectArgs.ProxyClient.TcpClient = tcpClient;
connectArgs.ProxyClient.ClientStream = clientStream;
......@@ -220,9 +221,41 @@ namespace Titanium.Web.Proxy
}
}
if (connectArgs != null && await HttpHelper.IsPriMethod(clientStream) == 1)
{
// todo
string httpCmd = await clientStreamReader.ReadLineAsync(cancellationToken);
if (httpCmd == "PRI * HTTP/2.0")
{
// HTTP/2 Connection Preface
string line = await clientStreamReader.ReadLineAsync(cancellationToken);
if (line != string.Empty) throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received");
line = await clientStreamReader.ReadLineAsync(cancellationToken);
if (line != "SM") throw new Exception($"HTTP/2 Protocol violation. 'SM' expected, '{line}' received");
line = await clientStreamReader.ReadLineAsync(cancellationToken);
if (line != string.Empty) throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received");
//create new connection
using (var connection = await GetServerConnection(connectArgs, true, cancellationToken))
{
await connection.StreamWriter.WriteLineAsync("PRI * HTTP/2.0", cancellationToken);
await connection.StreamWriter.WriteLineAsync(cancellationToken);
await connection.StreamWriter.WriteLineAsync("SM", cancellationToken);
await connection.StreamWriter.WriteLineAsync(cancellationToken);
await TcpHelper.SendHttp2(clientStream, connection.Stream, BufferSize,
(buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); },
(buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); },
connectArgs.CancellationTokenSource, ExceptionFunc);
}
}
}
//Now create the request
await HandleHttpSessionRequest(endPoint, tcpClient, clientStream, clientStreamReader,
clientStreamWriter, cancellationTokenSource, connectHostname, connectRequest);
clientStreamWriter, cancellationTokenSource, connectHostname, connectArgs?.WebSession.ConnectRequest);
}
catch (ProxyHttpException e)
{
......
......@@ -120,9 +120,32 @@ namespace Titanium.Web.Proxy.Helpers
/// </summary>
/// <param name="clientStream">The client stream.</param>
/// <returns>1: when CONNECT, 0: when valid HTTP method, -1: otherwise</returns>
internal static async Task<int> IsConnectMethod(CustomBufferedStream clientStream)
internal static Task<int> IsConnectMethod(CustomBufferedStream clientStream)
{
bool isConnect = true;
return StartsWith(clientStream, "CONNECT");
}
/// <summary>
/// Determines whether is pri method (HTTP/2).
/// </summary>
/// <param name="clientStream">The client stream.</param>
/// <returns>1: when PRI, 0: when valid HTTP method, -1: otherwise</returns>
internal static Task<int> IsPriMethod(CustomBufferedStream clientStream)
{
return StartsWith(clientStream, "PRI");
}
/// <summary>
/// Determines whether the stream starts with the given string.
/// </summary>
/// <param name="clientStream">The client stream.</param>
/// <param name="expectedStart">The expected start.</param>
/// <returns>
/// 1: when starts with the given string, 0: when valid HTTP method, -1: otherwise
/// </returns>
private static async Task<int> StartsWith(CustomBufferedStream clientStream, string expectedStart)
{
bool isExpected = true;
int legthToCheck = 10;
for (int i = 0; i < legthToCheck; i++)
{
......@@ -134,7 +157,7 @@ namespace Titanium.Web.Proxy.Helpers
if (b == ' ' && i > 2)
{
return isConnect ? 1 : 0;
return isExpected ? 1 : 0;
}
char ch = (char)b;
......@@ -143,14 +166,14 @@ namespace Titanium.Web.Proxy.Helpers
return -1;
}
if (i > 6 || ch != "CONNECT"[i])
if (i >= expectedStart.Length || ch != expectedStart[i])
{
isConnect = false;
isExpected = false;
}
}
// only letters
return isConnect ? 1 : 0;
return isExpected ? 1 : 0;
}
}
}
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Http;
namespace Titanium.Web.Proxy.Helpers
{
......@@ -7,5 +10,20 @@ namespace Titanium.Web.Proxy.Helpers
public HttpRequestWriter(Stream stream, int bufferSize) : base(stream, bufferSize)
{
}
/// <summary>
/// Writes the request.
/// </summary>
/// <param name="request"></param>
/// <param name="flush"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task WriteRequestAsync(Request request, bool flush = true,
CancellationToken cancellationToken = default)
{
await WriteLineAsync(Request.CreateRequestLine(request.Method, request.OriginalUrl, request.HttpVersion),
cancellationToken);
await WriteAsync(request, flush, cancellationToken);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Helpers
{
public class Ref<T>
{
public Ref()
{
}
public Ref(T value)
{
Value = value;
}
public T Value { get; set; }
public override string ToString()
{
T value = Value;
return value == null ? string.Empty : value.ToString();
}
public static implicit operator T(Ref<T> r)
{
return r.Value;
}
public static implicit operator Ref<T>(T value)
{
return new Ref<T>(value);
}
}
}
......@@ -130,7 +130,8 @@ namespace Titanium.Web.Proxy.Helpers
/// <param name="exceptionFunc"></param>
/// <returns></returns>
internal static async Task SendRawApm(Stream clientStream, Stream serverStream, int bufferSize,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive, CancellationTokenSource cancellationTokenSource,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive,
CancellationTokenSource cancellationTokenSource,
ExceptionHandler exceptionFunc)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
......@@ -234,7 +235,8 @@ namespace Titanium.Web.Proxy.Helpers
/// <param name="exceptionFunc"></param>
/// <returns></returns>
private static async Task SendRawTap(Stream clientStream, Stream serverStream, int bufferSize,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive, CancellationTokenSource cancellationTokenSource,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive,
CancellationTokenSource cancellationTokenSource,
ExceptionHandler exceptionFunc)
{
//Now async relay all server=>client & client=>server data
......@@ -263,12 +265,101 @@ namespace Titanium.Web.Proxy.Helpers
/// <param name="exceptionFunc"></param>
/// <returns></returns>
internal static Task SendRaw(Stream clientStream, Stream serverStream, int bufferSize,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive, CancellationTokenSource cancellationTokenSource,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive,
CancellationTokenSource cancellationTokenSource,
ExceptionHandler exceptionFunc)
{
// todo: fix APM mode
return SendRawTap(clientStream, serverStream, bufferSize, onDataSend, onDataReceive, cancellationTokenSource,
return SendRawTap(clientStream, serverStream, bufferSize, onDataSend, onDataReceive,
cancellationTokenSource,
exceptionFunc);
}
/// <summary>
/// relays the input clientStream to the server at the specified host name and port with the given httpCmd and headers
/// as prefix
/// Usefull for websocket requests
/// Task-based Asynchronous Pattern
/// </summary>
/// <param name="clientStream"></param>
/// <param name="serverStream"></param>
/// <param name="bufferSize"></param>
/// <param name="onDataSend"></param>
/// <param name="onDataReceive"></param>
/// <param name="cancellationTokenSource"></param>
/// <param name="exceptionFunc"></param>
/// <returns></returns>
internal static async Task SendHttp2(Stream clientStream, Stream serverStream, int bufferSize,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive,
CancellationTokenSource cancellationTokenSource,
ExceptionHandler exceptionFunc)
{
var connectionId = Guid.NewGuid();
//Now async relay all server=>client & client=>server data
var sendRelay =
CopyHttp2FrameAsync(clientStream, serverStream, onDataSend, bufferSize, connectionId, cancellationTokenSource.Token);
var receiveRelay =
CopyHttp2FrameAsync(serverStream, clientStream, onDataReceive, bufferSize, connectionId, cancellationTokenSource.Token);
await Task.WhenAny(sendRelay, receiveRelay);
cancellationTokenSource.Cancel();
await Task.WhenAll(sendRelay, receiveRelay);
}
private static async Task CopyHttp2FrameAsync(Stream input, Stream output, Action<byte[], int, int> onCopy,
int bufferSize, Guid connectionId, CancellationToken cancellationToken)
{
var headerBuffer = new byte[9];
var buffer = new byte[32768];
while (true)
{
int read = await ForceRead(input, headerBuffer, 0, 9, cancellationToken);
if (read != 9)
{
return;
}
int length = (headerBuffer[0] << 16) + (headerBuffer[1] << 8) + headerBuffer[2];
byte type = headerBuffer[3];
byte flags = headerBuffer[4];
int streamId = ((headerBuffer[5] & 0x7f) << 24) + (headerBuffer[6] << 16) + (headerBuffer[7] << 8) + headerBuffer[8];
read = await ForceRead(input, buffer, 0, length, cancellationToken);
if (read != length)
{
return;
}
await output.WriteAsync(headerBuffer, 0, headerBuffer.Length, cancellationToken);
await output.WriteAsync(buffer, 0, length, cancellationToken);
/*using (var fs = new System.IO.FileStream($@"c:\11\{connectionId}.{streamId}.dat", FileMode.Append))
{
fs.Write(headerBuffer, 0, headerBuffer.Length);
fs.Write(buffer, 0, length);
}*/
}
}
private static async Task<int> ForceRead(Stream input, byte[] buffer, int offset, int bytesToRead, CancellationToken cancellationToken)
{
int totalRead = 0;
while (bytesToRead > 0)
{
int read = await input.ReadAsync(buffer, offset, bytesToRead, cancellationToken);
if (read == -1)
{
break;
}
totalRead += read;
bytesToRead -= read;
offset += read;
}
return totalRead;
}
}
}
......@@ -26,7 +26,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// <param name="remotePort"></param>
/// <param name="applicationProtocols"></param>
/// <param name="httpVersion"></param>
/// <param name="isHttps"></param>
/// <param name="decryptSsl"></param>
/// <param name="isConnect"></param>
/// <param name="proxyServer"></param>
/// <param name="upStreamEndPoint"></param>
......@@ -34,7 +34,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// <param name="cancellationToken"></param>
/// <returns></returns>
internal async Task<TcpConnection> CreateClient(string remoteHostName, int remotePort,
List<SslApplicationProtocol> applicationProtocols, Version httpVersion, bool isHttps, bool isConnect,
List<SslApplicationProtocol> applicationProtocols, Version httpVersion, bool decryptSsl, bool isConnect,
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy, CancellationToken cancellationToken)
{
bool useUpstreamProxy = false;
......@@ -71,22 +71,24 @@ namespace Titanium.Web.Proxy.Network.Tcp
stream = new CustomBufferedStream(client.GetStream(), proxyServer.BufferSize);
if (useUpstreamProxy && (isConnect || isHttps))
if (useUpstreamProxy && (isConnect || decryptSsl))
{
var writer = new HttpRequestWriter(stream, proxyServer.BufferSize);
await writer.WriteLineAsync($"CONNECT {remoteHostName}:{remotePort} HTTP/{httpVersion}", cancellationToken);
await writer.WriteLineAsync($"Host: {remoteHostName}:{remotePort}", cancellationToken);
await writer.WriteLineAsync($"{KnownHeaders.Connection}: {KnownHeaders.ConnectionKeepAlive}", cancellationToken);
var connectRequest = new ConnectRequest
{
OriginalUrl = $"{remoteHostName}:{remotePort}",
HttpVersion = httpVersion,
};
connectRequest.Headers.AddHeader(KnownHeaders.Connection, KnownHeaders.ConnectionKeepAlive);
if (!string.IsNullOrEmpty(externalProxy.UserName) && externalProxy.Password != null)
{
await HttpHeader.ProxyConnectionKeepAlive.WriteToStreamAsync(writer, cancellationToken);
await writer.WriteLineAsync(KnownHeaders.ProxyAuthorization + ": Basic " +
Convert.ToBase64String(Encoding.UTF8.GetBytes(
externalProxy.UserName + ":" + externalProxy.Password)), cancellationToken);
connectRequest.Headers.AddHeader(HttpHeader.ProxyConnectionKeepAlive);
connectRequest.Headers.AddHeader(HttpHeader.GetProxyAuthorizationHeader(externalProxy.UserName, externalProxy.Password));
}
await writer.WriteLineAsync(cancellationToken);
await writer.WriteRequestAsync(connectRequest, cancellationToken: cancellationToken);
using (var reader = new CustomBinaryReader(stream, proxyServer.BufferSize))
{
......@@ -104,7 +106,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
}
}
if (isHttps)
if (decryptSsl)
{
var sslStream = new SslStream(stream, false, proxyServer.ValidateServerCertificate,
proxyServer.SelectClientCertificate);
......@@ -118,7 +120,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
}
// server connection is always HTTP 1.x, todo
options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList;
//options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList;
options.TargetHost = remoteHostName;
options.ClientCertificates = null;
......@@ -143,7 +145,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
UpStreamEndPoint = upStreamEndPoint,
HostName = remoteHostName,
Port = remotePort,
IsHttps = isHttps,
IsHttps = decryptSsl,
UseUpstreamProxy = useUpstreamProxy,
TcpClient = client,
StreamReader = new CustomBinaryReader(stream, proxyServer.BufferSize),
......
......@@ -58,29 +58,6 @@ namespace Titanium.Web.Proxy
{
// read the request line
string httpCmd = await clientStreamReader.ReadLineAsync(cancellationToken);
if (httpCmd == "PRI * HTTP/2.0")
{
// HTTP/2 Connection Preface
string line = await clientStreamReader.ReadLineAsync(cancellationToken);
if (line != string.Empty) throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received");
line = await clientStreamReader.ReadLineAsync(cancellationToken);
if (line != "SM") throw new Exception($"HTTP/2 Protocol violation. 'SM' expected, '{line}' received");
line = await clientStreamReader.ReadLineAsync(cancellationToken);
if (line != string.Empty) throw new Exception($"HTTP/2 Protocol violation. Empty string expected, '{line}' received");
// todo
var buffer = new byte[1024];
await clientStreamReader.ReadBytesAsync(buffer, 0, 3, cancellationToken);
int length = (buffer[0] << 16) + (buffer[1] << 8) + buffer[2];
await clientStreamReader.ReadBytesAsync(buffer, 0, length, cancellationToken);
byte type = buffer[0];
byte flags = buffer[1];
}
if (string.IsNullOrEmpty(httpCmd))
{
......@@ -101,7 +78,8 @@ namespace Titanium.Web.Proxy
out var version);
//Read the request headers in to unique and non-unique header collections
await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.Headers, cancellationToken);
await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.Headers,
cancellationToken);
Uri httpRemoteUri;
if (uriSchemeRegex.IsMatch(httpUrl))
......@@ -153,7 +131,8 @@ namespace Titanium.Web.Proxy
await InvokeBeforeResponse(args);
//send the response
await clientStreamWriter.WriteResponseAsync(args.WebSession.Response, cancellationToken: cancellationToken);
await clientStreamWriter.WriteResponseAsync(args.WebSession.Response,
cancellationToken: cancellationToken);
return;
}
......@@ -214,7 +193,8 @@ namespace Titanium.Web.Proxy
{
//prepare the prefix content
await connection.StreamWriter.WriteLineAsync(httpCmd, cancellationToken);
await connection.StreamWriter.WriteHeadersAsync(request.Headers, cancellationToken: cancellationToken);
await connection.StreamWriter.WriteHeadersAsync(request.Headers,
cancellationToken: cancellationToken);
string httpStatus = await connection.StreamReader.ReadLineAsync(cancellationToken);
Response.ParseResponseLine(httpStatus, out var responseVersion,
......@@ -224,11 +204,13 @@ namespace Titanium.Web.Proxy
response.StatusCode = responseStatusCode;
response.StatusDescription = responseStatusDescription;
await HeaderParser.ReadHeaders(connection.StreamReader, response.Headers, cancellationToken);
await HeaderParser.ReadHeaders(connection.StreamReader, response.Headers,
cancellationToken);
if (!args.IsTransparent)
{
await clientStreamWriter.WriteResponseAsync(response, cancellationToken: cancellationToken);
await clientStreamWriter.WriteResponseAsync(response,
cancellationToken: cancellationToken);
}
//If user requested call back then do it
......
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