Skip to content
This repository was archived by the owner on Jul 9, 2023. It is now read-only.

beta #689

Merged
merged 4 commits into from
Nov 30, 2019
Merged

beta #689

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.StreamExtended.Network;

namespace Titanium.Web.Proxy.Examples.Basic
{
Expand All @@ -22,6 +25,9 @@ public class ProxyTestController
public ProxyTestController()
{
proxyServer = new ProxyServer();

proxyServer.EnableHttp2 = true;

// generate root certificate without storing it in file system
//proxyServer.CertificateManager.CreateRootCertificate(false);

Expand All @@ -32,11 +38,11 @@ public ProxyTestController()
{
if (exception is ProxyHttpException phex)
{
await writeToConsole(exception.Message + ": " + phex.InnerException?.Message, true);
await writeToConsole(exception.Message + ": " + phex.InnerException?.Message, ConsoleColor.Red);
}
else
{
await writeToConsole(exception.Message, true);
await writeToConsole(exception.Message, ConsoleColor.Red);
}
};
proxyServer.ForwardToUpstreamGateway = true;
Expand Down Expand Up @@ -146,6 +152,38 @@ private async Task onBeforeTunnelConnectRequest(object sender, TunnelConnectSess
}
}

private void WebSocket_DataSent(object sender, DataEventArgs e)
{
var args = (SessionEventArgs)sender;
WebSocketDataSentReceived(args, e, true);
}

private void WebSocket_DataReceived(object sender, DataEventArgs e)
{
var args = (SessionEventArgs)sender;
WebSocketDataSentReceived(args, e, false);
}

private void WebSocketDataSentReceived(SessionEventArgs args, DataEventArgs e, bool sent)
{
var color = sent ? ConsoleColor.Green : ConsoleColor.Blue;

foreach (var frame in args.WebSocketDecoder.Decode(e.Buffer, e.Offset, e.Count))
{
if (frame.OpCode == WebsocketOpCode.Binary)
{
var data = frame.Data.ToArray();
string str = string.Join(",", data.ToArray().Select(x => x.ToString("X2")));
writeToConsole(str, color).Wait();
}

if (frame.OpCode == WebsocketOpCode.Text)
{
writeToConsole(frame.GetText(), color).Wait();
}
}
}

private Task onBeforeTunnelConnectResponse(object sender, TunnelConnectSessionEventArgs e)
{
return Task.FromResult(false);
Expand Down Expand Up @@ -205,6 +243,12 @@ private async Task multipartRequestPartSent(object sender, MultipartRequestPartS

private async Task onResponse(object sender, SessionEventArgs e)
{
if (e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Websocket)
{
e.DataSent += WebSocket_DataSent;
e.DataReceived += WebSocket_DataReceived;
}

await writeToConsole("Active Server Connections:" + ((ProxyServer)sender).ServerConnectionCount);

string ext = System.IO.Path.GetExtension(e.HttpClient.Request.RequestUri.AbsolutePath);
Expand Down Expand Up @@ -277,14 +321,14 @@ public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs
return Task.FromResult(0);
}

private async Task writeToConsole(string message, bool useRedColor = false)
private async Task writeToConsole(string message, ConsoleColor? consoleColor = null)
{
await @lock.WaitAsync();

if (useRedColor)
if (consoleColor.HasValue)
{
ConsoleColor existing = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.ForegroundColor = consoleColor.Value;
Console.WriteLine(message);
Console.ForegroundColor = existing;
}
Expand Down
1 change: 1 addition & 0 deletions src/Titanium.Web.Proxy.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/CodeAnnotations/NamespacesWithAnnotations/=Titanium_002EWeb_002EProxy_002EExamples_002EWpf_002EAnnotations/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=docfx_002Ejson/@EntryIndexedValue">docfx.json</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_EMBEDDED_ARRANGEMENT/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/LINE_FEED_AT_FILE_END/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
Expand Down
4 changes: 4 additions & 0 deletions src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class SessionEventArgs : SessionEventArgsBase
/// </summary>
private bool reRequest;

private WebSocketDecoder webSocketDecoder;

/// <summary>
/// Is this session a HTTP/2 promise?
/// </summary>
Expand Down Expand Up @@ -58,6 +60,8 @@ public bool ReRequest
}
}

public WebSocketDecoder WebSocketDecoder => webSocketDecoder ??= new WebSocketDecoder(BufferPool);

/// <summary>
/// Occurs when multipart request part sent.
/// </summary>
Expand Down
45 changes: 33 additions & 12 deletions src/Titanium.Web.Proxy/ExplicitClientHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect
try
{
TunnelConnectSessionEventArgs? connectArgs = null;


var method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken);
if (clientStream.IsClosed)
{
return;
}

// Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request)
if (await HttpHelper.IsConnectMethod(clientStream, BufferPool, cancellationToken) == 1)
if (method == KnownMethod.Connect)
{
// read the first line HTTP command
var requestLine = await clientStream.ReadRequestLine(cancellationToken);
Expand All @@ -75,6 +81,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect

// filter out excluded host names
bool decryptSsl = endPoint.DecryptSsl && connectArgs.DecryptSsl;
bool sendRawData = !decryptSsl;

if (connectArgs.DenyConnect)
{
Expand Down Expand Up @@ -113,6 +120,10 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect
await clientStream.WriteResponseAsync(response, cancellationToken);

var clientHelloInfo = await SslTools.PeekClientHello(clientStream, BufferPool, cancellationToken);
if (clientStream.IsClosed)
{
return;
}

bool isClientHello = clientHelloInfo != null;
if (clientHelloInfo != null)
Expand Down Expand Up @@ -224,31 +235,41 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect
$"Couldn't authenticate host '{connectHostname}' with certificate '{certName}'.", e, connectArgs);
}

if (await HttpHelper.IsConnectMethod(clientStream, BufferPool, cancellationToken) == -1)
method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken);
if (clientStream.IsClosed)
{
decryptSsl = false;
return;
}

if (!decryptSsl)
if (method == KnownMethod.Invalid)
{
sendRawData = true;
await tcpConnectionFactory.Release(prefetchConnectionTask, true);
prefetchConnectionTask = null;
}
}
else if (clientHelloInfo == null)
{
method = await HttpHelper.GetMethod(clientStream, BufferPool, cancellationToken);
if (clientStream.IsClosed)
{
return;
}
}

if (cancellationTokenSource.IsCancellationRequested)
{
throw new Exception("Session was terminated by user.");
}

// Hostname is excluded or it is not an HTTPS connect
if (!decryptSsl || !isClientHello)
if (method == KnownMethod.Invalid)
{
if (!isClientHello)
{
connectRequest.TunnelType = TunnelType.Websocket;
}
sendRawData = true;
}

// Hostname is excluded or it is not an HTTPS connect
if (sendRawData)
{
// create new connection to server.
// If we detected that client tunnel CONNECTs without SSL by checking for empty client hello then
// this connection should not be HTTPS.
Expand Down Expand Up @@ -302,7 +323,7 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool,
}
}

if (connectArgs != null && await HttpHelper.IsPriMethod(clientStream, BufferPool, cancellationToken) == 1)
if (connectArgs != null && method == KnownMethod.Pri)
{
// todo
string? httpCmd = await clientStream.ReadLineAsync(cancellationToken);
Expand Down
119 changes: 82 additions & 37 deletions src/Titanium.Web.Proxy/Helpers/HttpHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,32 +167,11 @@ internal static string GetWildCardDomainName(string hostname)
}

/// <summary>
/// Determines whether is connect method.
/// Gets the HTTP method from the stream.
/// </summary>
/// <returns>1: when CONNECT, 0: when valid HTTP method, -1: otherwise</returns>
internal static ValueTask<int> IsConnectMethod(IPeekStream httpReader, IBufferPool bufferPool, CancellationToken cancellationToken = default)
public static async ValueTask<KnownMethod> GetMethod(IPeekStream httpReader, IBufferPool bufferPool, CancellationToken cancellationToken = default)
{
return startsWith(httpReader, bufferPool, "CONNECT", cancellationToken);
}

/// <summary>
/// Determines whether is pri method (HTTP/2).
/// </summary>
/// <returns>1: when PRI, 0: when valid HTTP method, -1: otherwise</returns>
internal static ValueTask<int> IsPriMethod(IPeekStream httpReader, IBufferPool bufferPool, CancellationToken cancellationToken = default)
{
return startsWith(httpReader, bufferPool, "PRI", cancellationToken);
}

/// <summary>
/// Determines whether the stream starts with the given string.
/// </summary>
/// <returns>
/// 1: when starts with the given string, 0: when valid HTTP method, -1: otherwise
/// </returns>
private static async ValueTask<int> startsWith(IPeekStream httpReader, IBufferPool bufferPool, string expectedStart, CancellationToken cancellationToken = default)
{
const int lengthToCheck = 10;
const int lengthToCheck = 20;
if (bufferPool.BufferSize < lengthToCheck)
{
throw new Exception($"Buffer is too small. Minimum size is {lengthToCheck} bytes");
Expand All @@ -201,13 +180,12 @@ private static async ValueTask<int> startsWith(IPeekStream httpReader, IBufferPo
byte[] buffer = bufferPool.GetBuffer(bufferPool.BufferSize);
try
{
bool isExpected = true;
int i = 0;
while (i < lengthToCheck)
{
int peeked = await httpReader.PeekBytesAsync(buffer, i, i, lengthToCheck - i, cancellationToken);
if (peeked <= 0)
return -1;
return KnownMethod.Invalid;

peeked += i;

Expand All @@ -216,27 +194,94 @@ private static async ValueTask<int> startsWith(IPeekStream httpReader, IBufferPo
int b = buffer[i];

if (b == ' ' && i > 2)
return isExpected ? 1 : 0;
else
{
char ch = (char)b;
if (ch < 'A' || ch > 'z' || (ch > 'Z' && ch < 'a')) // ASCII letter
return -1;
else if (i >= expectedStart.Length || ch != expectedStart[i])
isExpected = false;
}
return getKnownMethod(buffer.AsSpan(0, i));

char ch = (char)b;
if ((ch < 'A' || ch > 'z' || (ch > 'Z' && ch < 'a')) && (ch != '-')) // ASCII letter
return KnownMethod.Invalid;

i++;
}
}

// only letters
return 0;
// only letters, but no space (or shorter than 3 characters)
return KnownMethod.Invalid;
}
finally
{
bufferPool.ReturnBuffer(buffer);
}
}

private static KnownMethod getKnownMethod(ReadOnlySpan<byte> method)
{
// the following methods are supported:
// Connect
// Delete
// Get
// Head
// Options
// Post
// Put
// Trace
// Pri

// method parameter should have at least 3 bytes
byte b1 = method[0];
byte b2 = method[1];
byte b3 = method[2];

switch (method.Length)
{
case 3:
// Get or Put
if (b1 == 'G')
return b2 == 'E' && b3 == 'T' ? KnownMethod.Get : KnownMethod.Unknown;

if (b1 == 'P')
{
if (b2 == 'U')
return b3 == 'T' ? KnownMethod.Put : KnownMethod.Unknown;

if (b2 == 'R')
return b3 == 'I' ? KnownMethod.Pri : KnownMethod.Unknown;
}

break;
case 4:
// Head or Post
if (b1 == 'H')
return b2 == 'E' && b3 == 'A' && method[3] == 'D' ? KnownMethod.Head : KnownMethod.Unknown;

if (b1 == 'P')
return b2 == 'O' && b3 == 'S' && method[3] == 'T' ? KnownMethod.Post : KnownMethod.Unknown;

break;
case 5:
// Trace
if (b1 == 'T')
return b2 == 'R' && b3 == 'A' && method[3] == 'C' && method[4] == 'E' ? KnownMethod.Trace : KnownMethod.Unknown;

break;
case 6:
// Delete
if (b1 == 'D')
return b2 == 'E' && b3 == 'L' && method[3] == 'E' && method[4] == 'T' && method[5] == 'E' ? KnownMethod.Delete : KnownMethod.Unknown;

break;
case 7:
// Connect or Options
if (b1 == 'C')
return b2 == 'O' && b3 == 'N' && method[3] == 'N' && method[4] == 'E' && method[5] == 'C' && method[6] == 'T' ? KnownMethod.Connect : KnownMethod.Unknown;

if (b1 == 'O')
return b2 == 'P' && b3 == 'T' && method[3] == 'I' && method[4] == 'O' && method[5] == 'N' && method[6] == 'S' ? KnownMethod.Options : KnownMethod.Unknown;

break;
}


return KnownMethod.Unknown;
}
}
}
Loading