Unverified Commit 9bae9513 authored by honfika's avatar honfika Committed by GitHub

Merge pull request #692 from justcoding121/beta

stable
parents 931d59c1 f9a74d7b
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;
......@@ -10,6 +12,7 @@ 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
{
......@@ -22,6 +25,9 @@ namespace Titanium.Web.Proxy.Examples.Basic
public ProxyTestController()
{
proxyServer = new ProxyServer();
//proxyServer.EnableHttp2 = true;
// generate root certificate without storing it in file system
//proxyServer.CertificateManager.CreateRootCertificate(false);
......@@ -32,11 +38,11 @@ namespace Titanium.Web.Proxy.Examples.Basic
{
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;
......@@ -146,6 +152,38 @@ namespace Titanium.Web.Proxy.Examples.Basic
}
}
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);
......@@ -205,6 +243,12 @@ namespace Titanium.Web.Proxy.Examples.Basic
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);
......@@ -277,14 +321,14 @@ namespace Titanium.Web.Proxy.Examples.Basic
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;
}
......
<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>
......
......@@ -26,6 +26,8 @@ namespace Titanium.Web.Proxy.EventArguments
/// </summary>
private bool reRequest;
private WebSocketDecoder webSocketDecoder;
/// <summary>
/// Is this session a HTTP/2 promise?
/// </summary>
......@@ -58,6 +60,8 @@ namespace Titanium.Web.Proxy.EventArguments
}
}
public WebSocketDecoder WebSocketDecoder => webSocketDecoder ??= new WebSocketDecoder(BufferPool);
/// <summary>
/// Occurs when multipart request part sent.
/// </summary>
......
......@@ -47,9 +47,15 @@ namespace Titanium.Web.Proxy
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);
......@@ -75,6 +81,7 @@ namespace Titanium.Web.Proxy
// filter out excluded host names
bool decryptSsl = endPoint.DecryptSsl && connectArgs.DecryptSsl;
bool sendRawData = !decryptSsl;
if (connectArgs.DenyConnect)
{
......@@ -113,6 +120,10 @@ namespace Titanium.Web.Proxy
await clientStream.WriteResponseAsync(response, cancellationToken);
var clientHelloInfo = await SslTools.PeekClientHello(clientStream, BufferPool, cancellationToken);
if (clientStream.IsClosed)
{
return;
}
bool isClientHello = clientHelloInfo != null;
if (clientHelloInfo != null)
......@@ -130,26 +141,29 @@ namespace Titanium.Web.Proxy
bool http2Supported = false;
var alpn = clientHelloInfo.GetAlpn();
if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2))
if (EnableHttp2)
{
// test server HTTP/2 support
try
{
// todo: this is a hack, because Titanium does not support HTTP protocol changing currently
var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs,
true, SslExtensions.Http2ProtocolAsList,
true, 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)
var alpn = clientHelloInfo.GetAlpn();
if (alpn != null && alpn.Contains(SslApplicationProtocol.Http2))
{
// ignore
// test server HTTP/2 support
try
{
// todo: this is a hack, because Titanium does not support HTTP protocol changing currently
var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs,
true, SslExtensions.Http2ProtocolAsList,
true, 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
}
}
}
......@@ -224,36 +238,46 @@ namespace Titanium.Web.Proxy
$"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.
var connection = await tcpConnectionFactory.GetServerConnection(this, connectArgs,
true, SslExtensions.Http2ProtocolAsList,
true, null,
true, cancellationToken);
try
......@@ -302,7 +326,7 @@ namespace Titanium.Web.Proxy
}
}
if (connectArgs != null && await HttpHelper.IsPriMethod(clientStream, BufferPool, cancellationToken) == 1)
if (connectArgs != null && method == KnownMethod.Pri)
{
// todo
string? httpCmd = await clientStream.ReadLineAsync(cancellationToken);
......
......@@ -167,32 +167,11 @@ namespace Titanium.Web.Proxy.Helpers
}
/// <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");
......@@ -201,13 +180,12 @@ namespace Titanium.Web.Proxy.Helpers
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;
......@@ -216,27 +194,94 @@ namespace Titanium.Web.Proxy.Helpers
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;
}
}
}
namespace Titanium.Web.Proxy.Helpers
{
internal enum KnownMethod
{
Unknown,
Invalid,
// RFC 7231: Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
Connect,
Delete,
Get,
Head,
Options,
Post,
Put,
Trace,
// RFC 7540: Hypertext Transfer Protocol Version 2
Pri,
// RFC 5789: PATCH Method for HTTP
Patch,
// RFC 3744: Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol
Acl,
// RFC 3253: Versioning Extensions to WebDAV (Web Distributed Authoring and Versioning)
BaselineControl,
Checkin,
Checkout,
Label,
Merge,
Mkactivity,
Mkworkspace,
Report,
Unckeckout,
Update,
VersionControl,
// RFC 3648: Web Distributed Authoring and Versioning (WebDAV) Ordered Collections Protocol
Orderpatch,
// RFC 4437: Web Distributed Authoring and Versioning (WebDAV): Redirect Reference Resources
Mkredirectref,
Updateredirectref,
// RFC 4791: Calendaring Extensions to WebDAV (CalDAV)
Mkcalendar,
// RFC 4918: HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)
Copy,
Lock,
Mkcol,
Move,
Propfind,
Proppatch,
Unlock,
// RFC 5323: Web Distributed Authoring and Versioning (WebDAV) SEARCH
Search,
// RFC 5842: Binding Extensions to Web Distributed Authoring and Versioning (WebDAV)
Bind,
Rebind,
Unbind,
// Internet Draft snell-link-method: HTTP Link and Unlink Methods
Link,
Unlink,
}
}
......@@ -2,33 +2,33 @@
using System.Threading.Tasks;
using Titanium.Web.Proxy.StreamExtended.Network;
internal class NullWriter : IHttpStreamWriter
namespace Titanium.Web.Proxy.Helpers
{
public static NullWriter Instance { get; } = new NullWriter();
public void Write(byte[] buffer, int offset, int count)
internal class NullWriter : IHttpStreamWriter
{
}
public static NullWriter Instance { get; } = new NullWriter();
#if NET45
public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
}
public void Write(byte[] buffer, int offset, int count)
{
}
public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
#if NET45
return Net45Compatibility.CompletedTask;
#else
public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
return Task.CompletedTask;
#endif
}
public ValueTask WriteLineAsync(CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
public ValueTask WriteLineAsync(CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
public ValueTask WriteLineAsync(string value, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
public ValueTask WriteLineAsync(string value, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
}
}
......@@ -10,6 +10,8 @@ namespace Titanium.Web.Proxy
class Net45Compatibility
{
public static byte[] EmptyArray = new byte[0];
public static Task CompletedTask = new Task(() => { });
}
}
#endif
......@@ -60,6 +60,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
cacheKeyBuilder.Append("-");
cacheKeyBuilder.Append(remotePort);
cacheKeyBuilder.Append("-");
// when creating Tcp client isConnect won't matter
cacheKeyBuilder.Append(isHttps);
......@@ -408,6 +409,7 @@ retry:
continue;
}
break;
}
catch (Exception e)
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Titanium.Web.Proxy.StreamExtended.BufferPool;
namespace Titanium.Web.Proxy
{
public class WebSocketDecoder
{
private byte[] buffer;
private long bufferLength;
internal WebSocketDecoder(IBufferPool bufferPool)
{
buffer = new byte[bufferPool.BufferSize];
}
public IEnumerable<WebSocketFrame> Decode(byte[] data, int offset, int count)
{
var buffer = data.AsMemory(offset, count);
bool copied = false;
if (bufferLength > 0)
{
// already have remaining data
buffer = copyToBuffer(buffer);
copied = true;
}
while (true)
{
var data1 = buffer.Span;
if (!isDataEnough(data1))
{
break;
}
var opCode = (WebsocketOpCode)(data1[0] & 0xf);
bool isFinal = (data1[0] & 0x80) != 0;
byte b = data1[1];
long size = b & 0x7f;
// todo: size > int.Max??
bool masked = (b & 0x80) != 0;
int idx = 2;
if (size > 125)
{
if (size == 126)
{
size = (data1[2] << 8) + data1[3];
idx = 4;
}
else
{
size = ((long)data1[2] << 56) + ((long)data1[3] << 48) + ((long)data1[4] << 40) + ((long)data1[5] << 32) +
((long)data1[6] << 24) + (data1[7] << 16) + (data1[8] << 8) + data1[9];
idx = 10;
}
}
if (data1.Length < idx + size)
{
break;
}
if (masked)
{
//mask = (uint)(((long)data1[idx++] << 24) + (data1[idx++] << 16) + (data1[idx++] << 8) + data1[idx++]);
//mask = (uint)(data1[idx++] + (data1[idx++] << 8) + (data1[idx++] << 16) + ((long)data1[idx++] << 24));
var uData = MemoryMarshal.Cast<byte, uint>(data1.Slice(idx, (int)size + 4));
idx += 4;
uint mask = uData[0];
long size1 = size;
if (size > 4)
{
uData = uData.Slice(1);
for (int i = 0; i < uData.Length; i++)
{
uData[i] = uData[i] ^ mask;
}
size1 -= uData.Length * 4;
}
if (size1 > 0)
{
int pos = (int)(idx + size - size1);
data1[pos] ^= (byte)mask;
if (size1 > 1)
{
data1[pos + 1] ^= (byte)(mask >> 8);
}
if (size1 > 2)
{
data1[pos + 2] ^= (byte)(mask >> 16);
}
; }
}
var frameData = buffer.Slice(idx, (int)size);
var frame = new WebSocketFrame { IsFinal = isFinal, Data = frameData, OpCode = opCode };
yield return frame;
buffer = buffer.Slice((int)(idx + size));
}
if (!copied && buffer.Length > 0)
{
copyToBuffer(buffer);
}
}
private Memory<byte> copyToBuffer(ReadOnlyMemory<byte> data)
{
long requiredLength = bufferLength + data.Length;
if (requiredLength > buffer.Length)
{
Array.Resize(ref buffer, (int)Math.Min(requiredLength, buffer.Length * 2));
}
data.CopyTo(buffer.AsMemory((int)bufferLength));
bufferLength += data.Length;
return buffer.AsMemory(0, (int)bufferLength);
}
private static bool isDataEnough(ReadOnlySpan<byte> data)
{
int length = data.Length;
if (length < 2)
return false;
byte size = data[1];
if ((size & 0x80) != 0) // masked
length -= 4;
size &= 0x7f;
if (size == 126)
{
if (length < 2)
{
return false;
}
}
else if (size == 127)
{
if (length < 10)
{
return false;
}
}
return length >= size;
}
}
}
using System;
using System.Text;
namespace Titanium.Web.Proxy
{
public class WebSocketFrame
{
public bool IsFinal { get; internal set; }
public WebsocketOpCode OpCode { get; internal set; }
public ReadOnlyMemory<byte> Data { get; internal set; }
public string GetText()
{
return GetText(Encoding.UTF8);
}
public string GetText(Encoding encoding)
{
#if NETSTANDARD2_1
return encoding.GetString(Data.Span);
#else
return encoding.GetString(Data.ToArray());
#endif
}
}
}
namespace Titanium.Web.Proxy
{
public enum WebsocketOpCode : byte
{
Continuation,
Text,
Binary,
ConnectionClose = 8,
Ping,
Pong,
}
}
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