Unverified Commit 80dd44e5 authored by honfika's avatar honfika Committed by GitHub

Merge pull request #353 from justcoding121/develop

beta|develop
parents 7a44e456 5fe25998
......@@ -143,7 +143,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
}
//intecept & cancel redirect or update requests
public async Task OnRequest(object sender, SessionEventArgs e)
private async Task OnRequest(object sender, SessionEventArgs e)
{
Console.WriteLine("Active Client Connections:" + ((ProxyServer)sender).ClientConnectionCount);
Console.WriteLine(e.WebSession.Request.Url);
......@@ -151,6 +151,12 @@ namespace Titanium.Web.Proxy.Examples.Basic
//read request headers
requestHeaderHistory[e.Id] = e.WebSession.Request.Headers;
////This sample shows how to get the multipart form data headers
//if (e.WebSession.Request.Host == "mail.yahoo.com" && e.WebSession.Request.IsMultipartFormData)
//{
// e.MultipartRequestPartSent += MultipartRequestPartSent;
//}
if (e.WebSession.Request.HasBody)
{
//Get/Set request body bytes
......@@ -185,7 +191,17 @@ namespace Titanium.Web.Proxy.Examples.Basic
}
//Modify response
public async Task OnResponse(object sender, SessionEventArgs e)
private void MultipartRequestPartSent(object sender, MultipartRequestPartSentEventArgs e)
{
var session = (SessionEventArgs)sender;
Console.WriteLine("Multipart form data headers:");
foreach (var header in e.Headers)
{
Console.WriteLine(header);
}
}
private async Task OnResponse(object sender, SessionEventArgs e)
{
Console.WriteLine("Active Server Connections:" + ((ProxyServer)sender).ServerConnectionCount);
......
......@@ -11,7 +11,6 @@ using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network;
namespace Titanium.Web.Proxy.Examples.Wpf
{
......@@ -63,13 +62,14 @@ namespace Titanium.Web.Proxy.Examples.Wpf
public MainWindow()
{
proxyServer = new ProxyServer();
//proxyServer.CertificateEngine = CertificateEngine.BouncyCastle;
//proxyServer.CertificateEngine = CertificateEngine.DefaultWindows;
proxyServer.TrustRootCertificate = true;
proxyServer.CertificateManager.TrustRootCertificateAsAdministrator();
proxyServer.ForwardToUpstreamGateway = true;
var explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, 8000, true)
{
ExcludedHttpsHostNameRegex = new[] { "ssllabs.com" },
//IncludedHttpsHostNameRegex = new string[0],
};
......@@ -134,10 +134,9 @@ namespace Titanium.Web.Proxy.Examples.Wpf
SessionListItem item = null;
await Dispatcher.InvokeAsync(() =>
{
if (sessionDictionary.TryGetValue(e.WebSession, out var item2))
if (sessionDictionary.TryGetValue(e.WebSession, out item))
{
item2.Update();
item = item2;
item.Update();
}
});
......@@ -147,6 +146,11 @@ namespace Titanium.Web.Proxy.Examples.Wpf
{
e.WebSession.Response.KeepBody = true;
await e.GetResponseBody();
await Dispatcher.InvokeAsync(() =>
{
item.Update();
});
}
}
}
......
......@@ -13,7 +13,7 @@ namespace Titanium.Web.Proxy.Examples.Wpf
private string protocol;
private string host;
private string url;
private long bodySize;
private long? bodySize;
private string process;
private long receivedDataCount;
private long sentDataCount;
......@@ -48,7 +48,7 @@ namespace Titanium.Web.Proxy.Examples.Wpf
set => SetField(ref url, value);
}
public long BodySize
public long? BodySize
{
get => bodySize;
set => SetField(ref bodySize, value);
......@@ -75,10 +75,13 @@ namespace Titanium.Web.Proxy.Examples.Wpf
public event PropertyChangedEventHandler PropertyChanged;
protected void SetField<T>(ref T field, T value,[CallerMemberName] string propertyName = null)
{
if (!Equals(field, value))
{
field = value;
OnPropertyChanged(propertyName);
}
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
......@@ -105,7 +108,24 @@ namespace Titanium.Web.Proxy.Examples.Wpf
Url = request.RequestUri.AbsolutePath;
}
BodySize = response?.ContentLength ?? -1;
if (!IsTunnelConnect)
{
long responseSize = -1;
if (response != null)
{
if (response.ContentLength != -1)
{
responseSize = response.ContentLength;
}
else if (response.IsBodyRead && response.Body != null)
{
responseSize = response.Body.Length;
}
}
BodySize = responseSize;
}
Process = GetProcessDescription(WebSession.ProcessId.Value);
}
......
......@@ -51,8 +51,8 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="StreamExtended, Version=1.0.110.0, Culture=neutral, PublicKeyToken=bbfa0f1d54f50043, processorArchitecture=MSIL">
<HintPath>..\..\packages\StreamExtended.1.0.110-beta\lib\net45\StreamExtended.dll</HintPath>
<Reference Include="StreamExtended, Version=1.0.132.0, Culture=neutral, PublicKeyToken=bbfa0f1d54f50043, processorArchitecture=MSIL">
<HintPath>..\..\packages\StreamExtended.1.0.132-beta\lib\net45\StreamExtended.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
......
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="StreamExtended" version="1.0.110-beta" targetFramework="net45" />
<package id="StreamExtended" version="1.0.132-beta" targetFramework="net45" />
</packages>
\ No newline at end of file
using System;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.EventArguments
{
public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e);
}
\ No newline at end of file
......@@ -3,8 +3,9 @@ using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using StreamExtended.Helpers;
using StreamExtended.Network;
using Titanium.Web.Proxy.Decompression;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Http.Responses;
......@@ -21,6 +22,8 @@ namespace Titanium.Web.Proxy.EventArguments
/// </summary>
public class SessionEventArgs : EventArgs, IDisposable
{
private static readonly byte[] emptyData = new byte[0];
/// <summary>
/// Size of Buffers used by this object
/// </summary>
......@@ -31,6 +34,8 @@ namespace Titanium.Web.Proxy.EventArguments
/// </summary>
private Func<SessionEventArgs, Task> httpResponseHandler;
private readonly Action<Exception> exceptionFunc;
/// <summary>
/// Backing field for corresponding public property
/// </summary>
......@@ -39,9 +44,9 @@ namespace Titanium.Web.Proxy.EventArguments
/// <summary>
/// Holds a reference to client
/// </summary>
internal ProxyClient ProxyClient { get; set; }
internal ProxyClient ProxyClient { get; }
internal bool HasMulipartEventSubscribers => MultipartRequestPartSent != null;
private bool hasMulipartEventSubscribers => MultipartRequestPartSent != null;
/// <summary>
/// Returns a unique Id for this request/response session
......@@ -103,10 +108,12 @@ namespace Titanium.Web.Proxy.EventArguments
/// </summary>
internal SessionEventArgs(int bufferSize,
ProxyEndPoint endPoint,
Func<SessionEventArgs, Task> httpResponseHandler)
Func<SessionEventArgs, Task> httpResponseHandler,
Action<Exception> exceptionFunc)
{
this.bufferSize = bufferSize;
this.httpResponseHandler = httpResponseHandler;
this.exceptionFunc = exceptionFunc;
ProxyClient = new ProxyClient();
WebSession = new HttpWebClient(bufferSize);
......@@ -135,42 +142,21 @@ namespace Titanium.Web.Proxy.EventArguments
/// <summary>
/// Read request body content as bytes[] for current session
/// </summary>
private async Task ReadRequestBody()
private async Task ReadRequestBodyAsync()
{
WebSession.Request.EnsureBodyAvailable(false);
//Caching check
if (!WebSession.Request.IsBodyRead)
{
//If chunked then its easy just read the whole body with the content length mentioned in the request header
using (var requestBodyStream = new MemoryStream())
{
//For chunked request we need to read data as they arrive, until we reach a chunk end symbol
if (WebSession.Request.IsChunked)
{
await ProxyClient.ClientStreamReader.CopyBytesToStreamChunked(requestBodyStream);
}
else
{
//If not chunked then its easy just read the whole body with the content length mentioned in the request header
if (WebSession.Request.ContentLength > 0)
{
//If not chunked then its easy just read the amount of bytes mentioned in content length header of response
await ProxyClient.ClientStreamReader.CopyBytesToStream(requestBodyStream, WebSession.Request.ContentLength);
}
else if (WebSession.Request.HttpVersion.Major == 1 && WebSession.Request.HttpVersion.Minor == 0)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(requestBodyStream, long.MaxValue);
}
}
var request = WebSession.Request;
WebSession.Request.Body = await GetDecompressedResponseBody(WebSession.Request.ContentEncoding, requestBodyStream.ToArray());
}
//If not already read (not cached yet)
if (!request.IsBodyRead)
{
var body = await ReadBodyAsync(ProxyClient.ClientStreamReader, request.IsChunked, request.ContentLength, request.ContentEncoding, true);
request.Body = body;
//Now set the flag to true
//So that next time we can deliver body from cache
WebSession.Request.IsBodyRead = true;
var body = WebSession.Request.Body;
request.IsBodyRead = true;
OnDataSent(body, 0, body.Length);
}
}
......@@ -181,73 +167,208 @@ namespace Titanium.Web.Proxy.EventArguments
internal async Task ClearResponse()
{
//siphon out the body
await ReadResponseBody();
await ReadResponseBodyAsync();
WebSession.Response = new Response();
}
internal void OnDataSent(byte[] buffer, int offset, int count)
{
try
{
DataSent?.Invoke(this, new DataEventArgs(buffer, offset, count));
}
catch (Exception ex)
{
var ex2 = new Exception("Exception thrown in user event", ex);
exceptionFunc(ex2);
}
}
internal void OnDataReceived(byte[] buffer, int offset, int count)
{
try
{
DataReceived?.Invoke(this, new DataEventArgs(buffer, offset, count));
}
catch (Exception ex)
{
var ex2 = new Exception("Exception thrown in user event", ex);
exceptionFunc(ex2);
}
}
internal void OnMultipartRequestPartSent(string boundary, HeaderCollection headers)
{
try
{
MultipartRequestPartSent?.Invoke(this, new MultipartRequestPartSentEventArgs(boundary, headers));
}
catch (Exception ex)
{
var ex2 = new Exception("Exception thrown in user event", ex);
exceptionFunc(ex2);
}
}
/// <summary>
/// Read response body as byte[] for current response
/// </summary>
private async Task ReadResponseBody()
private async Task ReadResponseBodyAsync()
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot read the response body before request is made to server.");
}
var response = WebSession.Response;
if (!response.HasBody)
{
return;
}
//If not already read (not cached yet)
if (!WebSession.Response.IsBodyRead)
if (!response.IsBodyRead)
{
if (WebSession.Response.HasBody)
var body = await ReadBodyAsync(WebSession.ServerConnection.StreamReader, response.IsChunked, response.ContentLength, response.ContentEncoding, false);
response.Body = body;
//Now set the flag to true
//So that next time we can deliver body from cache
response.IsBodyRead = true;
OnDataReceived(body, 0, body.Length);
}
}
private async Task<byte[]> ReadBodyAsync(CustomBinaryReader reader, bool isChunked, long contentLength, string contentEncoding, bool isRequest)
{
using (var responseBodyStream = new MemoryStream())
using (var bodyStream = new MemoryStream())
{
//If chuncked the read chunk by chunk until we hit chunk end symbol
if (WebSession.Response.IsChunked)
var writer = new HttpWriter(bodyStream, bufferSize);
if (isRequest)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStreamChunked(responseBodyStream);
await CopyRequestBodyAsync(writer, true);
}
else
{
if (WebSession.Response.ContentLength > 0)
await CopyResponseBodyAsync(writer, true);
}
return await GetDecompressedBodyAsync(contentEncoding, bodyStream.ToArray());
}
}
/// <summary>
/// This is called when the request is PUT/POST/PATCH to read the body
/// </summary>
/// <returns></returns>
internal async Task CopyRequestBodyAsync(HttpWriter writer, bool removeChunkedEncoding)
{
// End the operation
var request = WebSession.Request;
var reader = ProxyClient.ClientStreamReader;
long contentLength = request.ContentLength;
//send the request body bytes to server
if (contentLength > 0 && hasMulipartEventSubscribers && request.IsMultipartFormData)
{
string boundary = HttpHelper.GetBoundaryFromContentType(request.ContentType);
using (var copyStream = new CopyStream(reader, writer, bufferSize))
using (var copyStreamReader = new CustomBinaryReader(copyStream, bufferSize))
{
while (contentLength > copyStream.ReadBytes)
{
//If not chunked then its easy just read the amount of bytes mentioned in content length header of response
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(responseBodyStream, WebSession.Response.ContentLength);
long read = await ReadUntilBoundaryAsync(copyStreamReader, contentLength, boundary);
if (read == 0)
{
break;
}
else if (WebSession.Response.HttpVersion.Major == 1 && WebSession.Response.HttpVersion.Minor == 0 ||
WebSession.Response.ContentLength == -1)
if (contentLength > copyStream.ReadBytes)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(responseBodyStream, long.MaxValue);
var headers = new HeaderCollection();
await HeaderParser.ReadHeaders(copyStreamReader, headers);
OnMultipartRequestPartSent(boundary, headers);
}
}
WebSession.Response.Body = await GetDecompressedResponseBody(WebSession.Response.ContentEncoding, responseBodyStream.ToArray());
await copyStream.FlushAsync();
}
}
else
{
WebSession.Response.Body = new byte[0];
await writer.CopyBodyAsync(reader, request.IsChunked, contentLength, removeChunkedEncoding);
}
}
//set this to true for caching
WebSession.Response.IsBodyRead = true;
var body = WebSession.Response.Body;
OnDataReceived(body, 0, body.Length);
internal async Task CopyResponseBodyAsync(HttpWriter writer, bool removeChunkedEncoding)
{
var response = WebSession.Response;
var reader = WebSession.ServerConnection.StreamReader;
await writer.CopyBodyAsync(reader, response.IsChunked, response.ContentLength, removeChunkedEncoding);
}
/// <summary>
/// Read a line from the byte stream
/// </summary>
/// <returns></returns>
private async Task<long> ReadUntilBoundaryAsync(CustomBinaryReader reader, long totalBytesToRead, string boundary)
{
int bufferDataLength = 0;
var buffer = BufferPool.GetBuffer(bufferSize);
try
{
int boundaryLength = boundary.Length + 4;
long bytesRead = 0;
while (bytesRead < totalBytesToRead && (reader.DataAvailable || await reader.FillBufferAsync()))
{
byte newChar = reader.ReadByteFromBuffer();
buffer[bufferDataLength] = newChar;
bufferDataLength++;
bytesRead++;
if (bufferDataLength >= boundaryLength)
{
int startIdx = bufferDataLength - boundaryLength;
if (buffer[startIdx] == '-' && buffer[startIdx + 1] == '-')
{
startIdx += 2;
bool ok = true;
for (int i = 0; i < boundary.Length; i++)
{
if (buffer[startIdx + i] != boundary[i])
{
ok = false;
break;
}
}
if (ok)
{
break;
}
}
}
if (bufferDataLength == buffer.Length)
{
//boundary is not longer than 70 bytes according to the specification, so keeping the last 100 (minimum 74) bytes is enough
const int bytesToKeep = 100;
Buffer.BlockCopy(buffer, buffer.Length - bytesToKeep, buffer, 0, bytesToKeep);
bufferDataLength = bytesToKeep;
}
}
return bytesRead;
}
finally
{
BufferPool.ReturnBuffer(buffer);
}
}
......@@ -259,7 +380,7 @@ namespace Titanium.Web.Proxy.EventArguments
{
if (!WebSession.Request.IsBodyRead)
{
await ReadRequestBody();
await ReadRequestBodyAsync();
}
return WebSession.Request.Body;
......@@ -273,7 +394,7 @@ namespace Titanium.Web.Proxy.EventArguments
{
if (!WebSession.Request.IsBodyRead)
{
await ReadRequestBody();
await ReadRequestBodyAsync();
}
return WebSession.Request.BodyString;
......@@ -293,7 +414,7 @@ namespace Titanium.Web.Proxy.EventArguments
//syphon out the request body from client before setting the new body
if (!WebSession.Request.IsBodyRead)
{
await ReadRequestBody();
await ReadRequestBodyAsync();
}
WebSession.Request.Body = body;
......@@ -314,7 +435,7 @@ namespace Titanium.Web.Proxy.EventArguments
//syphon out the request body from client before setting the new body
if (!WebSession.Request.IsBodyRead)
{
await ReadRequestBody();
await ReadRequestBodyAsync();
}
await SetRequestBody(WebSession.Request.Encoding.GetBytes(body));
......@@ -328,7 +449,7 @@ namespace Titanium.Web.Proxy.EventArguments
{
if (!WebSession.Response.IsBodyRead)
{
await ReadResponseBody();
await ReadResponseBodyAsync();
}
return WebSession.Response.Body;
......@@ -342,7 +463,7 @@ namespace Titanium.Web.Proxy.EventArguments
{
if (!WebSession.Response.IsBodyRead)
{
await ReadResponseBody();
await ReadResponseBodyAsync();
}
return WebSession.Response.BodyString;
......@@ -400,12 +521,12 @@ namespace Titanium.Web.Proxy.EventArguments
await SetResponseBody(bodyBytes);
}
private async Task<byte[]> GetDecompressedResponseBody(string encodingType, byte[] responseBodyStream)
private async Task<byte[]> GetDecompressedBodyAsync(string encodingType, byte[] bodyStream)
{
var decompressionFactory = new DecompressionFactory();
var decompressor = decompressionFactory.Create(encodingType);
return await decompressor.Decompress(responseBodyStream, bufferSize);
return await decompressor.Decompress(bodyStream, bufferSize);
}
/// <summary>
......@@ -493,8 +614,8 @@ namespace Titanium.Web.Proxy.EventArguments
{
var response = new RedirectResponse();
response.HttpVersion = WebSession.Request.HttpVersion;
response.Headers.AddHeader("Location", url);
response.Body = new byte[0];
response.Headers.AddHeader(KnownHeaders.Location, url);
response.Body = emptyData;
await Respond(response);
}
......@@ -527,6 +648,10 @@ namespace Titanium.Web.Proxy.EventArguments
httpResponseHandler = null;
CustomUpStreamProxyUsed = null;
DataSent = null;
DataReceived = null;
MultipartRequestPartSent = null;
WebSession.FinishSession();
}
}
......
using Titanium.Web.Proxy.Http;
using System;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.EventArguments
......@@ -7,8 +8,8 @@ namespace Titanium.Web.Proxy.EventArguments
{
public bool IsHttpsConnect { get; set; }
internal TunnelConnectSessionEventArgs(int bufferSize, ProxyEndPoint endPoint, ConnectRequest connectRequest)
: base(bufferSize, endPoint, null)
internal TunnelConnectSessionEventArgs(int bufferSize, ProxyEndPoint endPoint, ConnectRequest connectRequest, Action<Exception> exceptionFunc)
: base(bufferSize, endPoint, null, exceptionFunc)
{
WebSession.Request = connectRequest;
}
......
using System;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
namespace Titanium.Web.Proxy.Extensions
{
internal static class FuncExtensions
{
public static void InvokeParallel<T>(this Func<object, T, Task> callback, object sender, T args)
public static void InvokeParallel<T>(this AsyncEventHandler<T> callback, object sender, T args)
{
var invocationList = callback.GetInvocationList();
var handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, T, Task>)invocationList[i])(sender, args);
handlerTasks[i] = ((AsyncEventHandler<T>)invocationList[i])(sender, args);
}
Task.WhenAll(handlerTasks).Wait();
}
public static async Task InvokeParallelAsync<T>(this Func<object, T, Task> callback, object sender, T args, Action<Exception> exceptionFunc)
public static async Task InvokeParallelAsync<T>(this AsyncEventHandler<T> callback, object sender, T args, Action<Exception> exceptionFunc)
{
var invocationList = callback.GetInvocationList();
var handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = InvokeAsync((Func<object, T, Task>)invocationList[i], sender, args, exceptionFunc);
handlerTasks[i] = InternalInvokeAsync((AsyncEventHandler<T>)invocationList[i], sender, args, exceptionFunc);
}
await Task.WhenAll(handlerTasks);
}
private static async Task InvokeAsync<T>(Func<object, T, Task> callback, object sender, T args, Action<Exception> exceptionFunc)
public static async Task InvokeAsync<T>(this AsyncEventHandler<T> callback, object sender, T args, Action<Exception> exceptionFunc)
{
var invocationList = callback.GetInvocationList();
for (int i = 0; i < invocationList.Length; i++)
{
await InternalInvokeAsync((AsyncEventHandler<T>)invocationList[i], sender, args, exceptionFunc);
}
}
private static async Task InternalInvokeAsync<T>(AsyncEventHandler<T> callback, object sender, T args, Action<Exception> exceptionFunc)
{
try
{
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StreamExtended;
namespace Titanium.Web.Proxy.Extensions
{
static class SslExtensions
{
public static string GetServerName(this ClientHelloInfo clientHelloInfo)
{
if (clientHelloInfo.Extensions != null && clientHelloInfo.Extensions.TryGetValue("server_name", out var serverNameExtension))
{
return serverNameExtension.Data;
}
return null;
}
}
}
using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
using StreamExtended.Helpers;
namespace Titanium.Web.Proxy.Extensions
{
......@@ -18,16 +18,33 @@ namespace Titanium.Web.Proxy.Extensions
/// <param name="output"></param>
/// <param name="onCopy"></param>
/// <param name="bufferSize"></param>
internal static async Task CopyToAsync(this Stream input, Stream output, Action<byte[], int, int> onCopy, int bufferSize)
internal static Task CopyToAsync(this Stream input, Stream output, Action<byte[], int, int> onCopy, int bufferSize)
{
byte[] buffer = new byte[bufferSize];
while (true)
return CopyToAsync(input, output, onCopy, bufferSize, CancellationToken.None);
}
/// <summary>
/// Copy streams asynchronously
/// </summary>
/// <param name="input"></param>
/// <param name="output"></param>
/// <param name="onCopy"></param>
/// <param name="bufferSize"></param>
/// <param name="cancellationToken"></param>
internal static async Task CopyToAsync(this Stream input, Stream output, Action<byte[], int, int> onCopy, int bufferSize, CancellationToken cancellationToken)
{
int num = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
byte[] buffer = BufferPool.GetBuffer(bufferSize);
try
{
while (!cancellationToken.IsCancellationRequested)
{
// cancellation is not working on Socket ReadAsync
// https://github.com/dotnet/corefx/issues/15033
int num = await input.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).WithCancellation(cancellationToken);
int bytesRead;
if ((bytesRead = num) != 0)
if ((bytesRead = num) != 0 && !cancellationToken.IsCancellationRequested)
{
await output.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
await output.WriteAsync(buffer, 0, bytesRead, CancellationToken.None);
onCopy?.Invoke(buffer, 0, bytesRead);
}
else
......@@ -36,65 +53,24 @@ namespace Titanium.Web.Proxy.Extensions
}
}
}
/// <summary>
/// copies the specified bytes to the stream from the input stream
/// </summary>
/// <param name="streamReader"></param>
/// <param name="stream"></param>
/// <param name="totalBytesToRead"></param>
/// <returns></returns>
internal static async Task CopyBytesToStream(this CustomBinaryReader streamReader, Stream stream, long totalBytesToRead)
{
var buffer = streamReader.Buffer;
long remainingBytes = totalBytesToRead;
while (remainingBytes > 0)
{
int bytesToRead = buffer.Length;
if (remainingBytes < bytesToRead)
finally
{
bytesToRead = (int)remainingBytes;
}
int bytesRead = await streamReader.ReadBytesAsync(buffer, bytesToRead);
if (bytesRead == 0)
{
break;
}
remainingBytes -= bytesRead;
await stream.WriteAsync(buffer, 0, bytesRead);
BufferPool.ReturnBuffer(buffer);
}
}
/// <summary>
/// Copies the stream chunked
/// </summary>
/// <param name="clientStreamReader"></param>
/// <param name="stream"></param>
/// <returns></returns>
internal static async Task CopyBytesToStreamChunked(this CustomBinaryReader clientStreamReader, Stream stream)
{
while (true)
private static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
string chuchkHead = await clientStreamReader.ReadLineAsync();
int chunkSize = int.Parse(chuchkHead, NumberStyles.HexNumber);
if (chunkSize != 0)
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
await CopyBytesToStream(clientStreamReader, stream, chunkSize);
//chunk trail
await clientStreamReader.ReadLineAsync();
}
else
if (task != await Task.WhenAny(task, tcs.Task))
{
await clientStreamReader.ReadLineAsync();
break;
return default(T);
}
}
return await task;
}
}
}
using Titanium.Web.Proxy.Helpers;
using System;
using System.Net.Sockets;
using System.Reflection;
using Titanium.Web.Proxy.Helpers;
namespace Titanium.Web.Proxy.Extensions
{
internal static class TcpExtensions
{
private static readonly Func<Socket, bool> socketCleanedUpGetter;
static TcpExtensions()
{
var property = typeof(Socket).GetProperty("CleanedUp", BindingFlags.NonPublic | BindingFlags.Instance);
if (property != null)
{
var method = property.GetMethod;
if (method != null && method.ReturnType == typeof(bool))
{
socketCleanedUpGetter = (Func<Socket, bool>)Delegate.CreateDelegate(typeof(Func<Socket, bool>), method);
}
}
}
/// <summary>
/// Gets the local port from a native TCP row object.
......@@ -24,5 +41,30 @@ namespace Titanium.Web.Proxy.Extensions
{
return (tcpRow.remotePort1 << 8) + tcpRow.remotePort2 + (tcpRow.remotePort3 << 24) + (tcpRow.remotePort4 << 16);
}
internal static void CloseSocket(this TcpClient tcpClient)
{
if (tcpClient == null)
{
return;
}
try
{
//This line is important!
//contributors please don't remove it without discussion
//It helps to avoid eventual deterioration of performance due to TCP port exhaustion
//due to default TCP CLOSE_WAIT timeout for 4 minutes
if (socketCleanedUpGetter == null || !socketCleanedUpGetter(tcpClient.Client))
{
tcpClient.LingerState = new LingerOption(true, 0);
}
tcpClient.Close();
}
catch
{
}
}
}
}
using System;
using System.Text;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Helpers
......@@ -28,7 +29,7 @@ namespace Titanium.Web.Proxy.Helpers
foreach (string parameter in parameters)
{
var split = parameter.Split(ProxyConstants.EqualSplit, 2);
if (split.Length == 2 && split[0].Trim().Equals("charset", StringComparison.CurrentCultureIgnoreCase))
if (split.Length == 2 && split[0].Trim().Equals(KnownHeaders.ContentTypeCharset, StringComparison.CurrentCultureIgnoreCase))
{
string value = split[1];
if (value.Equals("x-user-defined", StringComparison.OrdinalIgnoreCase))
......@@ -65,7 +66,7 @@ namespace Titanium.Web.Proxy.Helpers
foreach (string parameter in parameters)
{
var split = parameter.Split(ProxyConstants.EqualSplit, 2);
if (split.Length == 2 && split[0].Trim().Equals("boundary", StringComparison.CurrentCultureIgnoreCase))
if (split.Length == 2 && split[0].Trim().Equals(KnownHeaders.ContentTypeBoundary, StringComparison.CurrentCultureIgnoreCase))
{
string value = split[1];
if (value.Length > 2 && value[0] == '"' && value[value.Length - 1] == '"')
......@@ -97,6 +98,13 @@ namespace Titanium.Web.Proxy.Helpers
if (hostname.Split(ProxyConstants.DotSplit).Length > 2)
{
int idx = hostname.IndexOf(ProxyConstants.DotSplit);
//issue #352
if(hostname.Substring(0, idx).Contains("-"))
{
return hostname;
}
string rootDomain = hostname.Substring(idx + 1);
return "*." + rootDomain;
}
......
......@@ -4,8 +4,7 @@ namespace Titanium.Web.Proxy.Helpers
{
sealed class HttpRequestWriter : HttpWriter
{
public HttpRequestWriter(Stream stream, int bufferSize)
: base(stream, bufferSize, true)
public HttpRequestWriter(Stream stream, int bufferSize) : base(stream, bufferSize)
{
}
}
......
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using StreamExtended.Network;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Helpers
{
sealed class HttpResponseWriter : HttpWriter
{
public HttpResponseWriter(Stream stream, int bufferSize)
: base(stream, bufferSize, true)
public HttpResponseWriter(Stream stream, int bufferSize) : base(stream, bufferSize)
{
}
......@@ -39,102 +33,7 @@ namespace Titanium.Web.Proxy.Helpers
/// <returns></returns>
public Task WriteResponseStatusAsync(Version version, int code, string description)
{
return WriteLineAsync($"HTTP/{version.Major}.{version.Minor} {code} {description}");
}
/// <summary>
/// Writes the byte array body to the stream; optionally chunked
/// </summary>
/// <param name="data"></param>
/// <param name="isChunked"></param>
/// <returns></returns>
internal async Task WriteResponseBodyAsync(byte[] data, bool isChunked)
{
if (!isChunked)
{
await BaseStream.WriteAsync(data, 0, data.Length);
}
else
{
await WriteResponseBodyChunkedAsync(data);
}
}
/// <summary>
/// Copies the specified content length number of bytes to the output stream from the given inputs stream
/// optionally chunked
/// </summary>
/// <param name="bufferSize"></param>
/// <param name="inStreamReader"></param>
/// <param name="isChunked"></param>
/// <param name="contentLength"></param>
/// <returns></returns>
internal async Task WriteResponseBodyAsync(int bufferSize, CustomBinaryReader inStreamReader, bool isChunked,
long contentLength)
{
if (!isChunked)
{
//http 1.0
if (contentLength == -1)
{
contentLength = long.MaxValue;
}
await inStreamReader.CopyBytesToStream(BaseStream, contentLength);
}
else
{
await WriteResponseBodyChunkedAsync(inStreamReader);
}
}
/// <summary>
/// Copies the streams chunked
/// </summary>
/// <param name="inStreamReader"></param>
/// <returns></returns>
internal async Task WriteResponseBodyChunkedAsync(CustomBinaryReader inStreamReader)
{
while (true)
{
string chunkHead = await inStreamReader.ReadLineAsync();
int chunkSize = int.Parse(chunkHead, NumberStyles.HexNumber);
if (chunkSize != 0)
{
var chunkHeadBytes = Encoding.ASCII.GetBytes(chunkSize.ToString("x2"));
await BaseStream.WriteAsync(chunkHeadBytes, 0, chunkHeadBytes.Length);
await BaseStream.WriteAsync(ProxyConstants.NewLineBytes, 0, ProxyConstants.NewLineBytes.Length);
await inStreamReader.CopyBytesToStream(BaseStream, chunkSize);
await BaseStream.WriteAsync(ProxyConstants.NewLineBytes, 0, ProxyConstants.NewLineBytes.Length);
await inStreamReader.ReadLineAsync();
}
else
{
await inStreamReader.ReadLineAsync();
await BaseStream.WriteAsync(ProxyConstants.ChunkEnd, 0, ProxyConstants.ChunkEnd.Length);
break;
}
}
}
/// <summary>
/// Copies the given input bytes to output stream chunked
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
internal async Task WriteResponseBodyChunkedAsync(byte[] data)
{
var chunkHead = Encoding.ASCII.GetBytes(data.Length.ToString("x2"));
await BaseStream.WriteAsync(chunkHead, 0, chunkHead.Length);
await BaseStream.WriteAsync(ProxyConstants.NewLineBytes, 0, ProxyConstants.NewLineBytes.Length);
await BaseStream.WriteAsync(data, 0, data.Length);
await BaseStream.WriteAsync(ProxyConstants.NewLineBytes, 0, ProxyConstants.NewLineBytes.Length);
await BaseStream.WriteAsync(ProxyConstants.ChunkEnd, 0, ProxyConstants.ChunkEnd.Length);
return WriteLineAsync(Response.CreateResponseLine(version, code, description));
}
}
}
using System.IO;
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using StreamExtended.Helpers;
using StreamExtended.Network;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Helpers
{
abstract class HttpWriter : StreamWriter
internal class HttpWriter : CustomBinaryWriter
{
protected HttpWriter(Stream stream, int bufferSize, bool leaveOpen)
: base(stream, Encoding.ASCII, bufferSize, leaveOpen)
public int BufferSize { get; }
private readonly char[] charBuffer;
private static readonly byte[] newLine = ProxyConstants.NewLine;
private static readonly Encoder encoder = Encoding.ASCII.GetEncoder();
internal HttpWriter(Stream stream, int bufferSize) : base(stream)
{
NewLine = ProxyConstants.NewLine;
BufferSize = bufferSize;
// ASCII encoder max byte count is char count + 1
charBuffer = new char[BufferSize - 1];
}
public void WriteHeaders(HeaderCollection headers, bool flush = true)
public Task WriteLineAsync()
{
if (headers != null)
return WriteAsync(newLine);
}
public async Task WriteAsync(string value)
{
foreach (var header in headers)
int charCount = value.Length;
value.CopyTo(0, charBuffer, 0, charCount);
if (charCount < BufferSize)
{
header.WriteToStream(this);
var buffer = BufferPool.GetBuffer(BufferSize);
try
{
int idx = encoder.GetBytes(charBuffer, 0, charCount, buffer, 0, true);
await WriteAsync(buffer, 0, idx);
}
finally
{
BufferPool.ReturnBuffer(buffer);
}
WriteLine();
if (flush)
}
else
{
Flush();
var buffer = new byte[charCount + 1];
int idx = encoder.GetBytes(charBuffer, 0, charCount, buffer, 0, true);
await WriteAsync(buffer, 0, idx);
}
}
public async Task WriteLineAsync(string value)
{
await WriteAsync(value);
await WriteLineAsync();
}
/// <summary>
/// Write the headers to client
/// </summary>
......@@ -50,8 +84,161 @@ namespace Titanium.Web.Proxy.Helpers
await WriteLineAsync();
if (flush)
{
// flush the stream
await FlushAsync();
}
}
public async Task WriteAsync(byte[] data, bool flush = false)
{
await WriteAsync(data, 0, data.Length);
if (flush)
{
// flush the stream
await FlushAsync();
}
}
public async Task WriteAsync(byte[] data, int offset, int count, bool flush)
{
await WriteAsync(data, offset, count);
if (flush)
{
// flush the stream
await FlushAsync();
}
}
/// <summary>
/// Writes the byte array body to the stream; optionally chunked
/// </summary>
/// <param name="data"></param>
/// <param name="isChunked"></param>
/// <returns></returns>
internal Task WriteBodyAsync(byte[] data, bool isChunked)
{
if (isChunked)
{
return WriteBodyChunkedAsync(data);
}
return WriteAsync(data);
}
/// <summary>
/// Copies the specified content length number of bytes to the output stream from the given inputs stream
/// optionally chunked
/// </summary>
/// <param name="streamReader"></param>
/// <param name="isChunked"></param>
/// <param name="contentLength"></param>
/// <param name="removeChunkedEncoding"></param>
/// <returns></returns>
internal Task CopyBodyAsync(CustomBinaryReader streamReader, bool isChunked, long contentLength, bool removeChunkedEncoding)
{
//For chunked request we need to read data as they arrive, until we reach a chunk end symbol
if (isChunked)
{
//Need to revist, find any potential bugs
//send the body bytes to server in chunks
return CopyBodyChunkedAsync(streamReader, removeChunkedEncoding);
}
//http 1.0
if (contentLength == -1)
{
contentLength = long.MaxValue;
}
//If not chunked then its easy just read the amount of bytes mentioned in content length header
return CopyBytesFromStream(streamReader, contentLength);
}
/// <summary>
/// Copies the given input bytes to output stream chunked
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
private async Task WriteBodyChunkedAsync(byte[] data)
{
var chunkHead = Encoding.ASCII.GetBytes(data.Length.ToString("x2"));
await WriteAsync(chunkHead);
await WriteLineAsync();
await WriteAsync(data);
await WriteLineAsync();
await WriteLineAsync("0");
await WriteLineAsync();
}
/// <summary>
/// Copies the streams chunked
/// </summary>
/// <param name="reader"></param>
/// <param name="removeChunkedEncoding"></param>
/// <returns></returns>
private async Task CopyBodyChunkedAsync(CustomBinaryReader reader, bool removeChunkedEncoding)
{
while (true)
{
string chunkHead = await reader.ReadLineAsync();
int chunkSize = int.Parse(chunkHead, NumberStyles.HexNumber);
if (!removeChunkedEncoding)
{
await WriteLineAsync(chunkHead);
}
if (chunkSize != 0)
{
await CopyBytesFromStream(reader, chunkSize);
}
if (!removeChunkedEncoding)
{
await WriteLineAsync();
}
//chunk trail
await reader.ReadLineAsync();
if (chunkSize == 0)
{
break;
}
}
}
/// <summary>
/// Copies the specified bytes to the stream from the input stream
/// </summary>
/// <param name="reader"></param>
/// <param name="count"></param>
/// <returns></returns>
private async Task CopyBytesFromStream(CustomBinaryReader reader, long count)
{
var buffer = reader.Buffer;
long remainingBytes = count;
while (remainingBytes > 0)
{
int bytesToRead = buffer.Length;
if (remainingBytes < bytesToRead)
{
bytesToRead = (int)remainingBytes;
}
int bytesRead = await reader.ReadBytesAsync(buffer, bytesToRead);
if (bytesRead == 0)
{
break;
}
remainingBytes -= bytesRead;
await WriteAsync(buffer, 0, bytesRead);
}
}
}
}
......@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Network.Tcp;
......@@ -111,16 +112,21 @@ namespace Titanium.Web.Proxy.Helpers
/// </summary>
/// <param name="clientStream"></param>
/// <param name="serverStream"></param>
/// <param name="bufferSize"></param>
/// <param name="onDataSend"></param>
/// <param name="onDataReceive"></param>
/// <returns></returns>
internal static async Task SendRaw(Stream clientStream, Stream serverStream, int bufferSize,
Action<byte[], int, int> onDataSend, Action<byte[], int, int> onDataReceive)
{
var cts = new CancellationTokenSource();
//Now async relay all server=>client & client=>server data
var sendRelay = clientStream.CopyToAsync(serverStream, onDataSend, bufferSize);
var sendRelay = clientStream.CopyToAsync(serverStream, onDataSend, bufferSize, cts.Token);
var receiveRelay = serverStream.CopyToAsync(clientStream, onDataReceive, bufferSize, cts.Token);
var receiveRelay = serverStream.CopyToAsync(clientStream, onDataReceive, bufferSize);
await Task.WhenAny(sendRelay, receiveRelay);
cts.Cancel();
await Task.WhenAll(sendRelay, receiveRelay);
}
......
......@@ -22,7 +22,7 @@ namespace Titanium.Web.Proxy.Http
StatusDescription = "Connection Established"
};
response.Headers.AddHeader("Timestamp", DateTime.Now.ToString());
response.Headers.AddHeader(KnownHeaders.Timestamp, DateTime.Now.ToString());
return response;
}
}
......
......@@ -63,6 +63,21 @@ namespace Titanium.Web.Proxy.Http
return null;
}
public HttpHeader GetFirstHeader(string name)
{
if (Headers.TryGetValue(name, out var header))
{
return header;
}
if (NonUniqueHeaders.TryGetValue(name, out var headers))
{
return headers.FirstOrDefault();
}
return null;
}
/// <summary>
/// Returns all headers
/// </summary>
......@@ -235,7 +250,7 @@ namespace Titanium.Web.Proxy.Http
return null;
}
internal string SetOrAddHeaderValue(string headerName, string value)
internal void SetOrAddHeaderValue(string headerName, string value)
{
if (Headers.TryGetValue(headerName, out var header))
{
......@@ -245,8 +260,6 @@ namespace Titanium.Web.Proxy.Http
{
Headers.Add(headerName, new HttpHeader(headerName, value));
}
return null;
}
/// <summary>
......@@ -255,12 +268,12 @@ namespace Titanium.Web.Proxy.Http
internal void FixProxyHeaders()
{
//If proxy-connection close was returned inform to close the connection
string proxyHeader = GetHeaderValueOrNull("proxy-connection");
RemoveHeader("proxy-connection");
string proxyHeader = GetHeaderValueOrNull(KnownHeaders.ProxyConnection);
RemoveHeader(KnownHeaders.ProxyConnection);
if (proxyHeader != null)
{
SetOrAddHeaderValue("connection", proxyHeader);
SetOrAddHeaderValue(KnownHeaders.Connection, proxyHeader);
}
}
......
......@@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network.Tcp;
......@@ -80,26 +79,17 @@ namespace Titanium.Web.Proxy.Http
/// </summary>
/// <returns></returns>
internal async Task SendRequest(bool enable100ContinueBehaviour)
{
var stream = ServerConnection.Stream;
byte[] requestBytes;
using (var ms = new MemoryStream())
using (var writer = new HttpRequestWriter(ms, bufferSize))
{
var upstreamProxy = ServerConnection.UpStreamProxy;
bool useUpstreamProxy = upstreamProxy != null && ServerConnection.IsHttps == false;
var writer = ServerConnection.StreamWriter;
//prepare the request & headers
if (useUpstreamProxy)
{
writer.WriteLine($"{Request.Method} {Request.OriginalUrl} HTTP/{Request.HttpVersion.Major}.{Request.HttpVersion.Minor}");
}
else
{
writer.WriteLine($"{Request.Method} {Request.RequestUri.PathAndQuery} HTTP/{Request.HttpVersion.Major}.{Request.HttpVersion.Minor}");
}
await writer.WriteLineAsync(Request.CreateRequestLine(Request.Method,
useUpstreamProxy ? Request.OriginalUrl : Request.RequestUri.PathAndQuery,
Request.HttpVersion));
//Send Authentication to Upstream proxy if needed
......@@ -108,27 +98,20 @@ namespace Titanium.Web.Proxy.Http
&& !string.IsNullOrEmpty(upstreamProxy.UserName)
&& upstreamProxy.Password != null)
{
HttpHeader.ProxyConnectionKeepAlive.WriteToStream(writer);
HttpHeader.GetProxyAuthorizationHeader(upstreamProxy.UserName, upstreamProxy.Password).WriteToStream(writer);
await HttpHeader.ProxyConnectionKeepAlive.WriteToStreamAsync(writer);
await HttpHeader.GetProxyAuthorizationHeader(upstreamProxy.UserName, upstreamProxy.Password).WriteToStreamAsync(writer);
}
//write request headers
foreach (var header in Request.Headers)
{
if (header.Name != "Proxy-Authorization")
if (header.Name != KnownHeaders.ProxyAuthorization)
{
header.WriteToStream(writer);
}
await header.WriteToStreamAsync(writer);
}
writer.WriteLine();
writer.Flush();
requestBytes = ms.ToArray();
}
await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
await stream.FlushAsync();
await writer.WriteLineAsync();
if (enable100ContinueBehaviour)
{
......@@ -191,6 +174,7 @@ namespace Titanium.Web.Proxy.Http
Response.Is100Continue = true;
Response.StatusCode = 0;
await ServerConnection.StreamReader.ReadLineAsync();
//now receive response
await ReceiveResponse();
return;
......@@ -203,6 +187,7 @@ namespace Titanium.Web.Proxy.Http
Response.ExpectationFailed = true;
Response.StatusCode = 0;
await ServerConnection.StreamReader.ReadLineAsync();
//now receive response
await ReceiveResponse();
return;
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Http
{
public static class KnownHeaders
{
// Both
public const string Connection = "connection";
public const string ConnectionClose = "close";
public const string ConnectionKeepAlive = "keep-alive";
public const string ContentLength = "content-length";
public const string ContentType = "content-type";
public const string ContentTypeCharset = "charset";
public const string ContentTypeBoundary = "boundary";
public const string Upgrade = "upgrade";
public const string UpgradeWebsocket = "websocket";
// Request headers
public const string AcceptEncoding = "accept-encoding";
public const string Authorization = "Authorization";
public const string Expect = "expect";
public const string Expect100Continue = "100-continue";
public const string Host = "host";
public const string ProxyAuthorization = "Proxy-Authorization";
public const string ProxyConnection = "Proxy-Connection";
public const string ProxyConnectionClose = "close";
// Response headers
public const string ContentEncoding = "content-encoding";
public const string Location = "Location";
public const string ProxyAuthenticate = "Proxy-Authenticate";
public const string TransferEncoding = "transfer-encoding";
public const string TransferEncodingChunked = "chunked";
// ???
public const string Timestamp = "Timestamp";
}
}
......@@ -67,14 +67,14 @@ namespace Titanium.Web.Proxy.Http
/// </summary>
public string Host
{
get => Headers.GetHeaderValueOrNull("host");
set => Headers.SetOrAddHeaderValue("host", value);
get => Headers.GetHeaderValueOrNull(KnownHeaders.Host);
set => Headers.SetOrAddHeaderValue(KnownHeaders.Host, value);
}
/// <summary>
/// Content encoding header value
/// </summary>
public string ContentEncoding => Headers.GetHeaderValueOrNull("content-encoding");
public string ContentEncoding => Headers.GetHeaderValueOrNull(KnownHeaders.ContentEncoding);
/// <summary>
/// Request content-length
......@@ -83,7 +83,7 @@ namespace Titanium.Web.Proxy.Http
{
get
{
string headerValue = Headers.GetHeaderValueOrNull("content-length");
string headerValue = Headers.GetHeaderValueOrNull(KnownHeaders.ContentLength);
if (headerValue == null)
{
......@@ -102,12 +102,12 @@ namespace Titanium.Web.Proxy.Http
{
if (value >= 0)
{
Headers.SetOrAddHeaderValue("content-length", value.ToString());
Headers.SetOrAddHeaderValue(KnownHeaders.ContentLength, value.ToString());
IsChunked = false;
}
else
{
Headers.RemoveHeader("content-length");
Headers.RemoveHeader(KnownHeaders.ContentLength);
}
}
}
......@@ -117,8 +117,8 @@ namespace Titanium.Web.Proxy.Http
/// </summary>
public string ContentType
{
get => Headers.GetHeaderValueOrNull("content-type");
set => Headers.SetOrAddHeaderValue("content-type", value);
get => Headers.GetHeaderValueOrNull(KnownHeaders.ContentType);
set => Headers.SetOrAddHeaderValue(KnownHeaders.ContentType, value);
}
/// <summary>
......@@ -128,19 +128,19 @@ namespace Titanium.Web.Proxy.Http
{
get
{
string headerValue = Headers.GetHeaderValueOrNull("transfer-encoding");
return headerValue != null && headerValue.ContainsIgnoreCase("chunked");
string headerValue = Headers.GetHeaderValueOrNull(KnownHeaders.TransferEncoding);
return headerValue != null && headerValue.ContainsIgnoreCase(KnownHeaders.TransferEncodingChunked);
}
set
{
if (value)
{
Headers.SetOrAddHeaderValue("transfer-encoding", "chunked");
Headers.SetOrAddHeaderValue(KnownHeaders.TransferEncoding, KnownHeaders.TransferEncodingChunked);
ContentLength = -1;
}
else
{
Headers.RemoveHeader("transfer-encoding");
Headers.RemoveHeader(KnownHeaders.TransferEncoding);
}
}
}
......@@ -152,8 +152,8 @@ namespace Titanium.Web.Proxy.Http
{
get
{
string headerValue = Headers.GetHeaderValueOrNull("expect");
return headerValue != null && headerValue.Equals("100-continue");
string headerValue = Headers.GetHeaderValueOrNull(KnownHeaders.Expect);
return headerValue != null && headerValue.Equals(KnownHeaders.Expect100Continue);
}
}
......@@ -241,14 +241,14 @@ namespace Titanium.Web.Proxy.Http
{
get
{
string headerValue = Headers.GetHeaderValueOrNull("upgrade");
string headerValue = Headers.GetHeaderValueOrNull(KnownHeaders.Upgrade);
if (headerValue == null)
{
return false;
}
return headerValue.Equals("websocket", StringComparison.CurrentCultureIgnoreCase);
return headerValue.Equals(KnownHeaders.UpgradeWebsocket, StringComparison.CurrentCultureIgnoreCase);
}
}
......@@ -275,7 +275,7 @@ namespace Titanium.Web.Proxy.Http
get
{
var sb = new StringBuilder();
sb.AppendLine($"{Method} {OriginalUrl} HTTP/{HttpVersion.Major}.{HttpVersion.Minor}");
sb.AppendLine(CreateRequestLine(Method, OriginalUrl, HttpVersion));
foreach (var header in Headers)
{
sb.AppendLine(header.ToString());
......@@ -286,6 +286,11 @@ namespace Titanium.Web.Proxy.Http
}
}
internal static string CreateRequestLine(string httpMethod, string httpUrl, Version version)
{
return $"{httpMethod} {httpUrl} HTTP/{version.Major}.{version.Minor}";
}
internal static void ParseRequestLine(string httpCmd, out string httpMethod, out string httpUrl, out Version version)
{
//break up the line into three components (method, remote URL & Http Version)
......
......@@ -42,12 +42,12 @@ namespace Titanium.Web.Proxy.Http
/// <summary>
/// Content encoding for this response
/// </summary>
public string ContentEncoding => Headers.GetHeaderValueOrNull("content-encoding")?.Trim();
public string ContentEncoding => Headers.GetHeaderValueOrNull(KnownHeaders.ContentEncoding)?.Trim();
/// <summary>
/// Http version
/// </summary>
public Version HttpVersion { get; set; }
public Version HttpVersion { get; set; } = HttpHeader.VersionUnknown;
/// <summary>
/// Keeps the response body data after the session is finished
......@@ -61,16 +61,24 @@ namespace Titanium.Web.Proxy.Http
{
get
{
long contentLength = ContentLength;
//If content length is set to 0 the response has no body
if (contentLength == 0)
{
return false;
}
//Has body only if response is chunked or content length >0
//If none are true then check if connection:close header exist, if so write response until server or client terminates the connection
if (IsChunked || ContentLength > 0 || !KeepAlive)
if (IsChunked || contentLength > 0 || !KeepAlive)
{
return true;
}
//has response if connection:keep-alive header exist and when version is http/1.0
//Because in Http 1.0 server can return a response without content-length (expectation being client would read until end of stream)
if (KeepAlive && HttpVersion.Minor == 0)
if (KeepAlive && HttpVersion == HttpHeader.Version10)
{
return true;
}
......@@ -86,11 +94,11 @@ namespace Titanium.Web.Proxy.Http
{
get
{
string headerValue = Headers.GetHeaderValueOrNull("connection");
string headerValue = Headers.GetHeaderValueOrNull(KnownHeaders.Connection);
if (headerValue != null)
{
if (headerValue.ContainsIgnoreCase("close"))
if (headerValue.ContainsIgnoreCase(KnownHeaders.ConnectionClose))
{
return false;
}
......@@ -103,7 +111,7 @@ namespace Titanium.Web.Proxy.Http
/// <summary>
/// Content type of this response
/// </summary>
public string ContentType => Headers.GetHeaderValueOrNull("content-type");
public string ContentType => Headers.GetHeaderValueOrNull(KnownHeaders.ContentType);
/// <summary>
/// Length of response body
......@@ -112,15 +120,14 @@ namespace Titanium.Web.Proxy.Http
{
get
{
string headerValue = Headers.GetHeaderValueOrNull("content-length");
string headerValue = Headers.GetHeaderValueOrNull(KnownHeaders.ContentLength);
if (headerValue == null)
{
return -1;
}
long.TryParse(headerValue, out long contentLen);
if (contentLen >= 0)
if (long.TryParse(headerValue, out long contentLen))
{
return contentLen;
}
......@@ -131,12 +138,12 @@ namespace Titanium.Web.Proxy.Http
{
if (value >= 0)
{
Headers.SetOrAddHeaderValue("content-length", value.ToString());
Headers.SetOrAddHeaderValue(KnownHeaders.ContentLength, value.ToString());
IsChunked = false;
}
else
{
Headers.RemoveHeader("content-length");
Headers.RemoveHeader(KnownHeaders.ContentLength);
}
}
}
......@@ -148,19 +155,19 @@ namespace Titanium.Web.Proxy.Http
{
get
{
string headerValue = Headers.GetHeaderValueOrNull("transfer-encoding");
return headerValue != null && headerValue.ContainsIgnoreCase("chunked");
string headerValue = Headers.GetHeaderValueOrNull(KnownHeaders.TransferEncoding);
return headerValue != null && headerValue.ContainsIgnoreCase(KnownHeaders.TransferEncodingChunked);
}
set
{
if (value)
{
Headers.SetOrAddHeaderValue("transfer-encoding", "chunked");
Headers.SetOrAddHeaderValue(KnownHeaders.TransferEncoding, KnownHeaders.TransferEncodingChunked);
ContentLength = -1;
}
else
{
Headers.RemoveHeader("transfer-encoding");
Headers.RemoveHeader(KnownHeaders.TransferEncoding);
}
}
}
......@@ -228,7 +235,7 @@ namespace Titanium.Web.Proxy.Http
/// <summary>
/// Gets the resposne status.
/// </summary>
public string Status => $"HTTP/{HttpVersion?.Major}.{HttpVersion?.Minor} {StatusCode} {StatusDescription}";
public string Status => CreateResponseLine(HttpVersion, StatusCode, StatusDescription);
/// <summary>
/// Gets the header text.
......@@ -249,6 +256,11 @@ namespace Titanium.Web.Proxy.Http
}
}
internal static string CreateResponseLine(Version version, int statusCode, string statusDescription)
{
return $"HTTP/{version.Major}.{version.Minor} {statusCode} {statusDescription}";
}
internal static void ParseResponseLine(string httpStatus, out Version version, out int statusCode, out string statusDescription)
{
var httpResult = httpStatus.Split(ProxyConstants.SpaceSplit, 3);
......
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
namespace Titanium.Web.Proxy.Models
{
......@@ -10,6 +11,8 @@ namespace Titanium.Web.Proxy.Models
/// </summary>
public class HttpHeader
{
internal static readonly Version VersionUnknown = new Version(0, 0);
internal static readonly Version Version10 = new Version(1, 0);
internal static readonly Version Version11 = new Version(1, 1);
......@@ -54,19 +57,12 @@ namespace Titanium.Web.Proxy.Models
internal static HttpHeader GetProxyAuthorizationHeader(string userName, string password)
{
var result = new HttpHeader("Proxy-Authorization",
var result = new HttpHeader(KnownHeaders.ProxyAuthorization,
"Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{userName}:{password}")));
return result;
}
internal void WriteToStream(StreamWriter writer)
{
writer.Write(Name);
writer.Write(": ");
writer.WriteLine(Value);
}
internal async Task WriteToStreamAsync(StreamWriter writer)
internal async Task WriteToStreamAsync(HttpWriter writer)
{
await writer.WriteAsync(Name);
await writer.WriteAsync(": ");
......
#if DEBUG
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
namespace Titanium.Web.Proxy.Network
{
class DebugCustomBufferedStream : CustomBufferedStream
{
private const string basePath = @".";
private static int counter;
public int Counter { get; }
private readonly FileStream fileStreamSent;
private readonly FileStream fileStreamReceived;
public DebugCustomBufferedStream(Stream baseStream, int bufferSize) : base(baseStream, bufferSize)
{
Counter = Interlocked.Increment(ref counter);
fileStreamSent = new FileStream(Path.Combine(basePath, $"{Counter}_sent.dat"), FileMode.Create);
fileStreamReceived = new FileStream(Path.Combine(basePath, $"{Counter}_received.dat"), FileMode.Create);
}
protected override void OnDataSent(byte[] buffer, int offset, int count)
{
fileStreamSent.Write(buffer, offset, count);
}
protected override void OnDataReceived(byte[] buffer, int offset, int count)
{
fileStreamReceived.Write(buffer, offset, count);
}
public override void Flush()
{
fileStreamSent.Flush(true);
fileStreamReceived.Flush(true);
base.Flush();
}
}
}
#endif
\ No newline at end of file
......@@ -15,7 +15,7 @@ namespace Titanium.Web.Proxy.Network
internal TcpClient TcpClient { get; set; }
/// <summary>
/// holds the stream to client
/// Holds the stream to client
/// </summary>
internal CustomBufferedStream ClientStream { get; set; }
......@@ -25,7 +25,7 @@ namespace Titanium.Web.Proxy.Network
internal CustomBinaryReader ClientStreamReader { get; set; }
/// <summary>
/// used to write line by line to client
/// Used to write line by line to client
/// </summary>
internal HttpResponseWriter ClientStreamWriter { get; set; }
}
......
......@@ -2,6 +2,8 @@
using System;
using System.Net;
using System.Net.Sockets;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Network.Tcp
......@@ -11,6 +13,8 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// </summary>
internal class TcpConnection : IDisposable
{
private ProxyServer proxyServer { get; }
internal ExternalProxy UpStreamProxy { get; set; }
internal string HostName { get; set; }
......@@ -34,10 +38,15 @@ namespace Titanium.Web.Proxy.Network.Tcp
internal TcpClient TcpClient { private get; set; }
/// <summary>
/// used to read lines from server
/// Used to read lines from server
/// </summary>
internal CustomBinaryReader StreamReader { get; set; }
/// <summary>
/// Used to write lines to server
/// </summary>
internal HttpRequestWriter StreamWriter { get; set; }
/// <summary>
/// Server stream
/// </summary>
......@@ -48,9 +57,11 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// </summary>
internal DateTime LastAccess { get; set; }
internal TcpConnection()
internal TcpConnection(ProxyServer proxyServer)
{
LastAccess = DateTime.Now;
this.proxyServer = proxyServer;
this.proxyServer.UpdateServerConnectionCount(true);
}
/// <summary>
......@@ -58,25 +69,13 @@ namespace Titanium.Web.Proxy.Network.Tcp
/// </summary>
public void Dispose()
{
StreamReader?.Dispose();
Stream?.Dispose();
StreamReader?.Dispose();
TcpClient.CloseSocket();
try
{
if (TcpClient != null)
{
//This line is important!
//contributors please don't remove it without discussion
//It helps to avoid eventual deterioration of performance due to TCP port exhaustion
//due to default TCP CLOSE_WAIT timeout for 4 minutes
TcpClient.LingerState = new LingerOption(true, 0);
TcpClient.Close();
}
}
catch
{
}
proxyServer.UpdateServerConnectionCount(false);
}
}
}
......@@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Network.Tcp
......@@ -68,23 +69,20 @@ namespace Titanium.Web.Proxy.Network.Tcp
if (useUpstreamProxy && (isConnect || isHttps))
{
using (var writer = new HttpRequestWriter(stream, server.BufferSize))
{
var writer = new HttpRequestWriter(stream, server.BufferSize);
await writer.WriteLineAsync($"CONNECT {remoteHostName}:{remotePort} HTTP/{httpVersion}");
await writer.WriteLineAsync($"Host: {remoteHostName}:{remotePort}");
await writer.WriteLineAsync("Connection: Keep-Alive");
await writer.WriteLineAsync($"{KnownHeaders.Connection}: {KnownHeaders.ConnectionKeepAlive}");
if (!string.IsNullOrEmpty(externalProxy.UserName) && externalProxy.Password != null)
{
await HttpHeader.ProxyConnectionKeepAlive.WriteToStreamAsync(writer);
await writer.WriteLineAsync("Proxy-Authorization" + ": Basic " +
await writer.WriteLineAsync(KnownHeaders.ProxyAuthorization + ": Basic " +
Convert.ToBase64String(Encoding.UTF8.GetBytes(
externalProxy.UserName + ":" + externalProxy.Password)));
}
await writer.WriteLineAsync();
await writer.FlushAsync();
}
using (var reader = new CustomBinaryReader(stream, server.BufferSize))
{
......@@ -118,9 +116,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
throw;
}
server.UpdateServerConnectionCount(true);
return new TcpConnection
return new TcpConnection(server)
{
UpStreamProxy = externalProxy,
UpStreamEndPoint = upStreamEndPoint,
......@@ -130,6 +126,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
UseUpstreamProxy = useUpstreamProxy,
TcpClient = client,
StreamReader = new CustomBinaryReader(stream, server.BufferSize),
StreamWriter = new HttpRequestWriter(stream, server.BufferSize),
Stream = stream,
Version = httpVersion
};
......
......@@ -8,6 +8,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.Shared;
namespace Titanium.Web.Proxy
{
......@@ -20,37 +21,36 @@ namespace Titanium.Web.Proxy
return true;
}
var httpHeaders = session.WebSession.Request.Headers.ToArray();
var httpHeaders = session.WebSession.Request.Headers;
try
{
var header = httpHeaders.FirstOrDefault(t => t.Name == "Proxy-Authorization");
var header = httpHeaders.GetFirstHeader(KnownHeaders.ProxyAuthorization);
if (header == null)
{
session.WebSession.Response = await SendAuthentication407Response(clientStreamWriter, "Proxy Authentication Required");
return false;
}
string headerValue = header.Value.Trim();
if (!headerValue.StartsWith("basic", StringComparison.CurrentCultureIgnoreCase))
var headerValueParts = header.Value.Split(ProxyConstants.SpaceSplit);
if (headerValueParts.Length != 2 || !headerValueParts[0].Equals("basic", StringComparison.CurrentCultureIgnoreCase))
{
//Return not authorized
session.WebSession.Response = await SendAuthentication407Response(clientStreamWriter, "Proxy Authentication Invalid");
return false;
}
headerValue = headerValue.Substring(5).Trim();
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue));
if (decoded.Contains(":") == false)
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(headerValueParts[1]));
int colonIndex = decoded.IndexOf(':');
if (colonIndex == -1)
{
//Return not authorized
session.WebSession.Response = await SendAuthentication407Response(clientStreamWriter, "Proxy Authentication Invalid");
return false;
}
string username = decoded.Substring(0, decoded.IndexOf(':'));
string password = decoded.Substring(decoded.IndexOf(':') + 1);
string username = decoded.Substring(0, colonIndex);
string password = decoded.Substring(colonIndex + 1);
return await AuthenticateUserFunc(username, password);
}
catch (Exception e)
......@@ -72,8 +72,8 @@ namespace Titanium.Web.Proxy
StatusDescription = description
};
response.Headers.AddHeader("Proxy-Authenticate", $"Basic realm=\"{ProxyRealm}\"");
response.Headers.AddHeader("Proxy-Connection", "close");
response.Headers.AddHeader(KnownHeaders.ProxyAuthenticate, $"Basic realm=\"{ProxyRealm}\"");
response.Headers.AddHeader(KnownHeaders.ProxyConnection, KnownHeaders.ProxyConnectionClose);
await clientStreamWriter.WriteResponseAsync(response);
return response;
......
......@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Helpers.WinHttp;
using Titanium.Web.Proxy.Models;
......@@ -164,22 +165,22 @@ namespace Titanium.Web.Proxy
/// <summary>
/// Intercept request to server
/// </summary>
public event Func<object, SessionEventArgs, Task> BeforeRequest;
public event AsyncEventHandler<SessionEventArgs> BeforeRequest;
/// <summary>
/// Intercept response from server
/// </summary>
public event Func<object, SessionEventArgs, Task> BeforeResponse;
public event AsyncEventHandler<SessionEventArgs> BeforeResponse;
/// <summary>
/// Intercept tunnel connect reques
/// </summary>
public event Func<object, TunnelConnectSessionEventArgs, Task> TunnelConnectRequest;
public event AsyncEventHandler<TunnelConnectSessionEventArgs> TunnelConnectRequest;
/// <summary>
/// Intercept tunnel connect response
/// </summary>
public event Func<object, TunnelConnectSessionEventArgs, Task> TunnelConnectResponse;
public event AsyncEventHandler<TunnelConnectSessionEventArgs> TunnelConnectResponse;
/// <summary>
/// Occurs when client connection count changed.
......@@ -229,12 +230,12 @@ namespace Titanium.Web.Proxy
/// <summary>
/// Verifies the remote Secure Sockets Layer (SSL) certificate used for authentication
/// </summary>
public event Func<object, CertificateValidationEventArgs, Task> ServerCertificateValidationCallback;
public event AsyncEventHandler<CertificateValidationEventArgs> ServerCertificateValidationCallback;
/// <summary>
/// Callback tooverride client certificate during SSL mutual authentication
/// </summary>
public event Func<object, CertificateSelectionEventArgs, Task> ClientCertificateSelectionCallback;
public event AsyncEventHandler<CertificateSelectionEventArgs> ClientCertificateSelectionCallback;
/// <summary>
/// Callback for error events in proxy
......@@ -612,16 +613,14 @@ namespace Titanium.Web.Proxy
/// <param name="serverConnection"></param>
private void Dispose(CustomBufferedStream clientStream, CustomBinaryReader clientStreamReader, HttpResponseWriter clientStreamWriter, TcpConnection serverConnection)
{
clientStream?.Dispose();
clientStreamReader?.Dispose();
clientStreamWriter?.Dispose();
clientStream?.Dispose();
if (serverConnection != null)
{
serverConnection.Dispose();
serverConnection = null;
UpdateServerConnectionCount(false);
}
}
......@@ -766,22 +765,7 @@ namespace Titanium.Web.Proxy
finally
{
UpdateClientConnectionCount(false);
try
{
if (tcpClient != null)
{
//This line is important!
//contributors please don't remove it without discussion
//It helps to avoid eventual deterioration of performance due to TCP port exhaustion
//due to default TCP CLOSE_WAIT timeout for 4 minutes
tcpClient.LingerState = new LingerOption(true, 0);
tcpClient.Close();
}
}
catch
{
}
tcpClient.CloseSocket();
}
}
......
......@@ -49,7 +49,6 @@ namespace Titanium.Web.Proxy
{
//read the first line HTTP command
string httpCmd = await clientStreamReader.ReadLineAsync();
if (string.IsNullOrEmpty(httpCmd))
{
return;
......@@ -86,20 +85,20 @@ namespace Titanium.Web.Proxy
await HeaderParser.ReadHeaders(clientStreamReader, connectRequest.Headers);
var connectArgs = new TunnelConnectSessionEventArgs(BufferSize, endPoint, connectRequest);
var connectArgs = new TunnelConnectSessionEventArgs(BufferSize, endPoint, connectRequest, ExceptionFunc);
connectArgs.ProxyClient.TcpClient = tcpClient;
connectArgs.ProxyClient.ClientStream = clientStream;
if (TunnelConnectRequest != null)
{
await TunnelConnectRequest.InvokeParallelAsync(this, connectArgs, ExceptionFunc);
await TunnelConnectRequest.InvokeAsync(this, connectArgs, ExceptionFunc);
}
if (await CheckAuthorization(clientStreamWriter, connectArgs) == false)
{
if (TunnelConnectResponse != null)
{
await TunnelConnectResponse.InvokeParallelAsync(this, connectArgs, ExceptionFunc);
await TunnelConnectResponse.InvokeAsync(this, connectArgs, ExceptionFunc);
}
return;
......@@ -119,7 +118,7 @@ namespace Titanium.Web.Proxy
if (TunnelConnectResponse != null)
{
connectArgs.IsHttpsConnect = isClientHello;
await TunnelConnectResponse.InvokeParallelAsync(this, connectArgs, ExceptionFunc);
await TunnelConnectResponse.InvokeAsync(this, connectArgs, ExceptionFunc);
}
if (!excluded && isClientHello)
......@@ -153,26 +152,42 @@ namespace Titanium.Web.Proxy
return;
}
if (await CanBeHttpMethod(clientStream))
{
//Now read the actual HTTPS request line
httpCmd = await clientStreamReader.ReadLineAsync();
}
//Hostname is excluded or it is not an HTTPS connect
else
{
// It can be for example some Google (Cloude Messaging for Chrome) magic
excluded = true;
}
}
//Hostname is excluded or it is not an HTTPS connect
if (excluded || !isClientHello)
{
//create new connection
using (var connection = await GetServerConnection(connectArgs, true))
{
try
{
if (isClientHello)
{
if (clientStream.Available > 0)
int available = clientStream.Available;
if (available > 0)
{
//send the buffered data
var data = new byte[clientStream.Available];
await clientStream.ReadAsync(data, 0, data.Length);
await connection.Stream.WriteAsync(data, 0, data.Length);
await connection.Stream.FlushAsync();
var data = BufferPool.GetBuffer(BufferSize);
try
{
// clientStream.Available sbould be at most BufferSize because it is using the same buffer size
await clientStream.ReadAsync(data, 0, available);
await connection.StreamWriter.WriteAsync(data, 0, available, true);
}
finally
{
BufferPool.ReturnBuffer(data);
}
}
var serverHelloInfo = await SslTools.PeekServerHello(connection.Stream);
......@@ -180,12 +195,8 @@ namespace Titanium.Web.Proxy
}
await TcpHelper.SendRaw(clientStream, connection.Stream, BufferSize,
(buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); });
}
finally
{
UpdateServerConnectionCount(false);
}
(buffer, offset, count) => { connectArgs.OnDataSent(buffer, offset, count); },
(buffer, offset, count) => { connectArgs.OnDataReceived(buffer, offset, count); });
}
return;
......@@ -196,8 +207,17 @@ namespace Titanium.Web.Proxy
disposed = await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
httpRemoteUri.Scheme == UriSchemeHttps ? httpRemoteUri.Host : null, endPoint, connectRequest);
}
catch (IOException e)
{
ExceptionFunc(new Exception("Connection was aborted", e));
}
catch (SocketException e)
{
ExceptionFunc(new Exception("Could not connect", e));
}
catch (Exception e)
{
// is this the correct error message?
ExceptionFunc(new Exception("Error whilst authorizing request", e));
}
finally
......@@ -209,6 +229,34 @@ namespace Titanium.Web.Proxy
}
}
private async Task<bool> CanBeHttpMethod(CustomBufferedStream clientStream)
{
int legthToCheck = 10;
for (int i = 0; i < legthToCheck; i++)
{
int b = await clientStream.PeekByteAsync(i);
if (b == -1)
{
return false;
}
if (b == ' ' && i > 2)
{
// at least 3 letters and a space
return true;
}
if (!char.IsLetter((char)b))
{
// non letter or too short
return false;
}
}
// only letters
return true;
}
/// <summary>
/// This is called when this proxy acts as a reverse proxy (like a real http server)
/// So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client
......@@ -228,18 +276,14 @@ namespace Titanium.Web.Proxy
{
if (endPoint.EnableSsl)
{
var clientSslHelloInfo = await SslTools.PeekClientHello(clientStream);
var clientHelloInfo = await SslTools.PeekClientHello(clientStream);
if (clientSslHelloInfo != null)
if (clientHelloInfo != null)
{
var sslStream = new SslStream(clientStream);
clientStream = new CustomBufferedStream(sslStream, BufferSize);
string sniHostName = null;
if (clientSslHelloInfo.Extensions != null && clientSslHelloInfo.Extensions.TryGetValue("server_name", out var serverNameExtension))
{
sniHostName = serverNameExtension.Data;
}
string sniHostName = clientHelloInfo.GetServerName();
string certName = HttpHelper.GetWildCardDomainName(sniHostName ?? endPoint.GenericCertificateName);
var certificate = CertificateManager.CreateCertificate(certName, false);
......@@ -302,7 +346,7 @@ namespace Titanium.Web.Proxy
break;
}
var args = new SessionEventArgs(BufferSize, endPoint, HandleHttpSessionResponse)
var args = new SessionEventArgs(BufferSize, endPoint, HandleHttpSessionResponse, ExceptionFunc)
{
ProxyClient = { TcpClient = client },
WebSession = { ConnectRequest = connectRequest }
......@@ -349,7 +393,7 @@ namespace Titanium.Web.Proxy
//If user requested interception do it
if (BeforeRequest != null)
{
await BeforeRequest.InvokeParallelAsync(this, args, ExceptionFunc);
await BeforeRequest.InvokeAsync(this, args, ExceptionFunc);
}
if (args.WebSession.Request.CancelRequest)
......@@ -366,7 +410,6 @@ namespace Titanium.Web.Proxy
{
connection.Dispose();
connection = null;
UpdateServerConnectionCount(false);
}
if (connection == null)
......@@ -379,16 +422,8 @@ namespace Titanium.Web.Proxy
{
//prepare the prefix content
var requestHeaders = args.WebSession.Request.Headers;
byte[] requestBytes;
using (var ms = new MemoryStream())
using (var writer = new HttpRequestWriter(ms, BufferSize))
{
writer.WriteLine(httpCmd);
writer.WriteHeaders(requestHeaders);
requestBytes = ms.ToArray();
}
await connection.Stream.WriteAsync(requestBytes, 0, requestBytes.Length);
await connection.StreamWriter.WriteLineAsync(httpCmd);
await connection.StreamWriter.WriteHeadersAsync(requestHeaders);
string httpStatus = await connection.StreamReader.ReadLineAsync();
Response.ParseResponseLine(httpStatus, out var responseVersion, out int responseStatusCode, out string responseStatusDescription);
......@@ -403,11 +438,12 @@ namespace Titanium.Web.Proxy
//If user requested call back then do it
if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked)
{
await BeforeResponse.InvokeParallelAsync(this, args, ExceptionFunc);
await BeforeResponse.InvokeAsync(this, args, ExceptionFunc);
}
await TcpHelper.SendRaw(clientStream, connection.Stream, BufferSize,
(buffer, offset, count) => { args.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { args.OnDataReceived(buffer, offset, count); });
(buffer, offset, count) => { args.OnDataSent(buffer, offset, count); },
(buffer, offset, count) => { args.OnDataReceived(buffer, offset, count); });
args.Dispose();
break;
......@@ -478,15 +514,17 @@ namespace Titanium.Web.Proxy
//If 100 continue was the response inform that to the client
if (Enable100ContinueBehaviour)
{
var clientStreamWriter = args.ProxyClient.ClientStreamWriter;
if (request.Is100Continue)
{
await args.ProxyClient.ClientStreamWriter.WriteResponseStatusAsync(args.WebSession.Response.HttpVersion, (int)HttpStatusCode.Continue, "Continue");
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
await clientStreamWriter.WriteResponseStatusAsync(args.WebSession.Response.HttpVersion, (int)HttpStatusCode.Continue, "Continue");
await clientStreamWriter.WriteLineAsync();
}
else if (request.ExpectationFailed)
{
await args.ProxyClient.ClientStreamWriter.WriteResponseStatusAsync(args.WebSession.Response.HttpVersion, (int)HttpStatusCode.ExpectationFailed, "Expectation Failed");
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
await clientStreamWriter.WriteResponseStatusAsync(args.WebSession.Response.HttpVersion, (int)HttpStatusCode.ExpectationFailed, "Expectation Failed");
await clientStreamWriter.WriteLineAsync();
}
}
......@@ -513,8 +551,7 @@ namespace Titanium.Web.Proxy
//chunked send is not supported as of now
request.ContentLength = body.Length;
var newStream = args.WebSession.ServerConnection.Stream;
await newStream.WriteAsync(body, 0, body.Length);
await args.WebSession.ServerConnection.StreamWriter.WriteAsync(body);
}
else
{
......@@ -523,7 +560,8 @@ namespace Titanium.Web.Proxy
//If its a post/put/patch request, then read the client html body and send it to server
if (request.HasBody)
{
await SendClientRequestBody(args);
HttpWriter writer = args.WebSession.ServerConnection.StreamWriter;
await args.CopyRequestBodyAsync(writer, false);
}
}
}
......@@ -609,7 +647,7 @@ namespace Titanium.Web.Proxy
switch (header.Name.ToLower())
{
//these are the only encoding this proxy can read
case "accept-encoding":
case KnownHeaders.AcceptEncoding:
header.Value = "gzip,deflate";
break;
}
......@@ -617,118 +655,5 @@ namespace Titanium.Web.Proxy
requestHeaders.FixProxyHeaders();
}
/// <summary>
/// This is called when the request is PUT/POST/PATCH to read the body
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private async Task SendClientRequestBody(SessionEventArgs args)
{
// End the operation
var postStream = args.WebSession.ServerConnection.Stream;
//send the request body bytes to server
if (args.WebSession.Request.ContentLength > 0)
{
var request = args.WebSession.Request;
if (args.HasMulipartEventSubscribers && request.IsMultipartFormData)
{
string boundary = HttpHelper.GetBoundaryFromContentType(request.ContentType);
long bytesToSend = args.WebSession.Request.ContentLength;
while (bytesToSend > 0)
{
long read = await SendUntilBoundaryAsync(args.ProxyClient.ClientStream, postStream, bytesToSend, boundary);
if (read == 0)
{
break;
}
bytesToSend -= read;
if (bytesToSend > 0)
{
var headers = new HeaderCollection();
var stream = new CustomBufferedPeekStream(args.ProxyClient.ClientStream);
await HeaderParser.ReadHeaders(new CustomBinaryReader(stream, BufferSize), headers);
args.OnMultipartRequestPartSent(boundary, headers);
}
}
}
else
{
await args.ProxyClient.ClientStreamReader.CopyBytesToStream(postStream, args.WebSession.Request.ContentLength);
}
}
//Need to revist, find any potential bugs
//send the request body bytes to server in chunks
else if (args.WebSession.Request.IsChunked)
{
await args.ProxyClient.ClientStreamReader.CopyBytesToStreamChunked(postStream);
}
}
/// <summary>
/// Read a line from the byte stream
/// </summary>
/// <returns></returns>
public async Task<long> SendUntilBoundaryAsync(CustomBufferedStream inputStream, CustomBufferedStream outputStream, long totalBytesToRead, string boundary)
{
int bufferDataLength = 0;
// try to use the thread static buffer
var buffer = BufferPool.GetBuffer(BufferSize);
int boundaryLength = boundary.Length + 4;
long bytesRead = 0;
while (bytesRead < totalBytesToRead && (inputStream.DataAvailable || await inputStream.FillBufferAsync()))
{
byte newChar = inputStream.ReadByteFromBuffer();
buffer[bufferDataLength] = newChar;
bufferDataLength++;
bytesRead++;
if (bufferDataLength >= boundaryLength)
{
int startIdx = bufferDataLength - boundaryLength;
if (buffer[startIdx] == '-' && buffer[startIdx + 1] == '-'
&& buffer[bufferDataLength - 2] == '\r' && buffer[bufferDataLength - 1] == '\n')
{
startIdx += 2;
bool ok = true;
for (int i = 0; i < boundary.Length; i++)
{
if (buffer[startIdx + i] != boundary[i])
{
ok = false;
break;
}
}
if (ok)
{
break;
}
}
}
if (bufferDataLength == buffer.Length)
{
//boundary is not longer than 70 bytes accounrding to the specification, so keeping the last 100 (minimum 74) bytes is enough
const int bytesToKeep = 100;
await outputStream.WriteAsync(buffer, 0, buffer.Length - bytesToKeep);
Buffer.BlockCopy(buffer, buffer.Length - bytesToKeep, buffer, 0, bytesToKeep);
bufferDataLength = bytesToKeep;
}
}
if (bytesRead > 0)
{
await outputStream.WriteAsync(buffer, 0, bufferDataLength);
}
return bytesRead;
}
}
}
......@@ -43,7 +43,7 @@ namespace Titanium.Web.Proxy
//If user requested call back then do it
if (BeforeResponse != null && !response.ResponseLocked)
{
await BeforeResponse.InvokeParallelAsync(this, args, ExceptionFunc);
await BeforeResponse.InvokeAsync(this, args, ExceptionFunc);
}
//if user requested to send request again
......@@ -96,7 +96,7 @@ namespace Titanium.Web.Proxy
}
await clientStreamWriter.WriteHeadersAsync(response.Headers);
await clientStreamWriter.WriteResponseBodyAsync(response.Body, isChunked);
await clientStreamWriter.WriteBodyAsync(response.Body, isChunked);
}
else
{
......@@ -105,12 +105,9 @@ namespace Titanium.Web.Proxy
//Write body if exists
if (response.HasBody)
{
await clientStreamWriter.WriteResponseBodyAsync(BufferSize, args.WebSession.ServerConnection.StreamReader,
response.IsChunked, response.ContentLength);
await args.CopyResponseBodyAsync(clientStreamWriter, false);
}
}
await clientStreamWriter.FlushAsync();
}
catch (Exception e)
{
......
using System.Text;
namespace Titanium.Web.Proxy.Shared
namespace Titanium.Web.Proxy.Shared
{
/// <summary>
/// Literals shared by Proxy Server
......@@ -14,10 +12,6 @@ namespace Titanium.Web.Proxy.Shared
internal static readonly char[] SemiColonSplit = { ';' };
internal static readonly char[] EqualSplit = { '=' };
internal static readonly byte[] NewLineBytes = Encoding.ASCII.GetBytes(NewLine);
internal static readonly byte[] ChunkEnd = Encoding.ASCII.GetBytes(0.ToString("x2") + NewLine + NewLine);
internal const string NewLine = "\r\n";
internal static readonly byte[] NewLine = {(byte)'\r', (byte)'\n' };
}
}
......@@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Portable.BouncyCastle" Version="1.8.1.3" />
<PackageReference Include="StreamExtended" Version="1.0.110-beta" />
<PackageReference Include="StreamExtended" Version="1.0.132-beta" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
......@@ -21,7 +21,7 @@
<Version>4.4.0</Version>
</PackageReference>
<PackageReference Include="System.Security.Principal.Windows">
<Version>4.4.0</Version>
<Version>4.4.1</Version>
</PackageReference>
</ItemGroup>
......
......@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network.WinAuth;
......@@ -84,9 +85,9 @@ namespace Titanium.Web.Proxy
string scheme = authSchemes.FirstOrDefault(x => authHeader.Value.Equals(x, StringComparison.OrdinalIgnoreCase));
//clear any existing headers to avoid confusing bad servers
if (args.WebSession.Request.Headers.NonUniqueHeaders.ContainsKey("Authorization"))
if (args.WebSession.Request.Headers.NonUniqueHeaders.ContainsKey(KnownHeaders.Authorization))
{
args.WebSession.Request.Headers.NonUniqueHeaders.Remove("Authorization");
args.WebSession.Request.Headers.NonUniqueHeaders.Remove(KnownHeaders.Authorization);
}
//initial value will match exactly any of the schemes
......@@ -94,16 +95,16 @@ namespace Titanium.Web.Proxy
{
string clientToken = WinAuthHandler.GetInitialAuthToken(args.WebSession.Request.Host, scheme, args.Id);
var auth = new HttpHeader("Authorization", string.Concat(scheme, clientToken));
var auth = new HttpHeader(KnownHeaders.Authorization, string.Concat(scheme, clientToken));
//replace existing authorization header if any
if (args.WebSession.Request.Headers.Headers.ContainsKey("Authorization"))
if (args.WebSession.Request.Headers.Headers.ContainsKey(KnownHeaders.Authorization))
{
args.WebSession.Request.Headers.Headers["Authorization"] = auth;
args.WebSession.Request.Headers.Headers[KnownHeaders.Authorization] = auth;
}
else
{
args.WebSession.Request.Headers.Headers.Add("Authorization", auth);
args.WebSession.Request.Headers.Headers.Add(KnownHeaders.Authorization, auth);
}
//don't need to send body for Authorization request
......@@ -122,7 +123,7 @@ namespace Titanium.Web.Proxy
string clientToken = WinAuthHandler.GetFinalAuthToken(args.WebSession.Request.Host, serverToken, args.Id);
//there will be an existing header from initial client request
args.WebSession.Request.Headers.Headers["Authorization"] = new HttpHeader("Authorization", string.Concat(scheme, clientToken));
args.WebSession.Request.Headers.Headers[KnownHeaders.Authorization] = new HttpHeader(KnownHeaders.Authorization, string.Concat(scheme, clientToken));
//send body for final auth request
if (args.WebSession.Request.HasBody)
......
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