Commit 82326b01 authored by Jehonathan's avatar Jehonathan Committed by GitHub

Merge pull request #82 from justcoding121/master

sync with master
parents e1cd5fcd 703f5219
......@@ -15,6 +15,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
ProxyServer.BeforeRequest += OnRequest;
ProxyServer.BeforeResponse += OnResponse;
ProxyServer.ServerCertificateValidationCallback += OnCertificateValidation;
ProxyServer.ClientCertificateSelectionCallback += OnCertificateSelection;
//Exclude Https addresses you don't want to proxy
//Usefull for clients that use certificate pinning
......@@ -129,13 +130,25 @@ namespace Titanium.Web.Proxy.Examples.Basic
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public async Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
{
//set IsValid to true/false based on Certificate Errors
if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
e.IsValid = true;
else
await e.Session.Ok("Cannot validate server certificate! Not safe to proceed.");
return Task.FromResult(0);
}
/// <summary>
/// Allows overriding default client certificate selection logic during mutual authentication
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs e)
{
//set e.clientCertificate to override
return Task.FromResult(0);
}
}
}
\ No newline at end of file
......@@ -12,8 +12,11 @@ Features
========
* Supports Http(s) and most features of HTTP 1.1
* Supports relaying of WebSockets
* Supports script injection
* Support redirect/block/update requests
* Supports updating response
* Safely relays WebSocket requests over Http
* Support mutual SSL authentication
* Fully asynchronous proxy
Usage
=====
......@@ -35,6 +38,8 @@ Setup HTTP proxy:
ProxyServer.BeforeRequest += OnRequest;
ProxyServer.BeforeResponse += OnResponse;
ProxyServer.ServerCertificateValidationCallback += OnCertificateValidation;
ProxyServer.ClientCertificateSelectionCallback += OnCertificateSelection;
//Exclude Https addresses you don't want to proxy
//Usefull for clients that use certificate pinning
......@@ -84,9 +89,7 @@ Setup HTTP proxy:
```
Sample request and response event handlers
```csharp
//intecept & cancel, redirect or update requests
```csharp
public async Task OnRequest(object sender, SessionEventArgs e)
{
Console.WriteLine(e.WebSession.Request.Url);
......@@ -148,20 +151,27 @@ Sample request and response event handlers
}
}
/// Allows overriding default certificate validation logic
public async Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
/// Allows overriding default certificate validation logic
public Task OnCertificateValidation(object sender, CertificateValidationEventArgs e)
{
//set IsValid to true/false based on Certificate Errors
if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
e.IsValid = true;
else
await e.Session.Ok("Cannot validate server certificate! Not safe to proceed.");
return Task.FromResult(0);
}
/// Allows overriding default client certificate selection logic during mutual authentication
public Task OnCertificateSelection(object sender, CertificateSelectionEventArgs e)
{
//set e.clientCertificate to override
return Task.FromResult(0);
}
```
Future roadmap
============
* Support mutual authentication
* Implement Kerberos/NTLM authentication over HTTP protocols for windows domain
* Support Server Name Indication (SNI) for transparent endpoints
* Support HTTP 2.0
using System;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
namespace Titanium.Web.Proxy
{
public partial class ProxyServer
{
/// <summary>
/// Call back to override server certificate validation
/// </summary>
/// <param name="sender"></param>
/// <param name="certificate"></param>
/// <param name="chain"></param>
/// <param name="sslPolicyErrors"></param>
/// <returns></returns>
internal static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
//if user callback is registered then do it
if (ServerCertificateValidationCallback != null)
{
var args = new CertificateValidationEventArgs();
args.Certificate = certificate;
args.Chain = chain;
args.SslPolicyErrors = sslPolicyErrors;
Delegate[] invocationList = ServerCertificateValidationCallback.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, CertificateValidationEventArgs, Task>)invocationList[i])(null, args);
}
Task.WhenAll(handlerTasks).Wait();
return args.IsValid;
}
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
//By default
//do not allow this client to communicate with unauthenticated servers.
return false;
}
/// <summary>
/// Call back to select client certificate used for mutual authentication
/// </summary>
/// <param name="sender"></param>
/// <param name="certificate"></param>
/// <param name="chain"></param>
/// <param name="sslPolicyErrors"></param>
/// <returns></returns>
internal static X509Certificate SelectClientCertificate(
object sender,
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers)
{
X509Certificate clientCertificate = null;
var customSslStream = sender as SslStream;
if (acceptableIssuers != null &&
acceptableIssuers.Length > 0 &&
localCertificates != null &&
localCertificates.Count > 0)
{
// Use the first certificate that is from an acceptable issuer.
foreach (X509Certificate certificate in localCertificates)
{
string issuer = certificate.Issuer;
if (Array.IndexOf(acceptableIssuers, issuer) != -1)
clientCertificate = certificate;
}
}
if (localCertificates != null &&
localCertificates.Count > 0)
clientCertificate = localCertificates[0];
//If user call back is registered
if (ClientCertificateSelectionCallback != null)
{
var args = new CertificateSelectionEventArgs();
args.targetHost = targetHost;
args.localCertificates = localCertificates;
args.remoteCertificate = remoteCertificate;
args.acceptableIssuers = acceptableIssuers;
args.clientCertificate = clientCertificate;
Delegate[] invocationList = ClientCertificateSelectionCallback.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, CertificateSelectionEventArgs, Task>)invocationList[i])(null, args);
}
Task.WhenAll(handlerTasks).Wait();
return args.clientCertificate;
}
return clientCertificate;
}
}
}
namespace Titanium.Web.Proxy.Compression
{
class CompressionFactory
/// <summary>
/// A factory to generate the compression methods based on the type of compression
/// </summary>
internal class CompressionFactory
{
public ICompression Create(string type)
{
......
......@@ -4,7 +4,10 @@ using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Compression
{
class DeflateCompression : ICompression
/// <summary>
/// Concrete implementation of deflate compression
/// </summary>
internal class DeflateCompression : ICompression
{
public async Task<byte[]> Compress(byte[] responseBody)
{
......@@ -12,7 +15,7 @@ namespace Titanium.Web.Proxy.Compression
{
using (var zip = new DeflateStream(ms, CompressionMode.Compress, true))
{
await zip.WriteAsync(responseBody, 0, responseBody.Length).ConfigureAwait(false);
await zip.WriteAsync(responseBody, 0, responseBody.Length);
}
return ms.ToArray();
......
......@@ -4,7 +4,10 @@ using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Compression
{
class GZipCompression : ICompression
/// <summary>
/// concreate implementation of gzip compression
/// </summary>
internal class GZipCompression : ICompression
{
public async Task<byte[]> Compress(byte[] responseBody)
{
......@@ -12,7 +15,7 @@ namespace Titanium.Web.Proxy.Compression
{
using (var zip = new GZipStream(ms, CompressionMode.Compress, true))
{
await zip.WriteAsync(responseBody, 0, responseBody.Length).ConfigureAwait(false);
await zip.WriteAsync(responseBody, 0, responseBody.Length);
}
return ms.ToArray();
......
......@@ -2,6 +2,9 @@
namespace Titanium.Web.Proxy.Compression
{
/// <summary>
/// An inteface for http compression
/// </summary>
interface ICompression
{
Task<byte[]> Compress(byte[] responseBody);
......
......@@ -4,7 +4,10 @@ using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Compression
{
class ZlibCompression : ICompression
/// <summary>
/// concrete implementation of zlib compression
/// </summary>
internal class ZlibCompression : ICompression
{
public async Task<byte[]> Compress(byte[] responseBody)
{
......@@ -12,7 +15,7 @@ namespace Titanium.Web.Proxy.Compression
{
using (var zip = new ZlibStream(ms, CompressionMode.Compress, true))
{
await zip.WriteAsync(responseBody, 0, responseBody.Length).ConfigureAwait(false);
await zip.WriteAsync(responseBody, 0, responseBody.Length);
}
return ms.ToArray();
......
namespace Titanium.Web.Proxy.Decompression
{
class DecompressionFactory
/// <summary>
/// A factory to generate the de-compression methods based on the type of compression
/// </summary>
internal class DecompressionFactory
{
public IDecompression Create(string type)
internal IDecompression Create(string type)
{
switch(type)
{
......
......@@ -2,7 +2,11 @@
namespace Titanium.Web.Proxy.Decompression
{
class DefaultDecompression : IDecompression
/// <summary>
/// When no compression is specified just return the byte array
/// </summary>
internal class DefaultDecompression : IDecompression
{
public Task<byte[]> Decompress(byte[] compressedArray)
{
......
......@@ -5,7 +5,10 @@ using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Decompression
{
class DeflateDecompression : IDecompression
/// <summary>
/// concrete implementation of deflate de-compression
/// </summary>
internal class DeflateDecompression : IDecompression
{
public async Task<byte[]> Decompress(byte[] compressedArray)
{
......@@ -13,14 +16,14 @@ namespace Titanium.Web.Proxy.Decompression
using (var decompressor = new DeflateStream(stream, CompressionMode.Decompress))
{
var buffer = new byte[Constants.BUFFER_SIZE];
var buffer = new byte[ProxyConstants.BUFFER_SIZE];
using (var output = new MemoryStream())
{
int read;
while ((read = await decompressor.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
while ((read = await decompressor.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await output.WriteAsync(buffer, 0, read).ConfigureAwait(false);
await output.WriteAsync(buffer, 0, read);
}
return output.ToArray();
......
......@@ -5,19 +5,22 @@ using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Decompression
{
class GZipDecompression : IDecompression
/// <summary>
/// concrete implementation of gzip de-compression
/// </summary>
internal class GZipDecompression : IDecompression
{
public async Task<byte[]> Decompress(byte[] compressedArray)
{
using (var decompressor = new GZipStream(new MemoryStream(compressedArray), CompressionMode.Decompress))
{
var buffer = new byte[Constants.BUFFER_SIZE];
var buffer = new byte[ProxyConstants.BUFFER_SIZE];
using (var output = new MemoryStream())
{
int read;
while ((read = await decompressor.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
while ((read = await decompressor.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await output.WriteAsync(buffer, 0, read).ConfigureAwait(false);
await output.WriteAsync(buffer, 0, read);
}
return output.ToArray();
}
......
using System.IO;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Decompression
{
interface IDecompression
/// <summary>
/// An interface for decompression
/// </summary>
internal interface IDecompression
{
Task<byte[]> Decompress(byte[] compressedArray);
Task<byte[]> Decompress(byte[] compressedArray);
}
}
......@@ -5,21 +5,24 @@ using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Decompression
{
class ZlibDecompression : IDecompression
/// <summary>
/// concrete implemetation of zlib de-compression
/// </summary>
internal class ZlibDecompression : IDecompression
{
public async Task<byte[]> Decompress(byte[] compressedArray)
{
var memoryStream = new MemoryStream(compressedArray);
using (var decompressor = new ZlibStream(memoryStream, CompressionMode.Decompress))
{
var buffer = new byte[Constants.BUFFER_SIZE];
var buffer = new byte[ProxyConstants.BUFFER_SIZE];
using (var output = new MemoryStream())
{
int read;
while ((read = await decompressor.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
while ((read = await decompressor.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await output.WriteAsync(buffer, 0, read).ConfigureAwait(false);
await output.WriteAsync(buffer, 0, read);
}
return output.ToArray();
}
......
using System;
using System.Security.Cryptography.X509Certificates;
namespace Titanium.Web.Proxy.EventArguments
{
/// <summary>
/// An argument passed on to user for client certificate selection during mutual SSL authentication
/// </summary>
public class CertificateSelectionEventArgs : EventArgs, IDisposable
{
public object sender { get; internal set; }
public string targetHost { get; internal set; }
public X509CertificateCollection localCertificates { get; internal set; }
public X509Certificate remoteCertificate { get; internal set; }
public string[] acceptableIssuers { get; internal set; }
public X509Certificate clientCertificate { get; set; }
public void Dispose()
{
throw new NotImplementedException();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Http;
namespace Titanium.Web.Proxy.EventArguments
{
/// <summary>
/// An argument passed on to the user for validating the server certificate during SSL authentication
/// </summary>
public class CertificateValidationEventArgs : EventArgs, IDisposable
{
public string HostName => Session.WebSession.Request.Host;
public SessionEventArgs Session { get; internal set; }
public X509Certificate Certificate { get; internal set; }
public X509Chain Chain { get; internal set; }
public SslPolicyErrors SslPolicyErrors { get; internal set; }
......
......@@ -8,6 +8,7 @@ using Titanium.Web.Proxy.Http.Responses;
using Titanium.Web.Proxy.Extensions;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Network;
using System.Net;
namespace Titanium.Web.Proxy.EventArguments
{
......@@ -24,25 +25,28 @@ namespace Titanium.Web.Proxy.EventArguments
/// </summary>
internal SessionEventArgs()
{
Client = new ProxyClient();
WebSession = new HttpWebSession();
ProxyClient = new ProxyClient();
WebSession = new HttpWebClient();
}
/// <summary>
/// Holds a reference to server connection
/// Holds a reference to client
/// </summary>
internal ProxyClient Client { get; set; }
internal ProxyClient ProxyClient { get; set; }
/// <summary>
/// Does this session uses SSL
/// </summary>
public bool IsHttps { get; internal set; }
public bool IsHttps => WebSession.Request.RequestUri.Scheme == Uri.UriSchemeHttps;
public IPEndPoint ClientEndPoint => (IPEndPoint)ProxyClient.TcpClient.Client.RemoteEndPoint;
/// <summary>
/// A web session corresponding to a single request/response sequence
/// within a proxy connection
/// </summary>
public HttpWebSession WebSession { get; set; }
public HttpWebClient WebSession { get; set; }
/// <summary>
......@@ -76,7 +80,7 @@ namespace Titanium.Web.Proxy.EventArguments
//For chunked request we need to read data as they arrive, until we reach a chunk end symbol
if (WebSession.Request.IsChunked)
{
await this.Client.ClientStreamReader.CopyBytesToStreamChunked(requestBodyStream).ConfigureAwait(false);
await this.ProxyClient.ClientStreamReader.CopyBytesToStreamChunked(requestBodyStream);
}
else
{
......@@ -84,13 +88,13 @@ namespace Titanium.Web.Proxy.EventArguments
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 this.Client.ClientStreamReader.CopyBytesToStream(requestBodyStream, WebSession.Request.ContentLength).ConfigureAwait(false);
await this.ProxyClient.ClientStreamReader.CopyBytesToStream(requestBodyStream, WebSession.Request.ContentLength);
}
else if (WebSession.Request.HttpVersion.ToLower().Trim().Equals("http/1.0"))
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(requestBodyStream, long.MaxValue).ConfigureAwait(false);
else if(WebSession.Request.HttpVersion.Major == 1 && WebSession.Request.HttpVersion.Minor == 0)
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(requestBodyStream, long.MaxValue);
}
WebSession.Request.RequestBody = await GetDecompressedResponseBody(WebSession.Request.ContentEncoding, requestBodyStream.ToArray()).ConfigureAwait(false);
WebSession.Request.RequestBody = await GetDecompressedResponseBody(WebSession.Request.ContentEncoding, requestBodyStream.ToArray());
}
//Now set the flag to true
......@@ -113,21 +117,21 @@ namespace Titanium.Web.Proxy.EventArguments
//If chuncked the read chunk by chunk until we hit chunk end symbol
if (WebSession.Response.IsChunked)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStreamChunked(responseBodyStream).ConfigureAwait(false);
await WebSession.ServerConnection.StreamReader.CopyBytesToStreamChunked(responseBodyStream);
}
else
{
if (WebSession.Response.ContentLength > 0)
{
//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).ConfigureAwait(false);
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(responseBodyStream, WebSession.Response.ContentLength);
}
else if(WebSession.Response.HttpVersion.ToLower().Trim().Equals("http/1.0"))
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(responseBodyStream, long.MaxValue).ConfigureAwait(false);
else if(WebSession.Response.HttpVersion.Major == 1 && WebSession.Response.HttpVersion.Minor == 0)
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(responseBodyStream, long.MaxValue);
}
WebSession.Response.ResponseBody = await GetDecompressedResponseBody(WebSession.Response.ContentEncoding, responseBodyStream.ToArray()).ConfigureAwait(false);
WebSession.Response.ResponseBody = await GetDecompressedResponseBody(WebSession.Response.ContentEncoding, responseBodyStream.ToArray());
}
//set this to true for caching
......@@ -144,7 +148,7 @@ namespace Titanium.Web.Proxy.EventArguments
if (WebSession.Request.RequestLocked)
throw new Exception("You cannot call this function after request is made to server.");
await ReadRequestBody().ConfigureAwait(false);
await ReadRequestBody();
return WebSession.Request.RequestBody;
}
/// <summary>
......@@ -157,7 +161,7 @@ namespace Titanium.Web.Proxy.EventArguments
throw new Exception("You cannot call this function after request is made to server.");
await ReadRequestBody().ConfigureAwait(false);
await ReadRequestBody();
//Use the encoding specified in request to decode the byte[] data to string
return WebSession.Request.RequestBodyString ?? (WebSession.Request.RequestBodyString = WebSession.Request.Encoding.GetString(WebSession.Request.RequestBody));
......@@ -175,7 +179,7 @@ namespace Titanium.Web.Proxy.EventArguments
//syphon out the request body from client before setting the new body
if (!WebSession.Request.RequestBodyRead)
{
await ReadRequestBody().ConfigureAwait(false);
await ReadRequestBody();
}
WebSession.Request.RequestBody = body;
......@@ -198,7 +202,7 @@ namespace Titanium.Web.Proxy.EventArguments
//syphon out the request body from client before setting the new body
if (!WebSession.Request.RequestBodyRead)
{
await ReadRequestBody().ConfigureAwait(false);
await ReadRequestBody();
}
await SetRequestBody(WebSession.Request.Encoding.GetBytes(body));
......@@ -214,7 +218,7 @@ namespace Titanium.Web.Proxy.EventArguments
if (!WebSession.Request.RequestLocked)
throw new Exception("You cannot call this function before request is made to server.");
await ReadResponseBody().ConfigureAwait(false);
await ReadResponseBody();
return WebSession.Response.ResponseBody;
}
......@@ -227,7 +231,7 @@ namespace Titanium.Web.Proxy.EventArguments
if (!WebSession.Request.RequestLocked)
throw new Exception("You cannot call this function before request is made to server.");
await GetResponseBody().ConfigureAwait(false);
await GetResponseBody();
return WebSession.Response.ResponseBodyString ?? (WebSession.Response.ResponseBodyString = WebSession.Response.Encoding.GetString(WebSession.Response.ResponseBody));
}
......@@ -244,7 +248,7 @@ namespace Titanium.Web.Proxy.EventArguments
//syphon out the response body from server before setting the new body
if (WebSession.Response.ResponseBody == null)
{
await GetResponseBody().ConfigureAwait(false);
await GetResponseBody();
}
WebSession.Response.ResponseBody = body;
......@@ -268,12 +272,12 @@ namespace Titanium.Web.Proxy.EventArguments
//syphon out the response body from server before setting the new body
if (WebSession.Response.ResponseBody == null)
{
await GetResponseBody().ConfigureAwait(false);
await GetResponseBody();
}
var bodyBytes = WebSession.Response.Encoding.GetBytes(body);
await SetResponseBody(bodyBytes).ConfigureAwait(false);
await SetResponseBody(bodyBytes);
}
private async Task<byte[]> GetDecompressedResponseBody(string encodingType, byte[] responseBodyStream)
......@@ -281,7 +285,7 @@ namespace Titanium.Web.Proxy.EventArguments
var decompressionFactory = new DecompressionFactory();
var decompressor = decompressionFactory.Create(encodingType);
return await decompressor.Decompress(responseBodyStream).ConfigureAwait(false);
return await decompressor.Decompress(responseBodyStream);
}
......@@ -301,7 +305,7 @@ namespace Titanium.Web.Proxy.EventArguments
var result = Encoding.Default.GetBytes(html);
await Ok(result).ConfigureAwait(false);
await Ok(result);
}
/// <summary>
......@@ -317,7 +321,7 @@ namespace Titanium.Web.Proxy.EventArguments
response.HttpVersion = WebSession.Request.HttpVersion;
response.ResponseBody = result;
await Respond(response).ConfigureAwait(false);
await Respond(response);
WebSession.Request.CancelRequest = true;
}
......@@ -330,7 +334,7 @@ namespace Titanium.Web.Proxy.EventArguments
response.ResponseHeaders.Add(new Models.HttpHeader("Location", url));
response.ResponseBody = Encoding.ASCII.GetBytes(string.Empty);
await Respond(response).ConfigureAwait(false);
await Respond(response);
WebSession.Request.CancelRequest = true;
}
......@@ -345,7 +349,7 @@ namespace Titanium.Web.Proxy.EventArguments
WebSession.Response = response;
await ProxyServer.HandleHttpSessionResponse(this).ConfigureAwait(false);
await ProxyServer.HandleHttpSessionResponse(this);
}
}
......
using System;
namespace Titanium.Web.Proxy.Extensions
{
public static class ByteArrayExtensions
{
/// <summary>
/// Get the sub array from byte of data
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="index"></param>
/// <param name="length"></param>
/// <returns></returns>
public static T[] SubArray<T>(this T[] data, int index, int length)
{
T[] result = new T[length];
Array.Copy(data, index, result, 0, length);
return result;
}
}
}
......@@ -7,10 +7,14 @@ namespace Titanium.Web.Proxy.Extensions
/// <summary>
/// Extensions on HttpWebSession object
/// </summary>
public static class HttpWebRequestExtensions
internal static class HttpWebRequestExtensions
{
//Get encoding of the HTTP request
public static Encoding GetEncoding(this Request request)
/// <summary>
/// parse the character encoding of request from request headers
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
internal static Encoding GetEncoding(this Request request)
{
try
{
......@@ -19,7 +23,7 @@ namespace Titanium.Web.Proxy.Extensions
return Encoding.GetEncoding("ISO-8859-1");
//extract the encoding by finding the charset
var contentTypes = request.ContentType.Split(Constants.SemiColonSplit);
var contentTypes = request.ContentType.Split(ProxyConstants.SemiColonSplit);
foreach (var contentType in contentTypes)
{
var encodingSplit = contentType.Split('=');
......
......@@ -4,9 +4,14 @@ using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Extensions
{
public static class HttpWebResponseExtensions
internal static class HttpWebResponseExtensions
{
public static Encoding GetResponseCharacterEncoding(this Response response)
/// <summary>
/// Gets the character encoding of response from response headers
/// </summary>
/// <param name="response"></param>
/// <returns></returns>
internal static Encoding GetResponseCharacterEncoding(this Response response)
{
try
{
......@@ -15,7 +20,7 @@ namespace Titanium.Web.Proxy.Extensions
return Encoding.GetEncoding("ISO-8859-1");
//extract the encoding by finding the charset
var contentTypes = response.ContentType.Split(Constants.SemiColonSplit);
var contentTypes = response.ContentType.Split(ProxyConstants.SemiColonSplit);
foreach (var contentType in contentTypes)
{
var encodingSplit = contentType.Split('=');
......
using System;
using System.Globalization;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
......@@ -8,9 +7,19 @@ using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Extensions
{
public static class StreamHelper
/// <summary>
/// Extensions used for Stream and CustomBinaryReader objects
/// </summary>
internal static class StreamExtensions
{
public static async Task CopyToAsync(this Stream input, string initialData, Stream output)
/// <summary>
/// Copy streams asynchronously with an initial data inserted to the beginning of stream
/// </summary>
/// <param name="input"></param>
/// <param name="initialData"></param>
/// <param name="output"></param>
/// <returns></returns>
internal static async Task CopyToAsync(this Stream input, string initialData, Stream output)
{
if (!string.IsNullOrEmpty(initialData))
{
......@@ -19,18 +28,24 @@ namespace Titanium.Web.Proxy.Extensions
}
await input.CopyToAsync(output);
}
/// <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 totalbytesRead = 0;
long bytesToRead;
if (totalBytesToRead < Constants.BUFFER_SIZE)
if (totalBytesToRead < ProxyConstants.BUFFER_SIZE)
{
bytesToRead = totalBytesToRead;
}
else
bytesToRead = Constants.BUFFER_SIZE;
bytesToRead = ProxyConstants.BUFFER_SIZE;
while (totalbytesRead < totalBytesToRead)
......@@ -50,11 +65,18 @@ namespace Titanium.Web.Proxy.Extensions
await stream.WriteAsync(buffer, 0, buffer.Length);
}
}
/// <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)
{
var chuchkHead = await clientStreamReader.ReadLineAsync().ConfigureAwait(false);
var chuchkHead = await clientStreamReader.ReadLineAsync();
var chunkSize = int.Parse(chuchkHead, NumberStyles.HexNumber);
if (chunkSize != 0)
......@@ -62,14 +84,127 @@ namespace Titanium.Web.Proxy.Extensions
var buffer = await clientStreamReader.ReadBytesAsync(chunkSize);
await stream.WriteAsync(buffer, 0, buffer.Length);
//chunk trail
await clientStreamReader.ReadLineAsync().ConfigureAwait(false);
await clientStreamReader.ReadLineAsync();
}
else
{
await clientStreamReader.ReadLineAsync();
break;
}
}
}
/// <summary>
/// Writes the byte array body to the given stream; optionally chunked
/// </summary>
/// <param name="clientStream"></param>
/// <param name="data"></param>
/// <param name="isChunked"></param>
/// <returns></returns>
internal static async Task WriteResponseBody(this Stream clientStream, byte[] data, bool isChunked)
{
if (!isChunked)
{
await clientStream.WriteAsync(data, 0, data.Length);
}
else
await WriteResponseBodyChunked(data, clientStream);
}
/// <summary>
/// Copies the specified content length number of bytes to the output stream from the given inputs stream
/// optionally chunked
/// </summary>
/// <param name="inStreamReader"></param>
/// <param name="outStream"></param>
/// <param name="isChunked"></param>
/// <param name="ContentLength"></param>
/// <returns></returns>
internal static async Task WriteResponseBody(this CustomBinaryReader inStreamReader, Stream outStream, bool isChunked, long ContentLength)
{
if (!isChunked)
{
//http 1.0
if (ContentLength == -1)
ContentLength = long.MaxValue;
int bytesToRead = ProxyConstants.BUFFER_SIZE;
if (ContentLength < ProxyConstants.BUFFER_SIZE)
bytesToRead = (int)ContentLength;
var buffer = new byte[ProxyConstants.BUFFER_SIZE];
var bytesRead = 0;
var totalBytesRead = 0;
while ((bytesRead += await inStreamReader.BaseStream.ReadAsync(buffer, 0, bytesToRead)) > 0)
{
await outStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
if (totalBytesRead == ContentLength)
break;
bytesRead = 0;
var remainingBytes = (ContentLength - totalBytesRead);
bytesToRead = remainingBytes > (long)ProxyConstants.BUFFER_SIZE ? ProxyConstants.BUFFER_SIZE : (int)remainingBytes;
}
}
else
await WriteResponseBodyChunked(inStreamReader, outStream);
}
/// <summary>
/// Copies the streams chunked
/// </summary>
/// <param name="inStreamReader"></param>
/// <param name="outStream"></param>
/// <returns></returns>
internal static async Task WriteResponseBodyChunked(this CustomBinaryReader inStreamReader, Stream outStream)
{
while (true)
{
var chunkHead = await inStreamReader.ReadLineAsync();
var chunkSize = int.Parse(chunkHead, NumberStyles.HexNumber);
if (chunkSize != 0)
{
var buffer = await inStreamReader.ReadBytesAsync(chunkSize);
var chunkHeadBytes = Encoding.ASCII.GetBytes(chunkSize.ToString("x2"));
await outStream.WriteAsync(chunkHeadBytes, 0, chunkHeadBytes.Length);
await outStream.WriteAsync(ProxyConstants.NewLineBytes, 0, ProxyConstants.NewLineBytes.Length);
await outStream.WriteAsync(buffer, 0, chunkSize);
await outStream.WriteAsync(ProxyConstants.NewLineBytes, 0, ProxyConstants.NewLineBytes.Length);
await inStreamReader.ReadLineAsync();
}
else
{
await clientStreamReader.ReadLineAsync().ConfigureAwait(false);
await inStreamReader.ReadLineAsync();
await outStream.WriteAsync(ProxyConstants.ChunkEnd, 0, ProxyConstants.ChunkEnd.Length);
break;
}
}
}
/// <summary>
/// Copies the given input bytes to output stream chunked
/// </summary>
/// <param name="data"></param>
/// <param name="outStream"></param>
/// <returns></returns>
internal static async Task WriteResponseBodyChunked(this byte[] data, Stream outStream)
{
var chunkHead = Encoding.ASCII.GetBytes(data.Length.ToString("x2"));
await outStream.WriteAsync(chunkHead, 0, chunkHead.Length);
await outStream.WriteAsync(ProxyConstants.NewLineBytes, 0, ProxyConstants.NewLineBytes.Length);
await outStream.WriteAsync(data, 0, data.Length);
await outStream.WriteAsync(ProxyConstants.NewLineBytes, 0, ProxyConstants.NewLineBytes.Length);
await outStream.WriteAsync(ProxyConstants.ChunkEnd, 0, ProxyConstants.ChunkEnd.Length);
}
}
}
\ No newline at end of file
using System.Net.Sockets;
namespace Titanium.Web.Proxy.Extensions
{
internal static class TcpExtensions
{
public static bool IsConnected(this Socket client)
/// <summary>
/// verifies if the underlying socket is connected before using a TcpClient connection
/// </summary>
/// <param name="client"></param>
/// <returns></returns>
internal static bool IsConnected(this Socket client)
{
// This is how you can determine whether a socket is still connected.
bool blockingState = client.Blocking;
......
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Network;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Helpers
{
/// <summary>
/// A custom binary reader that would allo us to read string line by line
/// using the specified encoding
/// as well as to read bytes as required
/// </summary>
public class CustomBinaryReader : IDisposable
internal class CustomBinaryReader : IDisposable
{
private Stream stream;
private Encoding encoding;
internal CustomBinaryReader(Stream stream)
{
this.stream = stream;
}
//default to UTF-8
this.encoding = Encoding.UTF8;
}
public Stream BaseStream => stream;
internal Stream BaseStream => stream;
/// <summary>
/// Read a line from the byte stream
......@@ -34,32 +35,39 @@ namespace Titanium.Web.Proxy.Helpers
/// <returns></returns>
internal async Task<string> ReadLineAsync()
{
var readBuffer = new StringBuilder();
try
using (var readBuffer = new MemoryStream())
{
var lastChar = default(char);
var buffer = new byte[1];
while (await this.stream.ReadAsync(buffer, 0, 1).ConfigureAwait(false) > 0)
try
{
if (lastChar == '\r' && buffer[0] == '\n')
{
return readBuffer.Remove(readBuffer.Length - 1, 1).ToString();
}
if (buffer[0] == '\0')
var lastChar = default(char);
var buffer = new byte[1];
while ((await this.stream.ReadAsync(buffer, 0, 1)) > 0)
{
return readBuffer.ToString();
//if new line
if (lastChar == '\r' && buffer[0] == '\n')
{
var result = readBuffer.ToArray();
return encoding.GetString(result.SubArray(0, result.Length - 1));
}
//end of stream
if (buffer[0] == '\0')
{
return encoding.GetString(readBuffer.ToArray());
}
await readBuffer.WriteAsync(buffer,0,1);
//store last char for new line comparison
lastChar = (char)buffer[0];
}
readBuffer.Append((char)buffer[0]);
lastChar = (char)buffer[0];
}
return readBuffer.ToString();
}
catch (IOException)
{
return readBuffer.ToString();
return encoding.GetString(readBuffer.ToArray());
}
catch (IOException)
{
throw;
}
}
}
......@@ -71,30 +79,35 @@ namespace Titanium.Web.Proxy.Helpers
{
string tmpLine;
var requestLines = new List<string>();
while (!string.IsNullOrEmpty(tmpLine = await ReadLineAsync().ConfigureAwait(false)))
while (!string.IsNullOrEmpty(tmpLine = await ReadLineAsync()))
{
requestLines.Add(tmpLine);
}
return requestLines;
}
/// <summary>
/// Read the specified number of raw bytes from the base stream
/// </summary>
/// <param name="totalBytesToRead"></param>
/// <returns></returns>
internal async Task<byte[]> ReadBytesAsync(long totalBytesToRead)
{
int bytesToRead = Constants.BUFFER_SIZE;
int bytesToRead = ProxyConstants.BUFFER_SIZE;
if (totalBytesToRead < Constants.BUFFER_SIZE)
if (totalBytesToRead < ProxyConstants.BUFFER_SIZE)
bytesToRead = (int)totalBytesToRead;
var buffer = new byte[Constants.BUFFER_SIZE];
var buffer = new byte[ProxyConstants.BUFFER_SIZE];
var bytesRead = 0;
var totalBytesRead = 0;
using (var outStream = new MemoryStream())
{
while ((bytesRead += await this.stream.ReadAsync(buffer, 0, bytesToRead).ConfigureAwait(false)) > 0)
while ((bytesRead += await this.stream.ReadAsync(buffer, 0, bytesToRead)) > 0)
{
await outStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
await outStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
if (totalBytesRead == totalBytesToRead)
......@@ -102,7 +115,7 @@ namespace Titanium.Web.Proxy.Helpers
bytesRead = 0;
var remainingBytes = (totalBytesToRead - totalBytesRead);
bytesToRead = remainingBytes > (long)Constants.BUFFER_SIZE ? Constants.BUFFER_SIZE : (int)remainingBytes;
bytesToRead = remainingBytes > (long)ProxyConstants.BUFFER_SIZE ? ProxyConstants.BUFFER_SIZE : (int)remainingBytes;
}
return outStream.ToArray();
......
......@@ -5,8 +5,12 @@ using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Helper classes for setting system proxy settings
/// </summary>
namespace Titanium.Web.Proxy.Helpers
{
internal static class NativeMethods
{
[DllImport("wininet.dll")]
......@@ -14,27 +18,27 @@ namespace Titanium.Web.Proxy.Helpers
int dwBufferLength);
}
internal class HttpSystemProxyValue
internal class HttpSystemProxyValue
{
public string HostName { get; set; }
public int Port { get; set; }
public bool IsSecure { get; set; }
internal string HostName { get; set; }
internal int Port { get; set; }
internal bool IsHttps { get; set; }
public override string ToString()
{
if (!IsSecure)
if (!IsHttps)
return "http=" + HostName + ":" + Port;
else
return "https=" + HostName + ":" + Port;
}
}
public static class SystemProxyHelper
internal static class SystemProxyHelper
{
public const int InternetOptionSettingsChanged = 39;
public const int InternetOptionRefresh = 37;
internal const int InternetOptionSettingsChanged = 39;
internal const int InternetOptionRefresh = 37;
public static void SetHttpProxy(string hostname, int port)
internal static void SetHttpProxy(string hostname, int port)
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
......@@ -44,11 +48,11 @@ namespace Titanium.Web.Proxy.Helpers
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => !x.IsSecure);
existingSystemProxyValues.RemoveAll(x => !x.IsHttps);
existingSystemProxyValues.Add(new HttpSystemProxyValue()
{
HostName = hostname,
IsSecure = false,
IsHttps = false,
Port = port
});
......@@ -59,19 +63,21 @@ namespace Titanium.Web.Proxy.Helpers
Refresh();
}
public static void RemoveHttpProxy()
/// <summary>
/// Remove the http proxy setting from current machine
/// </summary>
internal static void RemoveHttpProxy()
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
if (reg != null)
{
if (reg.GetValue("ProxyServer")!=null)
if (reg.GetValue("ProxyServer") != null)
{
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => !x.IsSecure);
existingSystemProxyValues.RemoveAll(x => !x.IsHttps);
if (!(existingSystemProxyValues.Count() == 0))
{
......@@ -89,11 +95,16 @@ namespace Titanium.Web.Proxy.Helpers
Refresh();
}
public static void SetHttpsProxy(string hostname, int port)
/// <summary>
/// Set the HTTPS proxy server for current machine
/// </summary>
/// <param name="hostname"></param>
/// <param name="port"></param>
internal static void SetHttpsProxy(string hostname, int port)
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
if (reg != null)
{
prepareRegistry(reg);
......@@ -101,11 +112,11 @@ namespace Titanium.Web.Proxy.Helpers
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => x.IsSecure);
existingSystemProxyValues.RemoveAll(x => x.IsHttps);
existingSystemProxyValues.Add(new HttpSystemProxyValue()
{
HostName = hostname,
IsSecure = true,
IsHttps = true,
Port = port
});
......@@ -116,7 +127,10 @@ namespace Titanium.Web.Proxy.Helpers
Refresh();
}
public static void RemoveHttpsProxy()
/// <summary>
/// Removes the https proxy setting to nothing
/// </summary>
internal static void RemoveHttpsProxy()
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
......@@ -127,7 +141,7 @@ namespace Titanium.Web.Proxy.Helpers
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => x.IsSecure);
existingSystemProxyValues.RemoveAll(x => x.IsHttps);
if (!(existingSystemProxyValues.Count() == 0))
{
......@@ -139,14 +153,17 @@ namespace Titanium.Web.Proxy.Helpers
reg.SetValue("ProxyEnable", 0);
reg.SetValue("ProxyServer", string.Empty);
}
}
}
Refresh();
}
public static void DisableAllProxy()
/// <summary>
/// Removes all types of proxy settings (both http & https)
/// </summary>
internal static void DisableAllProxy()
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
......@@ -159,6 +176,12 @@ namespace Titanium.Web.Proxy.Helpers
Refresh();
}
/// <summary>
/// Get the current system proxy setting values
/// </summary>
/// <param name="prevServerValue"></param>
/// <returns></returns>
private static List<HttpSystemProxyValue> GetSystemProxyValues(string prevServerValue)
{
var result = new List<HttpSystemProxyValue>();
......@@ -187,6 +210,11 @@ namespace Titanium.Web.Proxy.Helpers
return result;
}
/// <summary>
/// Parses the system proxy setting string
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private static HttpSystemProxyValue parseProxyValue(string value)
{
var tmp = Regex.Replace(value, @"\s+", " ").Trim().ToLower();
......@@ -197,7 +225,7 @@ namespace Titanium.Web.Proxy.Helpers
{
HostName = endPoint.Split(':')[0],
Port = int.Parse(endPoint.Split(':')[1]),
IsSecure = false
IsHttps = false
};
}
else if (tmp.StartsWith("https="))
......@@ -207,13 +235,16 @@ namespace Titanium.Web.Proxy.Helpers
{
HostName = endPoint.Split(':')[0],
Port = int.Parse(endPoint.Split(':')[1]),
IsSecure = true
IsHttps = true
};
}
return null;
}
/// <summary>
/// Prepares the proxy server registry (create empty values if they don't exist)
/// </summary>
/// <param name="reg"></param>
private static void prepareRegistry(RegistryKey reg)
{
if (reg.GetValue("ProxyEnable") == null)
......@@ -227,8 +258,10 @@ namespace Titanium.Web.Proxy.Helpers
}
}
/// <summary>
/// Refresh the settings so that the system know about a change in proxy setting
/// </summary>
private static void Refresh()
{
NativeMethods.InternetSetOption(IntPtr.Zero, InternetOptionSettingsChanged, IntPtr.Zero, 0);
......
......@@ -12,11 +12,24 @@ using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Helpers
{
public class TcpHelper
internal class TcpHelper
{
public async static Task SendRaw(Stream clientStream, string httpCmd, List<HttpHeader> requestHeaders, string hostName,
/// <summary>
/// relays the input clientStream to the server at the specified host name & port with the given httpCmd & headers as prefix
/// Usefull for websocket requests
/// </summary>
/// <param name="clientStream"></param>
/// <param name="httpCmd"></param>
/// <param name="requestHeaders"></param>
/// <param name="hostName"></param>
/// <param name="tunnelPort"></param>
/// <param name="isHttps"></param>
/// <returns></returns>
internal async static Task SendRaw(Stream clientStream, string httpCmd, List<HttpHeader> requestHeaders, string hostName,
int tunnelPort, bool isHttps)
{
//prepare the prefix content
StringBuilder sb = null;
if (httpCmd != null || requestHeaders != null)
{
......@@ -38,7 +51,7 @@ namespace Titanium.Web.Proxy.Helpers
TcpClient tunnelClient = null;
Stream tunnelStream = null;
//create the TcpClient to the server
try
{
tunnelClient = new TcpClient(hostName, tunnelPort);
......@@ -50,7 +63,7 @@ namespace Titanium.Web.Proxy.Helpers
try
{
sslStream = new SslStream(tunnelStream);
await sslStream.AuthenticateAsClientAsync(hostName, null, Constants.SupportedProtocols, false);
await sslStream.AuthenticateAsClientAsync(hostName, null, ProxyConstants.SupportedSslProtocols, false);
tunnelStream = sslStream;
}
catch
......@@ -64,6 +77,7 @@ namespace Titanium.Web.Proxy.Helpers
Task sendRelay;
//Now async relay all server=>client & client=>server data
if (sb != null)
sendRelay = clientStream.CopyToAsync(sb.ToString(), tunnelStream);
else
......@@ -72,7 +86,7 @@ namespace Titanium.Web.Proxy.Helpers
var receiveRelay = tunnelStream.CopyToAsync(string.Empty, clientStream);
await Task.WhenAll(sendRelay, receiveRelay).ConfigureAwait(false);
await Task.WhenAll(sendRelay, receiveRelay);
}
catch
{
......
......@@ -9,14 +9,23 @@ using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Http
{
public class HttpWebSession
/// <summary>
/// Used to communicate with the server over HTTP(S)
/// </summary>
public class HttpWebClient
{
/// <summary>
/// Connection to server
/// </summary>
internal TcpConnection ServerConnection { get; set; }
public Request Request { get; set; }
public Response Response { get; set; }
public bool IsSecure
/// <summary>
/// Is Https?
/// </summary>
public bool IsHttps
{
get
{
......@@ -24,31 +33,40 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Set the tcp connection to server used by this webclient
/// </summary>
/// <param name="Connection"></param>
internal void SetConnection(TcpConnection Connection)
{
Connection.LastAccess = DateTime.Now;
ServerConnection = Connection;
}
internal HttpWebSession()
internal HttpWebClient()
{
this.Request = new Request();
this.Response = new Response();
}
/// <summary>
/// Prepare & send the http(s) request
/// </summary>
/// <returns></returns>
internal async Task SendRequest()
{
Stream stream = ServerConnection.Stream;
StringBuilder requestLines = new StringBuilder();
//prepare the request & headers
requestLines.AppendLine(string.Join(" ", new string[3]
{
this.Request.Method,
this.Request.RequestUri.PathAndQuery,
this.Request.HttpVersion
string.Format("HTTP/{0}.{1}",this.Request.HttpVersion.Major, this.Request.HttpVersion.Minor)
}));
//write request headers
foreach (HttpHeader httpHeader in this.Request.RequestHeaders)
{
requestLines.AppendLine(httpHeader.Name + ':' + httpHeader.Value);
......@@ -64,7 +82,7 @@ namespace Titanium.Web.Proxy.Http
if (ProxyServer.Enable100ContinueBehaviour)
if (this.Request.ExpectContinue)
{
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(Constants.SpaceSplit, 3);
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
var responseStatusCode = httpResult[1].Trim();
var responseStatusDescription = httpResult[2].Trim();
......@@ -73,30 +91,41 @@ namespace Titanium.Web.Proxy.Http
&& responseStatusDescription.ToLower().Equals("continue"))
{
this.Request.Is100Continue = true;
await ServerConnection.StreamReader.ReadLineAsync().ConfigureAwait(false);
await ServerConnection.StreamReader.ReadLineAsync();
}
else if (responseStatusCode.Equals("417")
&& responseStatusDescription.ToLower().Equals("expectation failed"))
{
this.Request.ExpectationFailed = true;
await ServerConnection.StreamReader.ReadLineAsync().ConfigureAwait(false);
await ServerConnection.StreamReader.ReadLineAsync();
}
}
}
/// <summary>
/// Receive & parse the http response from server
/// </summary>
/// <returns></returns>
internal async Task ReceiveResponse()
{
//return if this is already read
if (this.Response.ResponseStatusCode != null) return;
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(Constants.SpaceSplit, 3);
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
if (string.IsNullOrEmpty(httpResult[0]))
{
await ServerConnection.StreamReader.ReadLineAsync().ConfigureAwait(false);
await ServerConnection.StreamReader.ReadLineAsync();
}
var httpVersion = httpResult[0].Trim().ToLower();
var version = new Version(1,1);
if (httpVersion == "http/1.0")
{
version = new Version(1, 0);
}
this.Response.HttpVersion = httpResult[0].Trim();
this.Response.HttpVersion = version;
this.Response.ResponseStatusCode = httpResult[1].Trim();
this.Response.ResponseStatusDescription = httpResult[2].Trim();
......@@ -106,7 +135,7 @@ namespace Titanium.Web.Proxy.Http
{
this.Response.Is100Continue = true;
this.Response.ResponseStatusCode = null;
await ServerConnection.StreamReader.ReadLineAsync().ConfigureAwait(false);
await ServerConnection.StreamReader.ReadLineAsync();
await ReceiveResponse();
return;
}
......@@ -115,16 +144,17 @@ namespace Titanium.Web.Proxy.Http
{
this.Response.ExpectationFailed = true;
this.Response.ResponseStatusCode = null;
await ServerConnection.StreamReader.ReadLineAsync().ConfigureAwait(false);
await ServerConnection.StreamReader.ReadLineAsync();
await ReceiveResponse();
return;
}
List<string> responseLines = await ServerConnection.StreamReader.ReadAllLinesAsync().ConfigureAwait(false);
//read response headers
List<string> responseLines = await ServerConnection.StreamReader.ReadAllLinesAsync();
for (int index = 0; index < responseLines.Count; ++index)
{
string[] strArray = responseLines[index].Split(Constants.ColonSplit, 2);
string[] strArray = responseLines[index].Split(ProxyConstants.ColonSplit, 2);
this.Response.ResponseHeaders.Add(new HttpHeader(strArray[0], strArray[1]));
}
}
......
......@@ -7,12 +7,29 @@ using Titanium.Web.Proxy.Extensions;
namespace Titanium.Web.Proxy.Http
{
/// <summary>
/// A HTTP(S) request object
/// </summary>
public class Request
{
/// <summary>
/// Request Method
/// </summary>
public string Method { get; set; }
/// <summary>
/// Request HTTP Uri
/// </summary>
public Uri RequestUri { get; set; }
public string HttpVersion { get; set; }
/// <summary>
/// Request Http Version
/// </summary>
public Version HttpVersion { get; set; }
/// <summary>
/// Request Http hostanem
/// </summary>
internal string Host
{
get
......@@ -32,6 +49,9 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Request content encoding
/// </summary>
internal string ContentEncoding
{
get
......@@ -47,6 +67,9 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Request content-length
/// </summary>
public long ContentLength
{
get
......@@ -65,7 +88,6 @@ namespace Titanium.Web.Proxy.Http
}
set
{
var header = RequestHeaders.FirstOrDefault(x => x.Name.ToLower() == "content-length");
if (value >= 0)
......@@ -85,6 +107,9 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Request content-type
/// </summary>
public string ContentType
{
get
......@@ -106,6 +131,9 @@ namespace Titanium.Web.Proxy.Http
}
/// <summary>
/// Is request body send as chunked bytes
/// </summary>
public bool IsChunked
{
get
......@@ -137,6 +165,9 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Does this request has a 100-continue header?
/// </summary>
public bool ExpectContinue
{
get
......@@ -147,19 +178,37 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Request Url
/// </summary>
public string Url { get { return RequestUri.OriginalString; } }
/// <summary>
/// Encoding for this request
/// </summary>
internal Encoding Encoding { get { return this.GetEncoding(); } }
/// <summary>
/// Terminates the underlying Tcp Connection to client after current request
/// </summary>
internal bool CancelRequest { get; set; }
/// <summary>
/// Request body as byte array
/// </summary>
internal byte[] RequestBody { get; set; }
/// <summary>
/// request body as string
/// </summary>
internal string RequestBodyString { get; set; }
internal bool RequestBodyRead { get; set; }
internal bool RequestLocked { get; set; }
/// <summary>
/// Does this request has an upgrade to websocket header?
/// </summary>
internal bool UpgradeToWebSocket
{
get
......@@ -176,8 +225,19 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Request heade collection
/// </summary>
public List<HttpHeader> RequestHeaders { get; set; }
/// <summary>
/// Does server responsed positively for 100 continue request
/// </summary>
public bool Is100Continue { get; internal set; }
/// <summary>
/// Server responsed negatively for the request for 100 continue
/// </summary>
public bool ExpectationFailed { get; internal set; }
public Request()
......
......@@ -4,10 +4,13 @@ using System.Linq;
using System.Text;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Extensions;
using System;
namespace Titanium.Web.Proxy.Http
{
/// <summary>
/// Http(s) response object
/// </summary>
public class Response
{
public string ResponseStatusCode { get; set; }
......@@ -15,7 +18,9 @@ namespace Titanium.Web.Proxy.Http
internal Encoding Encoding { get { return this.GetResponseCharacterEncoding(); } }
/// <summary>
/// Content encoding for this response
/// </summary>
internal string ContentEncoding
{
get
......@@ -31,7 +36,11 @@ namespace Titanium.Web.Proxy.Http
}
}
internal string HttpVersion { get; set; }
internal Version HttpVersion { get; set; }
/// <summary>
/// Keep the connection alive?
/// </summary>
internal bool ResponseKeepAlive
{
get
......@@ -48,6 +57,9 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Content type of this response
/// </summary>
public string ContentType
{
get
......@@ -64,6 +76,9 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Length of response body
/// </summary>
internal long ContentLength
{
get
......@@ -102,6 +117,9 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Response transfer-encoding is chunked?
/// </summary>
internal bool IsChunked
{
get
......@@ -140,14 +158,37 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Collection of all response headers
/// </summary>
public List<HttpHeader> ResponseHeaders { get; set; }
/// <summary>
/// Response network stream
/// </summary>
internal Stream ResponseStream { get; set; }
/// <summary>
/// response body contenst as byte array
/// </summary>
internal byte[] ResponseBody { get; set; }
/// <summary>
/// response body as string
/// </summary>
internal string ResponseBodyString { get; set; }
internal bool ResponseBodyRead { get; set; }
internal bool ResponseLocked { get; set; }
/// <summary>
/// Is response 100-continue
/// </summary>
public bool Is100Continue { get; internal set; }
/// <summary>
/// expectation failed returned by server?
/// </summary>
public bool ExpectationFailed { get; internal set; }
public Response()
......
using Titanium.Web.Proxy.Network;
namespace Titanium.Web.Proxy.Http.Responses
namespace Titanium.Web.Proxy.Http.Responses
{
/// <summary>
/// 200 Ok response
/// </summary>
public class OkResponse : Response
{
public OkResponse()
......
......@@ -2,6 +2,9 @@
namespace Titanium.Web.Proxy.Http.Responses
{
/// <summary>
/// Redirect response
/// </summary>
public class RedirectResponse : Response
{
public RedirectResponse()
......
......@@ -4,6 +4,9 @@ using System.Net.Sockets;
namespace Titanium.Web.Proxy.Models
{
/// <summary>
/// An abstract endpoint where the proxy listens
/// </summary>
public abstract class ProxyEndPoint
{
public ProxyEndPoint(IPAddress IpAddress, int Port, bool EnableSsl)
......@@ -20,6 +23,10 @@ namespace Titanium.Web.Proxy.Models
internal TcpListener listener { get; set; }
}
/// <summary>
/// A proxy endpoint that the client is aware of
/// So client application know that it is communicating with a proxy server
/// </summary>
public class ExplicitProxyEndPoint : ProxyEndPoint
{
internal bool IsSystemHttpProxy { get; set; }
......@@ -34,6 +41,10 @@ namespace Titanium.Web.Proxy.Models
}
}
/// <summary>
/// A proxy end point client is not aware of
/// Usefull when requests are redirected to this proxy end point through port forwarding
/// </summary>
public class TransparentProxyEndPoint : ProxyEndPoint
{
//Name of the Certificate need to be sent (same as the hostname we want to proxy)
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Models
namespace Titanium.Web.Proxy.Models
{
/// <summary>
/// An upstream proxy this proxy uses if any
/// </summary>
public class ExternalProxy
{
public string HostName { get; set; }
......
using System;
using System.Security.Cryptography.X509Certificates;
namespace Titanium.Web.Proxy.Network
{
/// <summary>
/// An object that holds the cached certificate
/// </summary>
internal class CachedCertificate
{
internal X509Certificate2 Certificate { get; set; }
/// <summary>
/// last time this certificate was used
/// Usefull in determining its cache lifetime
/// </summary>
internal DateTime LastAccess { get; set; }
internal CachedCertificate()
{
LastAccess = DateTime.Now;
}
}
}
using System;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Reflection;
using System.Threading.Tasks;
using System.Threading;
namespace Titanium.Web.Proxy.Helpers
{
public class CertificateManager : IDisposable
{
private const string CertCreateFormat =
"-ss {0} -n \"CN={1}, O={2}\" -sky {3} -cy {4} -m 120 -a sha256 -eku 1.3.6.1.5.5.7.3.1 {5}";
private readonly IDictionary<string, X509Certificate2> _certificateCache;
private static SemaphoreSlim semaphoreLock = new SemaphoreSlim(1);
public string Issuer { get; private set; }
public string RootCertificateName { get; private set; }
public X509Store MyStore { get; private set; }
public X509Store RootStore { get; private set; }
public CertificateManager(string issuer, string rootCertificateName)
{
Issuer = issuer;
RootCertificateName = rootCertificateName;
MyStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
RootStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
_certificateCache = new Dictionary<string, X509Certificate2>();
}
/// <summary>
/// Attempts to move a self-signed certificate to the root store.
/// </summary>
/// <returns>true if succeeded, else false</returns>
public async Task<bool> CreateTrustedRootCertificate()
{
X509Certificate2 rootCertificate =
await CreateCertificate(RootStore, RootCertificateName);
return rootCertificate != null;
}
/// <summary>
/// Attempts to remove the self-signed certificate from the root store.
/// </summary>
/// <returns>true if succeeded, else false</returns>
public async Task<bool> DestroyTrustedRootCertificate()
{
return await DestroyCertificate(RootStore, RootCertificateName);
}
public X509Certificate2Collection FindCertificates(string certificateSubject)
{
return FindCertificates(MyStore, certificateSubject);
}
protected virtual X509Certificate2Collection FindCertificates(X509Store store, string certificateSubject)
{
X509Certificate2Collection discoveredCertificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, certificateSubject, false);
return discoveredCertificates.Count > 0 ?
discoveredCertificates : null;
}
public async Task<X509Certificate2> CreateCertificate(string certificateName)
{
return await CreateCertificate(MyStore, certificateName);
}
protected async virtual Task<X509Certificate2> CreateCertificate(X509Store store, string certificateName)
{
if (_certificateCache.ContainsKey(certificateName))
return _certificateCache[certificateName];
await semaphoreLock.WaitAsync();
X509Certificate2 certificate = null;
try
{
store.Open(OpenFlags.ReadWrite);
string certificateSubject = string.Format("CN={0}, O={1}", certificateName, Issuer);
var certificates =
FindCertificates(store, certificateSubject);
if (certificates != null)
certificate = certificates[0];
if (certificate == null)
{
string[] args = new[] {
GetCertificateCreateArgs(store, certificateName) };
await CreateCertificate(args);
certificates = FindCertificates(store, certificateSubject);
return certificates != null ?
certificates[0] : null;
}
store.Close();
if (certificate != null && !_certificateCache.ContainsKey(certificateName))
_certificateCache.Add(certificateName, certificate);
return certificate;
}
finally
{
semaphoreLock.Release();
}
}
protected virtual Task<int> CreateCertificate(string[] args)
{
// there is no non-generic TaskCompletionSource
var tcs = new TaskCompletionSource<int>();
var process = new Process();
string file = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "makecert.exe");
if (!File.Exists(file))
throw new Exception("Unable to locate 'makecert.exe'.");
process.StartInfo.Verb = "runas";
process.StartInfo.Arguments = args != null ? args[0] : string.Empty;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = file;
process.EnableRaisingEvents = true;
process.Exited += (sender, processArgs) =>
{
tcs.SetResult(process.ExitCode);
process.Dispose();
};
bool started = process.Start();
if (!started)
{
//you may allow for the process to be re-used (started = false)
//but I'm not sure about the guarantees of the Exited event in such a case
throw new InvalidOperationException("Could not start process: " + process);
}
return tcs.Task;
}
public async Task<bool> DestroyCertificate(string certificateName)
{
return await DestroyCertificate(MyStore, certificateName);
}
protected virtual async Task<bool> DestroyCertificate(X509Store store, string certificateName)
{
await semaphoreLock.WaitAsync();
X509Certificate2Collection certificates = null;
try
{
store.Open(OpenFlags.ReadWrite);
string certificateSubject = string.Format("CN={0}, O={1}", certificateName, Issuer);
certificates = FindCertificates(store, certificateSubject);
if (certificates != null)
{
store.RemoveRange(certificates);
certificates = FindCertificates(store, certificateSubject);
}
store.Close();
if (certificates == null &&
_certificateCache.ContainsKey(certificateName))
{
_certificateCache.Remove(certificateName);
}
return certificates == null;
}
finally
{
semaphoreLock.Release();
}
}
protected virtual string GetCertificateCreateArgs(X509Store store, string certificateName)
{
bool isRootCertificate =
(certificateName == RootCertificateName);
string certCreatArgs = string.Format(CertCreateFormat,
store.Name, certificateName, Issuer,
isRootCertificate ? "signature" : "exchange",
isRootCertificate ? "authority" : "end",
isRootCertificate ? "-h 1 -r" : string.Format("-pe -in \"{0}\" -is Root", RootCertificateName));
return certCreatArgs;
}
public void Dispose()
{
if (MyStore != null)
MyStore.Close();
if (RootStore != null)
RootStore.Close();
}
}
using System;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Reflection;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
namespace Titanium.Web.Proxy.Network
{
/// <summary>
/// A class to manage SSL certificates used by this proxy server
/// </summary>
internal class CertificateManager : IDisposable
{
private const string CertCreateFormat =
"-ss {0} -n \"CN={1}, O={2}\" -sky {3} -cy {4} -m 120 -a sha256 -eku 1.3.6.1.5.5.7.3.1 {5}";
private readonly IDictionary<string, CachedCertificate> certificateCache;
private static SemaphoreSlim semaphoreLock = new SemaphoreSlim(1);
internal string Issuer { get; private set; }
internal string RootCertificateName { get; private set; }
internal X509Store MyStore { get; private set; }
internal X509Store RootStore { get; private set; }
internal CertificateManager(string issuer, string rootCertificateName)
{
Issuer = issuer;
RootCertificateName = rootCertificateName;
MyStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
RootStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
certificateCache = new Dictionary<string, CachedCertificate>();
}
/// <summary>
/// Attempts to move a self-signed certificate to the root store.
/// </summary>
/// <returns>true if succeeded, else false</returns>
internal async Task<bool> CreateTrustedRootCertificate()
{
X509Certificate2 rootCertificate =
await CreateCertificate(RootStore, RootCertificateName, true);
return rootCertificate != null;
}
/// <summary>
/// Attempts to remove the self-signed certificate from the root store.
/// </summary>
/// <returns>true if succeeded, else false</returns>
internal bool DestroyTrustedRootCertificate()
{
return DestroyCertificate(RootStore, RootCertificateName, false);
}
internal X509Certificate2Collection FindCertificates(string certificateSubject)
{
return FindCertificates(MyStore, certificateSubject);
}
protected virtual X509Certificate2Collection FindCertificates(X509Store store, string certificateSubject)
{
X509Certificate2Collection discoveredCertificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, certificateSubject, false);
return discoveredCertificates.Count > 0 ?
discoveredCertificates : null;
}
internal async Task<X509Certificate2> CreateCertificate(string certificateName, bool isRootCertificate)
{
return await CreateCertificate(MyStore, certificateName, isRootCertificate);
}
/// <summary>
/// Create an SSL certificate
/// </summary>
/// <param name="store"></param>
/// <param name="certificateName"></param>
/// <param name="isRootCertificate"></param>
/// <returns></returns>
protected async virtual Task<X509Certificate2> CreateCertificate(X509Store store, string certificateName, bool isRootCertificate)
{
await semaphoreLock.WaitAsync();
try
{
if (certificateCache.ContainsKey(certificateName))
{
var cached = certificateCache[certificateName];
cached.LastAccess = DateTime.Now;
return cached.Certificate;
}
X509Certificate2 certificate = null;
store.Open(OpenFlags.ReadWrite);
string certificateSubject = string.Format("CN={0}, O={1}", certificateName, Issuer);
X509Certificate2Collection certificates;
if (isRootCertificate)
{
certificates = FindCertificates(store, certificateSubject);
if (certificates != null)
certificate = certificates[0];
}
if (certificate == null)
{
string[] args = new[] {
GetCertificateCreateArgs(store, certificateName) };
await CreateCertificate(args);
certificates = FindCertificates(store, certificateSubject);
//remove it from store
if (!isRootCertificate)
DestroyCertificate(certificateName);
if (certificates != null)
certificate = certificates[0];
}
store.Close();
if (certificate != null && !certificateCache.ContainsKey(certificateName))
certificateCache.Add(certificateName, new CachedCertificate() { Certificate = certificate });
return certificate;
}
finally
{
semaphoreLock.Release();
}
}
/// <summary>
/// Create certificate using makecert.exe
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
protected virtual Task<int> CreateCertificate(string[] args)
{
// there is no non-generic TaskCompletionSource
var tcs = new TaskCompletionSource<int>();
var process = new Process();
string file = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "makecert.exe");
if (!File.Exists(file))
throw new Exception("Unable to locate 'makecert.exe'.");
process.StartInfo.Verb = "runas";
process.StartInfo.Arguments = args != null ? args[0] : string.Empty;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = file;
process.EnableRaisingEvents = true;
process.Exited += (sender, processArgs) =>
{
tcs.SetResult(process.ExitCode);
process.Dispose();
};
bool started = process.Start();
if (!started)
{
//you may allow for the process to be re-used (started = false)
//but I'm not sure about the guarantees of the Exited event in such a case
throw new InvalidOperationException("Could not start process: " + process);
}
return tcs.Task;
}
/// <summary>
/// Destroy an SSL certificate from local store
/// </summary>
/// <param name="certificateName"></param>
/// <returns></returns>
internal bool DestroyCertificate(string certificateName)
{
return DestroyCertificate(MyStore, certificateName, false);
}
/// <summary>
/// Destroy certificate from the specified store
/// optionally also remove from proxy certificate cache
/// </summary>
/// <param name="store"></param>
/// <param name="certificateName"></param>
/// <param name="removeFromCache"></param>
/// <returns></returns>
protected virtual bool DestroyCertificate(X509Store store, string certificateName, bool removeFromCache)
{
X509Certificate2Collection certificates = null;
store.Open(OpenFlags.ReadWrite);
string certificateSubject = string.Format("CN={0}, O={1}", certificateName, Issuer);
certificates = FindCertificates(store, certificateSubject);
if (certificates != null)
{
store.RemoveRange(certificates);
certificates = FindCertificates(store, certificateSubject);
}
store.Close();
if (removeFromCache &&
certificateCache.ContainsKey(certificateName))
{
certificateCache.Remove(certificateName);
}
return certificates == null;
}
/// <summary>
/// Create the neccessary arguments for makecert.exe to create the required certificate
/// </summary>
/// <param name="store"></param>
/// <param name="certificateName"></param>
/// <returns></returns>
protected virtual string GetCertificateCreateArgs(X509Store store, string certificateName)
{
bool isRootCertificate =
(certificateName == RootCertificateName);
string certCreatArgs = string.Format(CertCreateFormat,
store.Name, certificateName, Issuer,
isRootCertificate ? "signature" : "exchange",
isRootCertificate ? "authority" : "end",
isRootCertificate ? "-h 1 -r" : string.Format("-pe -in \"{0}\" -is Root", RootCertificateName));
return certCreatArgs;
}
private static bool clearCertificates { get; set; }
/// <summary>
/// Stops the certificate cache clear process
/// </summary>
internal void StopClearIdleCertificates()
{
clearCertificates = false;
}
/// <summary>
/// A method to clear outdated certificates
/// </summary>
internal async void ClearIdleCertificates()
{
clearCertificates = true;
while (clearCertificates)
{
await semaphoreLock.WaitAsync();
try
{
var cutOff = DateTime.Now.AddMinutes(-1 * ProxyServer.CertificateCacheTimeOutMinutes);
var outdated = certificateCache
.Where(x => x.Value.LastAccess < cutOff)
.ToList();
foreach (var cache in outdated)
certificateCache.Remove(cache.Key);
}
finally { semaphoreLock.Release(); }
//after a minute come back to check for outdated certificates in cache
await Task.Delay(1000 * 60);
}
}
public void Dispose()
{
if (MyStore != null)
MyStore.Close();
if (RootStore != null)
RootStore.Close();
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
namespace Titanium.Web.Proxy.Network
{
/// <summary>
/// Used to pass in Session object for ServerCertificateValidation Callback
/// </summary>
internal class CustomSslStream : SslStream
{
/// <summary>
/// Holds the current session
/// </summary>
internal SessionEventArgs Session { get; set; }
public CustomSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback)
:base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback)
{
}
}
}
......@@ -5,27 +5,27 @@ using Titanium.Web.Proxy.Helpers;
namespace Titanium.Web.Proxy.Network
{
/// <summary>
/// This class wraps Tcp connection to Server
/// This class wraps Tcp connection to client
/// </summary>
public class ProxyClient
{
/// <summary>
/// TcpClient used to communicate with server
/// TcpClient used to communicate with client
/// </summary>
internal TcpClient TcpClient { get; set; }
/// <summary>
/// holds the stream to server
/// holds the stream to client
/// </summary>
internal Stream ClientStream { get; set; }
/// <summary>
/// Used to read line by line from server
/// Used to read line by line from client
/// </summary>
internal CustomBinaryReader ClientStreamReader { get; set; }
/// <summary>
/// used to write line by line to server
/// used to write line by line to client
/// </summary>
internal StreamWriter ClientStreamWriter { get; set; }
......
using System;
using System.IO;
using System.Net.Sockets;
using Titanium.Web.Proxy.Helpers;
namespace Titanium.Web.Proxy.Network
{
/// <summary>
/// An object that holds TcpConnection to a particular server & port
/// </summary>
public class TcpConnection
{
internal string HostName { get; set; }
internal int port { get; set; }
internal bool IsHttps { get; set; }
/// <summary>
/// Http version
/// </summary>
internal Version Version { get; set; }
internal TcpClient TcpClient { get; set; }
/// <summary>
/// used to read lines from server
/// </summary>
internal CustomBinaryReader StreamReader { get; set; }
/// <summary>
/// Server stream
/// </summary>
internal Stream Stream { get; set; }
/// <summary>
/// Last time this connection was used
/// </summary>
internal DateTime LastAccess { get; set; }
internal TcpConnection()
{
LastAccess = DateTime.Now;
}
}
}
......@@ -10,88 +10,124 @@ using Titanium.Web.Proxy.Helpers;
using System.Threading;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Shared;
using System.Security.Cryptography.X509Certificates;
using Titanium.Web.Proxy.EventArguments;
namespace Titanium.Web.Proxy.Network
{
public class TcpConnection
/// <summary>
/// A class that manages Tcp Connection to server used by this proxy server
/// </summary>
internal class TcpConnectionManager
{
internal string HostName { get; set; }
internal int port { get; set; }
internal bool IsSecure { get; set; }
internal Version Version { get; set; }
internal TcpClient TcpClient { get; set; }
internal CustomBinaryReader StreamReader { get; set; }
internal Stream Stream { get; set; }
internal DateTime LastAccess { get; set; }
/// <summary>
/// Connection cache
/// </summary>
static Dictionary<string, List<TcpConnection>> connectionCache = new Dictionary<string, List<TcpConnection>>();
/// <summary>
/// A lock to manage concurrency
/// </summary>
static SemaphoreSlim connectionAccessLock = new SemaphoreSlim(1);
internal TcpConnection()
/// <summary>
/// Get a TcpConnection to the specified host, port optionally HTTPS and a particular HTTP version
/// </summary>
/// <param name="hostname"></param>
/// <param name="port"></param>
/// <param name="isHttps"></param>
/// <param name="version"></param>
/// <returns></returns>
internal static async Task<TcpConnection> GetClient(string hostname, int port, bool isHttps, Version version)
{
LastAccess = DateTime.Now;
}
}
List<TcpConnection> cachedConnections = null;
TcpConnection cached = null;
internal class TcpConnectionManager
{
static List<TcpConnection> ConnectionCache = new List<TcpConnection>();
//Get a unique string to identify this connection
var key = GetConnectionKey(hostname, port, isHttps, version);
internal static async Task<TcpConnection> GetClient(SessionEventArgs sessionArgs, string hostname, int port, bool isSecure, Version version)
{
TcpConnection cached = null;
while (true)
{
lock (ConnectionCache)
await connectionAccessLock.WaitAsync();
try
{
cached = ConnectionCache.FirstOrDefault(x => x.HostName == hostname && x.port == port &&
x.IsSecure == isSecure && x.TcpClient.Connected && x.Version.Equals(version));
connectionCache.TryGetValue(key, out cachedConnections);
if (cached != null)
ConnectionCache.Remove(cached);
if (cachedConnections != null && cachedConnections.Count > 0)
{
cached = cachedConnections.First();
cachedConnections.Remove(cached);
}
else
{
cached = null;
}
}
finally { connectionAccessLock.Release(); }
if (cached != null && !cached.TcpClient.Client.IsConnected())
{
cached.TcpClient.Client.Dispose();
cached.TcpClient.Close();
continue;
}
if (cached == null)
break;
}
if (cached == null)
cached = await CreateClient(sessionArgs, hostname, port, isSecure, version).ConfigureAwait(false);
cached = await CreateClient(hostname, port, isHttps, version);
//just create one more preemptively
if (ConnectionCache.Where(x => x.HostName == hostname && x.port == port &&
x.IsSecure == isSecure && x.TcpClient.Connected && x.Version.Equals(version)).Count() < 2)
if (cachedConnections == null || cachedConnections.Count() <= 2)
{
var task = CreateClient(sessionArgs, hostname, port, isSecure, version)
.ContinueWith(x => { if (x.Status == TaskStatus.RanToCompletion) ReleaseClient(x.Result); });
var task = CreateClient(hostname, port, isHttps, version)
.ContinueWith(async (x) => { if (x.Status == TaskStatus.RanToCompletion) await ReleaseClient(x.Result); });
}
return cached;
}
private static async Task<TcpConnection> CreateClient(SessionEventArgs sessionArgs, string hostname, int port, bool isSecure, Version version)
/// <summary>
/// Get a string to identfiy the connection to a particular host, port
/// </summary>
/// <param name="hostname"></param>
/// <param name="port"></param>
/// <param name="isHttps"></param>
/// <param name="version"></param>
/// <returns></returns>
internal static string GetConnectionKey(string hostname, int port, bool isHttps, Version version)
{
return string.Format("{0}:{1}:{2}:{3}:{4}", hostname.ToLower(), port, isHttps, version.Major, version.Minor);
}
/// <summary>
/// Create connection to a particular host/port optionally with SSL and a particular HTTP version
/// </summary>
/// <param name="hostname"></param>
/// <param name="port"></param>
/// <param name="isHttps"></param>
/// <param name="version"></param>
/// <returns></returns>
private static async Task<TcpConnection> CreateClient(string hostname, int port, bool isHttps, Version version)
{
TcpClient client;
Stream stream;
if (isSecure)
if (isHttps)
{
CustomSslStream sslStream = null;
SslStream sslStream = null;
//If this proxy uses another external proxy then create a tunnel request for HTTPS connections
if (ProxyServer.UpStreamHttpsProxy != null)
{
client = new TcpClient(ProxyServer.UpStreamHttpsProxy.HostName, ProxyServer.UpStreamHttpsProxy.Port);
stream = (Stream)client.GetStream();
using (var writer = new StreamWriter(stream, Encoding.ASCII, Constants.BUFFER_SIZE, true))
using (var writer = new StreamWriter(stream, Encoding.ASCII, ProxyConstants.BUFFER_SIZE, true))
{
await writer.WriteLineAsync(string.Format("CONNECT {0}:{1} {2}", sessionArgs.WebSession.Request.RequestUri.Host, sessionArgs.WebSession.Request.RequestUri.Port, sessionArgs.WebSession.Request.HttpVersion));
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", sessionArgs.WebSession.Request.RequestUri.Host, sessionArgs.WebSession.Request.RequestUri.Port));
await writer.WriteLineAsync(string.Format("CONNECT {0}:{1} {2}", hostname, port, version));
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", hostname, port));
await writer.WriteLineAsync("Connection: Keep-Alive");
await writer.WriteLineAsync();
await writer.FlushAsync();
......@@ -100,12 +136,12 @@ namespace Titanium.Web.Proxy.Network
using (var reader = new CustomBinaryReader(stream))
{
var result = await reader.ReadLineAsync().ConfigureAwait(false);
var result = await reader.ReadLineAsync();
if (!result.ToLower().Contains("200 connection established"))
throw new Exception("Upstream proxy failed to create a secure tunnel");
await reader.ReadAllLinesAsync().ConfigureAwait(false);
await reader.ReadAllLinesAsync();
}
}
else
......@@ -116,9 +152,9 @@ namespace Titanium.Web.Proxy.Network
try
{
sslStream = new CustomSslStream(stream, true, new RemoteCertificateValidationCallback(ProxyServer.ValidateServerCertificate));
sslStream.Session = sessionArgs;
await sslStream.AuthenticateAsClientAsync(hostname, null, Constants.SupportedProtocols, false).ConfigureAwait(false);
sslStream = new SslStream(stream, true, new RemoteCertificateValidationCallback(ProxyServer.ValidateServerCertificate),
new LocalCertificateSelectionCallback(ProxyServer.SelectClientCertificate));
await sslStream.AuthenticateAsClientAsync(hostname, null, ProxyConstants.SupportedSslProtocols, false);
stream = (Stream)sslStream;
}
catch
......@@ -146,7 +182,7 @@ namespace Titanium.Web.Proxy.Network
{
HostName = hostname,
port = port,
IsSecure = isSecure,
IsHttps = isHttps,
TcpClient = client,
StreamReader = new CustomBinaryReader(stream),
Stream = stream,
......@@ -154,30 +190,67 @@ namespace Titanium.Web.Proxy.Network
};
}
/// <summary>
/// Returns a Tcp Connection back to cache for reuse by other requests
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
internal static async Task ReleaseClient(TcpConnection connection)
{
connection.LastAccess = DateTime.Now;
var key = GetConnectionKey(connection.HostName, connection.port, connection.IsHttps, connection.Version);
await connectionAccessLock.WaitAsync();
try
{
List<TcpConnection> cachedConnections;
connectionCache.TryGetValue(key, out cachedConnections);
internal static void ReleaseClient(TcpConnection Connection)
if (cachedConnections != null)
cachedConnections.Add(connection);
else
connectionCache.Add(key, new List<TcpConnection>() { connection });
}
finally { connectionAccessLock.Release(); }
}
private static bool clearConenctions { get; set; }
/// <summary>
/// Stop clearing idle connections
/// </summary>
internal static void StopClearIdleConnections()
{
Connection.LastAccess = DateTime.Now;
ConnectionCache.Add(Connection);
clearConenctions = false;
}
/// <summary>
/// A method to clear idle connections
/// </summary>
internal async static void ClearIdleConnections()
{
while (true)
clearConenctions = true;
while (clearConenctions)
{
lock (ConnectionCache)
await connectionAccessLock.WaitAsync();
try
{
var cutOff = DateTime.Now.AddSeconds(-60);
var cutOff = DateTime.Now.AddMinutes(-1 * ProxyServer.ConnectionCacheTimeOutMinutes);
ConnectionCache
connectionCache
.SelectMany(x => x.Value)
.Where(x => x.LastAccess < cutOff)
.ToList()
.ForEach(x => x.TcpClient.Close());
ConnectionCache.RemoveAll(x => x.LastAccess < cutOff);
connectionCache.ToList().ForEach(x => x.Value.RemoveAll(y => y.LastAccess < cutOff));
}
finally { connectionAccessLock.Release(); }
await Task.Delay(1000 * 60 * 3).ConfigureAwait(false);
//every minute run this
await Task.Delay(1000 * 60);
}
}
......
......@@ -22,19 +22,61 @@ namespace Titanium.Web.Proxy
static ProxyServer()
{
ProxyEndPoints = new List<ProxyEndPoint>();
Initialize();
//default values
ConnectionCacheTimeOutMinutes = 3;
CertificateCacheTimeOutMinutes = 60;
}
/// <summary>
/// Manages certificates used by this proxy
/// </summary>
private static CertificateManager CertManager { get; set; }
/// <summary>
/// Does the root certificate used by this proxy is trusted by the machine?
/// </summary>
private static bool certTrusted { get; set; }
/// <summary>
/// Is the proxy currently running
/// </summary>
private static bool proxyRunning { get; set; }
/// <summary>
/// Name of the root certificate issuer
/// </summary>
public static string RootCertificateIssuerName { get; set; }
/// <summary>
/// Name of the root certificate
/// </summary>
public static string RootCertificateName { get; set; }
/// <summary>
/// Does this proxy uses the HTTP protocol 100 continue behaviour strictly?
/// Broken 100 contunue implementations on server/client may cause problems if enabled
/// </summary>
public static bool Enable100ContinueBehaviour { get; set; }
/// <summary>
/// Minutes TCP connection cache to servers to be kept alive when in idle state
/// </summary>
public static int ConnectionCacheTimeOutMinutes { get; set; }
/// <summary>
/// Minutes certificates should be kept in cache when not used
/// </summary>
public static int CertificateCacheTimeOutMinutes { get; set; }
/// <summary>
/// Intercept request to server
/// </summary>
public static event Func<object, SessionEventArgs, Task> BeforeRequest;
/// <summary>
/// Intercept response from server
/// </summary>
public static event Func<object, SessionEventArgs, Task> BeforeResponse;
/// <summary>
......@@ -51,15 +93,39 @@ namespace Titanium.Web.Proxy
/// Verifies the remote Secure Sockets Layer (SSL) certificate used for authentication
/// </summary>
public static event Func<object, CertificateValidationEventArgs, Task> ServerCertificateValidationCallback;
/// <summary>
/// Callback tooverride client certificate during SSL mutual authentication
/// </summary>
public static event Func<object, CertificateSelectionEventArgs, Task> ClientCertificateSelectionCallback;
/// <summary>
/// A list of IpAddress & port this proxy is listening to
/// </summary>
public static List<ProxyEndPoint> ProxyEndPoints { get; set; }
/// <summary>
/// Initialize the proxy
/// </summary>
public static void Initialize()
{
TcpConnectionManager.ClearIdleConnections();
CertManager.ClearIdleCertificates();
}
/// <summary>
/// Quit the proxy
/// </summary>
public static void Quit()
{
TcpConnectionManager.StopClearIdleConnections();
CertManager.StopClearIdleCertificates();
}
/// <summary>
/// Add a proxy end point
/// </summary>
/// <param name="endPoint"></param>
public static void AddEndPoint(ProxyEndPoint endPoint)
{
ProxyEndPoints.Add(endPoint);
......@@ -68,6 +134,11 @@ namespace Titanium.Web.Proxy
Listen(endPoint);
}
/// <summary>
/// Remove a proxy end point
/// Will throw error if the end point does'nt exist
/// </summary>
/// <param name="endPoint"></param>
public static void RemoveEndPoint(ProxyEndPoint endPoint)
{
......@@ -80,10 +151,13 @@ namespace Titanium.Web.Proxy
QuitListen(endPoint);
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine
/// </summary>
/// <param name="endPoint"></param>
public static void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint)
{
VerifyProxy(endPoint);
ValidateEndPointAsSystemProxy(endPoint);
//clear any settings previously added
ProxyEndPoints.OfType<ExplicitProxyEndPoint>().ToList().ForEach(x => x.IsSystemHttpProxy = false);
......@@ -99,14 +173,21 @@ namespace Titanium.Web.Proxy
}
/// <summary>
/// Remove any HTTP proxy setting of current machien
/// </summary>
public static void DisableSystemHttpProxy()
{
SystemProxyHelper.RemoveHttpProxy();
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine
/// </summary>
/// <param name="endPoint"></param>
public static void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint)
{
VerifyProxy(endPoint);
ValidateEndPointAsSystemProxy(endPoint);
if (!endPoint.EnableSsl)
{
......@@ -133,16 +214,25 @@ namespace Titanium.Web.Proxy
Console.WriteLine("Set endpoint at Ip {1} and port: {2} as System HTTPS Proxy", endPoint.GetType().Name, endPoint.IpAddress, endPoint.Port);
}
/// <summary>
/// Remove any HTTPS proxy setting for current machine
/// </summary>
public static void DisableSystemHttpsProxy()
{
SystemProxyHelper.RemoveHttpsProxy();
}
/// <summary>
/// Clear all proxy settings for current machine
/// </summary>
public static void DisableAllSystemProxies()
{
SystemProxyHelper.DisableAllProxy();
}
/// <summary>
/// Start this proxy server
/// </summary>
public static void Start()
{
if (proxyRunning)
......@@ -153,7 +243,7 @@ namespace Titanium.Web.Proxy
CertManager = new CertificateManager(RootCertificateIssuerName,
RootCertificateName);
certTrusted = CertManager.CreateTrustedRootCertificate().Result;
foreach (var endPoint in ProxyEndPoints)
......@@ -161,9 +251,14 @@ namespace Titanium.Web.Proxy
Listen(endPoint);
}
Initialize();
proxyRunning = true;
}
/// <summary>
/// Stop this proxy server
/// </summary>
public static void Stop()
{
if (!proxyRunning)
......@@ -186,9 +281,15 @@ namespace Titanium.Web.Proxy
CertManager.Dispose();
Quit();
proxyRunning = false;
}
/// <summary>
/// Listen on the given end point on local machine
/// </summary>
/// <param name="endPoint"></param>
private static void Listen(ProxyEndPoint endPoint)
{
endPoint.listener = new TcpListener(endPoint.IpAddress, endPoint.Port);
......@@ -199,13 +300,20 @@ namespace Titanium.Web.Proxy
endPoint.listener.BeginAcceptTcpClient(OnAcceptConnection, endPoint);
}
/// <summary>
/// Quit listening on the given end point
/// </summary>
/// <param name="endPoint"></param>
private static void QuitListen(ProxyEndPoint endPoint)
{
endPoint.listener.Stop();
}
private static void VerifyProxy(ExplicitProxyEndPoint endPoint)
/// <summary>
/// Verifiy if its safe to set this end point as System proxy
/// </summary>
/// <param name="endPoint"></param>
private static void ValidateEndPointAsSystemProxy(ExplicitProxyEndPoint endPoint)
{
if (ProxyEndPoints.Contains(endPoint) == false)
throw new Exception("Cannot set endPoints not added to proxy as system proxy");
......@@ -214,12 +322,17 @@ namespace Titanium.Web.Proxy
throw new Exception("Cannot set system proxy settings before proxy has been started.");
}
/// <summary>
/// When a connection is received from client act
/// </summary>
/// <param name="asyn"></param>
private static void OnAcceptConnection(IAsyncResult asyn)
{
var endPoint = (ProxyEndPoint)asyn.AsyncState;
try
{
//based on end point type call appropriate request handlers
var client = endPoint.listener.EndAcceptTcpClient(asyn);
if (endPoint.GetType() == typeof(TransparentProxyEndPoint))
HandleClient(endPoint as TransparentProxyEndPoint, client);
......@@ -236,6 +349,10 @@ namespace Titanium.Web.Proxy
// so just return.
return;
}
catch
{
//Other errors are discarded to keep proxy running
}
}
......
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using System.Text.RegularExpressions;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Helpers;
......@@ -20,6 +18,9 @@ using System.Threading.Tasks;
namespace Titanium.Web.Proxy
{
/// <summary>
/// Handle the requesr
/// </summary>
partial class ProxyServer
{
//This is called when client is aware of proxy
......@@ -33,8 +34,7 @@ namespace Titanium.Web.Proxy
try
{
//read the first line HTTP command
var httpCmd = await clientStreamReader.ReadLineAsync().ConfigureAwait(false);
var httpCmd = await clientStreamReader.ReadLineAsync();
if (string.IsNullOrEmpty(httpCmd))
{
......@@ -43,8 +43,9 @@ namespace Titanium.Web.Proxy
}
//break up the line into three components (method, remote URL & Http Version)
var httpCmdSplit = httpCmd.Split(Constants.SpaceSplit, 3);
var httpCmdSplit = httpCmd.Split(ProxyConstants.SpaceSplit, 3);
//Find the request Verb
var httpVerb = httpCmdSplit[0];
if (httpVerb.ToUpper() == "CONNECT")
......@@ -52,39 +53,50 @@ namespace Titanium.Web.Proxy
else
httpRemoteUri = new Uri(httpCmdSplit[1]);
string httpVersion = "HTTP/1.1";
//parse the HTTP version
Version version = new Version(1, 1);
if (httpCmdSplit.Length == 3)
httpVersion = httpCmdSplit[2];
{
string httpVersion = httpCmdSplit[2].Trim();
var excluded = endPoint.ExcludedHttpsHostNameRegex != null ? endPoint.ExcludedHttpsHostNameRegex.Any(x => Regex.IsMatch(httpRemoteUri.Host, x)) : false;
if (httpVersion == "http/1.0")
{
version = new Version(1, 0);
}
}
var excluded = endPoint.ExcludedHttpsHostNameRegex != null ?
endPoint.ExcludedHttpsHostNameRegex.Any(x => Regex.IsMatch(httpRemoteUri.Host, x)) : false;
//Client wants to create a secure tcp tunnel (its a HTTPS request)
if (httpVerb.ToUpper() == "CONNECT" && !excluded && httpRemoteUri.Port != 80)
{
httpRemoteUri = new Uri("https://" + httpCmdSplit[1]);
await clientStreamReader.ReadAllLinesAsync().ConfigureAwait(false);
await WriteConnectResponse(clientStreamWriter, httpVersion).ConfigureAwait(false);
await clientStreamReader.ReadAllLinesAsync();
var certificate = await CertManager.CreateCertificate(httpRemoteUri.Host);
await WriteConnectResponse(clientStreamWriter, version);
SslStream sslStream = null;
try
{
sslStream = new SslStream(clientStream, true);
//create the Tcp Connection to server and then release it to connection cache
//Just doing what CONNECT request is asking as to do
var tunnelClient = await TcpConnectionManager.GetClient(httpRemoteUri.Host, httpRemoteUri.Port, true, version);
await TcpConnectionManager.ReleaseClient(tunnelClient);
sslStream = new SslStream(clientStream, true);
var certificate = await CertManager.CreateCertificate(httpRemoteUri.Host, false);
//Successfully managed to authenticate the client using the fake certificate
await sslStream.AuthenticateAsServerAsync(certificate, false,
Constants.SupportedProtocols, false).ConfigureAwait(false);
ProxyConstants.SupportedSslProtocols, false);
//HTTPS server created - we can now decrypt the client's traffic
clientStream = sslStream;
clientStreamReader = new CustomBinaryReader(sslStream);
clientStreamWriter = new StreamWriter(sslStream);
//HTTPS server created - we can now decrypt the client's traffic
clientStream = sslStream;
}
}
catch
{
if (sslStream != null)
......@@ -94,26 +106,29 @@ namespace Titanium.Web.Proxy
return;
}
httpCmd = await clientStreamReader.ReadLineAsync().ConfigureAwait(false);
//Now read the actual HTTPS request line
httpCmd = await clientStreamReader.ReadLineAsync();
}
//Sorry cannot do a HTTPS request decrypt to port 80 at this time
else if (httpVerb.ToUpper() == "CONNECT")
{
await clientStreamReader.ReadAllLinesAsync().ConfigureAwait(false);
await WriteConnectResponse(clientStreamWriter, httpVersion).ConfigureAwait(false);
//Cyphen out CONNECT request headers
await clientStreamReader.ReadAllLinesAsync();
//write back successfull CONNECT response
await WriteConnectResponse(clientStreamWriter, version);
//Just relay the request/response without decrypting it
await TcpHelper.SendRaw(clientStream, null, null, httpRemoteUri.Host, httpRemoteUri.Port,
false).ConfigureAwait(false);
false);
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null);
return;
}
//Now create the request
await HandleHttpSessionRequest(client, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
httpRemoteUri.Scheme == Uri.UriSchemeHttps ? true : false).ConfigureAwait(false);
httpRemoteUri.Scheme == Uri.UriSchemeHttps ? true : false);
}
catch
{
......@@ -139,13 +154,13 @@ namespace Titanium.Web.Proxy
// certificate = CertManager.CreateCertificate(hostName);
//}
//else
certificate = await CertManager.CreateCertificate(endPoint.GenericCertificateName);
certificate = await CertManager.CreateCertificate(endPoint.GenericCertificateName, false);
try
{
//Successfully managed to authenticate the client using the fake certificate
await sslStream.AuthenticateAsServerAsync(certificate, false,
SslProtocols.Tls, false).ConfigureAwait(false);
SslProtocols.Tls, false);
clientStreamReader = new CustomBinaryReader(sslStream);
clientStreamWriter = new StreamWriter(sslStream);
......@@ -167,19 +182,29 @@ namespace Titanium.Web.Proxy
clientStreamReader = new CustomBinaryReader(clientStream);
}
var httpCmd = await clientStreamReader.ReadLineAsync().ConfigureAwait(false);
var httpCmd = await clientStreamReader.ReadLineAsync();
//Now create the request
await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
true).ConfigureAwait(false);
true);
}
/// <summary>
/// This is the core request handler method for a particular connection from client
/// </summary>
/// <param name="client"></param>
/// <param name="httpCmd"></param>
/// <param name="clientStream"></param>
/// <param name="clientStreamReader"></param>
/// <param name="clientStreamWriter"></param>
/// <param name="isHttps"></param>
/// <returns></returns>
private static async Task HandleHttpSessionRequest(TcpClient client, string httpCmd, Stream clientStream,
CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, bool IsHttps)
CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, bool isHttps)
{
TcpConnection connection = null;
string lastRequestHostName = null;
//Loop through each subsequest request on this particular client connection
//(assuming HTTP connection is kept alive by client)
while (true)
{
if (string.IsNullOrEmpty(httpCmd))
......@@ -189,58 +214,59 @@ namespace Titanium.Web.Proxy
}
var args = new SessionEventArgs();
args.Client.TcpClient = client;
args.ProxyClient.TcpClient = client;
try
{
//break up the line into three components (method, remote URL & Http Version)
var httpCmdSplit = httpCmd.Split(Constants.SpaceSplit, 3);
var httpCmdSplit = httpCmd.Split(ProxyConstants.SpaceSplit, 3);
var httpMethod = httpCmdSplit[0];
var httpVersion = httpCmdSplit[2];
Version version;
if (httpVersion == "HTTP/1.1")
{
version = new Version(1, 1);
}
else
//find the request HTTP version
Version version = new Version(1, 1);
if (httpCmdSplit.Length == 3)
{
version = new Version(1, 0);
var httpVersion = httpCmdSplit[2].ToLower().Trim();
if (httpVersion == "http/1.0")
{
version = new Version(1, 0);
}
}
args.WebSession.Request.RequestHeaders = new List<HttpHeader>();
//Read the request headers
string tmpLine;
while (!string.IsNullOrEmpty(tmpLine = await clientStreamReader.ReadLineAsync().ConfigureAwait(false)))
while (!string.IsNullOrEmpty(tmpLine = await clientStreamReader.ReadLineAsync()))
{
var header = tmpLine.Split(new char[] { ':' }, 2);
var header = tmpLine.Split(ProxyConstants.ColonSplit, 2);
args.WebSession.Request.RequestHeaders.Add(new HttpHeader(header[0], header[1]));
}
var httpRemoteUri = new Uri(!IsHttps ? httpCmdSplit[1] : (string.Concat("https://", args.WebSession.Request.Host, httpCmdSplit[1])));
args.IsHttps = IsHttps;
var httpRemoteUri = new Uri(!isHttps ? httpCmdSplit[1] : (string.Concat("https://", args.WebSession.Request.Host, httpCmdSplit[1])));
#if DEBUG
//Just ignore local requests while Debugging
//Its annoying
if (httpRemoteUri.Host.Contains("localhost"))
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null);
break;
}
#endif
args.WebSession.Request.RequestUri = httpRemoteUri;
args.WebSession.Request.Method = httpMethod;
args.WebSession.Request.HttpVersion = httpVersion;
args.Client.ClientStream = clientStream;
args.Client.ClientStreamReader = clientStreamReader;
args.Client.ClientStreamWriter = clientStreamWriter;
if (args.WebSession.Request.UpgradeToWebSocket)
{
await TcpHelper.SendRaw(clientStream, httpCmd, args.WebSession.Request.RequestHeaders,
httpRemoteUri.Host, httpRemoteUri.Port, args.IsHttps).ConfigureAwait(false);
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args);
return;
}
args.WebSession.Request.HttpVersion = version;
args.ProxyClient.ClientStream = clientStream;
args.ProxyClient.ClientStreamReader = clientStreamReader;
args.ProxyClient.ClientStreamWriter = clientStreamWriter;
PrepareRequestHeaders(args.WebSession.Request.RequestHeaders, args.WebSession);
args.WebSession.Request.Host = args.WebSession.Request.RequestUri.Host;
args.WebSession.Request.Host = args.WebSession.Request.RequestUri.Authority;
//If requested interception
//If user requested interception do it
if (BeforeRequest != null)
{
Delegate[] invocationList = BeforeRequest.GetInvocationList();
......@@ -251,55 +277,64 @@ namespace Titanium.Web.Proxy
handlerTasks[i] = ((Func<object, SessionEventArgs, Task>)invocationList[i])(null, args);
}
await Task.WhenAll(handlerTasks).ConfigureAwait(false);
await Task.WhenAll(handlerTasks);
}
//construct the web request that we are going to issue on behalf of the client.
connection = connection == null ?
await TcpConnectionManager.GetClient(args, args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version).ConfigureAwait(false)
: lastRequestHostName != args.WebSession.Request.RequestUri.Host ? await TcpConnectionManager.GetClient(args, args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version).ConfigureAwait(false)
: connection;
//if upgrading to websocket then relay the requet without reading the contents
if (args.WebSession.Request.UpgradeToWebSocket)
{
await TcpHelper.SendRaw(clientStream, httpCmd, args.WebSession.Request.RequestHeaders,
httpRemoteUri.Host, httpRemoteUri.Port, args.IsHttps);
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args);
return;
}
lastRequestHostName = args.WebSession.Request.RequestUri.Host;
//construct the web request that we are going to issue on behalf of the client.
connection = await TcpConnectionManager.GetClient(args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.IsHttps, version);
args.WebSession.Request.RequestLocked = true;
//If request was cancelled by user then dispose the client
if (args.WebSession.Request.CancelRequest)
{
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
//if expect continue is enabled then send the headers first
//and see if server would return 100 conitinue
if (args.WebSession.Request.ExpectContinue)
{
args.WebSession.SetConnection(connection);
await args.WebSession.SendRequest().ConfigureAwait(false);
await args.WebSession.SendRequest();
}
//If 100 continue was the response inform that to the client
if (Enable100ContinueBehaviour)
if (args.WebSession.Request.Is100Continue)
{
WriteResponseStatus(args.WebSession.Response.HttpVersion, "100",
"Continue", args.Client.ClientStreamWriter);
await args.Client.ClientStreamWriter.WriteLineAsync();
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100",
"Continue", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
else if (args.WebSession.Request.ExpectationFailed)
{
WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.Client.ClientStreamWriter);
await args.Client.ClientStreamWriter.WriteLineAsync();
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
//If expect continue is not enabled then set the connectio and send request headers
if (!args.WebSession.Request.ExpectContinue)
{
args.WebSession.SetConnection(connection);
await args.WebSession.SendRequest().ConfigureAwait(false);
await args.WebSession.SendRequest();
}
//If request was modified by user
if (args.WebSession.Request.RequestBodyRead)
{
if(args.WebSession.Request.ContentEncoding!=null)
if (args.WebSession.Request.ContentEncoding != null)
{
args.WebSession.Request.RequestBody = await GetCompressedResponseBody(args.WebSession.Request.ContentEncoding, args.WebSession.Request.RequestBody);
}
......@@ -307,7 +342,7 @@ namespace Titanium.Web.Proxy
args.WebSession.Request.ContentLength = args.WebSession.Request.RequestBody.Length;
var newStream = args.WebSession.ServerConnection.Stream;
await newStream.WriteAsync(args.WebSession.Request.RequestBody, 0, args.WebSession.Request.RequestBody.Length).ConfigureAwait(false);
await newStream.WriteAsync(args.WebSession.Request.RequestBody, 0, args.WebSession.Request.RequestBody.Length);
}
else
{
......@@ -316,26 +351,29 @@ namespace Titanium.Web.Proxy
//If its a post/put request, then read the client html body and send it to server
if (httpMethod.ToUpper() == "POST" || httpMethod.ToUpper() == "PUT")
{
await SendClientRequestBody(args).ConfigureAwait(false);
await SendClientRequestBody(args);
}
}
}
//If not expectation failed response was returned by server then parse response
if (!args.WebSession.Request.ExpectationFailed)
{
await HandleHttpSessionResponse(args).ConfigureAwait(false);
await HandleHttpSessionResponse(args);
}
//if connection is closing exit
if (args.WebSession.Response.ResponseKeepAlive == false)
{
connection.TcpClient.Close();
Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args);
return;
}
// read the next request
httpCmd = await clientStreamReader.ReadLineAsync().ConfigureAwait(false);
//send the tcp connection to server back to connection cache for reuse
await TcpConnectionManager.ReleaseClient(connection);
// read the next request
httpCmd = await clientStreamReader.ReadLineAsync();
}
catch
......@@ -346,19 +384,28 @@ namespace Titanium.Web.Proxy
}
if (connection != null)
TcpConnectionManager.ReleaseClient(connection);
}
private static async Task WriteConnectResponse(StreamWriter clientStreamWriter, string httpVersion)
/// <summary>
/// Write successfull CONNECT response to client
/// </summary>
/// <param name="clientStreamWriter"></param>
/// <param name="httpVersion"></param>
/// <returns></returns>
private static async Task WriteConnectResponse(StreamWriter clientStreamWriter, Version httpVersion)
{
await clientStreamWriter.WriteLineAsync(httpVersion + " 200 Connection established").ConfigureAwait(false);
await clientStreamWriter.WriteLineAsync(string.Format("Timestamp: {0}", DateTime.Now)).ConfigureAwait(false);
await clientStreamWriter.WriteLineAsync().ConfigureAwait(false);
await clientStreamWriter.FlushAsync().ConfigureAwait(false);
await clientStreamWriter.WriteLineAsync(string.Format("HTTP/{0}.{1} {2}", httpVersion.Major, httpVersion.Minor, "200 Connection established"));
await clientStreamWriter.WriteLineAsync(string.Format("Timestamp: {0}", DateTime.Now));
await clientStreamWriter.WriteLineAsync();
await clientStreamWriter.FlushAsync();
}
private static void PrepareRequestHeaders(List<HttpHeader> requestHeaders, HttpWebSession webRequest)
/// <summary>
/// prepare the request headers so that we can avoid encodings not parsable by this proxy
/// </summary>
/// <param name="requestHeaders"></param>
/// <param name="webRequest"></param>
private static void PrepareRequestHeaders(List<HttpHeader> requestHeaders, HttpWebClient webRequest)
{
for (var i = 0; i < requestHeaders.Count; i++)
{
......@@ -376,6 +423,11 @@ namespace Titanium.Web.Proxy
FixRequestProxyHeaders(requestHeaders);
webRequest.Request.RequestHeaders = requestHeaders;
}
/// <summary>
/// Fix proxy specific headers
/// </summary>
/// <param name="headers"></param>
private static void FixRequestProxyHeaders(List<HttpHeader> headers)
{
//If proxy-connection close was returned inform to close the connection
......@@ -394,7 +446,12 @@ namespace Titanium.Web.Proxy
headers.RemoveAll(x => x.Name.ToLower() == "proxy-connection");
}
//This is called when the request is PUT/POST to read the body
/// <summary>
/// This is called when the request is PUT/POST to read the body
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private static async Task SendClientRequestBody(SessionEventArgs args)
{
// End the operation
......@@ -404,7 +461,7 @@ namespace Titanium.Web.Proxy
{
try
{
await args.Client.ClientStreamReader.CopyBytesToStream(postStream, args.WebSession.Request.ContentLength).ConfigureAwait(false);
await args.ProxyClient.ClientStreamReader.CopyBytesToStream(postStream, args.WebSession.Request.ContentLength);
}
catch
{
......@@ -416,7 +473,7 @@ namespace Titanium.Web.Proxy
{
try
{
await args.Client.ClientStreamReader.CopyBytesToStreamChunked(postStream).ConfigureAwait(false);
await args.ProxyClient.ClientStreamReader.CopyBytesToStreamChunked(postStream);
}
catch
{
......@@ -425,58 +482,5 @@ namespace Titanium.Web.Proxy
}
}
/// <summary>
/// Call back to override server certificate validation
/// </summary>
/// <param name="sender"></param>
/// <param name="certificate"></param>
/// <param name="chain"></param>
/// <param name="sslPolicyErrors"></param>
/// <returns></returns>
internal static bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
var param = sender as CustomSslStream;
if (ServerCertificateValidationCallback != null)
{
var args = new CertificateValidationEventArgs();
args.Session = param.Session;
args.Certificate = certificate;
args.Chain = chain;
args.SslPolicyErrors = sslPolicyErrors;
Delegate[] invocationList = ServerCertificateValidationCallback.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, CertificateValidationEventArgs, Task>)invocationList[i])(null, args);
}
Task.WhenAll(handlerTasks).Wait();
if (!args.IsValid)
{
param.Session.WebSession.Request.CancelRequest = true;
}
return args.IsValid;
}
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
//By default
//do not allow this client to communicate with unauthenticated servers.
return false;
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Compression;
using Titanium.Web.Proxy.Shared;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Extensions;
namespace Titanium.Web.Proxy
{
/// <summary>
/// Handle the response from server
/// </summary>
partial class ProxyServer
{
//Called asynchronously when a request was successfully and we received the response
public static async Task HandleHttpSessionResponse(SessionEventArgs args)
{
await args.WebSession.ReceiveResponse().ConfigureAwait(false);
//read response & headers from server
await args.WebSession.ReceiveResponse();
try
{
if (!args.WebSession.Response.ResponseBodyRead)
args.WebSession.Response.ResponseStream = args.WebSession.ServerConnection.Stream;
//If user requested call back then do it
if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked)
{
Delegate[] invocationList = BeforeResponse.GetInvocationList();
......@@ -37,26 +38,28 @@ namespace Titanium.Web.Proxy
handlerTasks[i] = ((Func<object, SessionEventArgs, Task>)invocationList[i])(null, args);
}
await Task.WhenAll(handlerTasks).ConfigureAwait(false);
await Task.WhenAll(handlerTasks);
}
args.WebSession.Response.ResponseLocked = true;
//Write back to client 100-conitinue response if that's what server returned
if (args.WebSession.Response.Is100Continue)
{
WriteResponseStatus(args.WebSession.Response.HttpVersion, "100",
"Continue", args.Client.ClientStreamWriter);
await args.Client.ClientStreamWriter.WriteLineAsync();
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100",
"Continue", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
else if (args.WebSession.Response.ExpectationFailed)
{
WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.Client.ClientStreamWriter);
await args.Client.ClientStreamWriter.WriteLineAsync();
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
WriteResponseStatus(args.WebSession.Response.HttpVersion, args.WebSession.Response.ResponseStatusCode,
args.WebSession.Response.ResponseStatusDescription, args.Client.ClientStreamWriter);
//Write back response status to client
await WriteResponseStatus(args.WebSession.Response.HttpVersion, args.WebSession.Response.ResponseStatusCode,
args.WebSession.Response.ResponseStatusDescription, args.ProxyClient.ClientStreamWriter);
if (args.WebSession.Response.ResponseBodyRead)
{
......@@ -65,7 +68,7 @@ namespace Titanium.Web.Proxy
if (contentEncoding != null)
{
args.WebSession.Response.ResponseBody = await GetCompressedResponseBody(contentEncoding, args.WebSession.Response.ResponseBody).ConfigureAwait(false);
args.WebSession.Response.ResponseBody = await GetCompressedResponseBody(contentEncoding, args.WebSession.Response.ResponseBody);
if (isChunked == false)
args.WebSession.Response.ContentLength = args.WebSession.Response.ResponseBody.Length;
......@@ -73,23 +76,24 @@ namespace Titanium.Web.Proxy
args.WebSession.Response.ContentLength = -1;
}
await WriteResponseHeaders(args.Client.ClientStreamWriter, args.WebSession.Response.ResponseHeaders).ConfigureAwait(false);
await WriteResponseBody(args.Client.ClientStream, args.WebSession.Response.ResponseBody, isChunked).ConfigureAwait(false);
await WriteResponseHeaders(args.ProxyClient.ClientStreamWriter, args.WebSession.Response.ResponseHeaders);
await args.ProxyClient.ClientStream.WriteResponseBody(args.WebSession.Response.ResponseBody, isChunked);
}
else
{
await WriteResponseHeaders(args.Client.ClientStreamWriter, args.WebSession.Response.ResponseHeaders);
await WriteResponseHeaders(args.ProxyClient.ClientStreamWriter, args.WebSession.Response.ResponseHeaders);
if (args.WebSession.Response.IsChunked || args.WebSession.Response.ContentLength > 0 || args.WebSession.Response.HttpVersion.ToLower().Trim() == "http/1.0")
await WriteResponseBody(args.WebSession.ServerConnection.StreamReader, args.Client.ClientStream, args.WebSession.Response.IsChunked, args.WebSession.Response.ContentLength).ConfigureAwait(false);
if (args.WebSession.Response.IsChunked || args.WebSession.Response.ContentLength > 0 ||
(args.WebSession.Response.HttpVersion.Major == 1 && args.WebSession.Response.HttpVersion.Minor == 0))
await args.WebSession.ServerConnection.StreamReader.WriteResponseBody(args.ProxyClient.ClientStream, args.WebSession.Response.IsChunked, args.WebSession.Response.ContentLength);
}
await args.Client.ClientStream.FlushAsync();
await args.ProxyClient.ClientStream.FlushAsync();
}
catch
{
Dispose(args.Client.TcpClient, args.Client.ClientStream, args.Client.ClientStreamReader, args.Client.ClientStreamWriter, args);
Dispose(args.ProxyClient.TcpClient, args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader, args.ProxyClient.ClientStreamWriter, args);
}
finally
{
......@@ -97,20 +101,39 @@ namespace Titanium.Web.Proxy
}
}
/// <summary>
/// get the compressed response body from give response bytes
/// </summary>
/// <param name="encodingType"></param>
/// <param name="responseBodyStream"></param>
/// <returns></returns>
private static async Task<byte[]> GetCompressedResponseBody(string encodingType, byte[] responseBodyStream)
{
var compressionFactory = new CompressionFactory();
var compressor = compressionFactory.Create(encodingType);
return await compressor.Compress(responseBodyStream).ConfigureAwait(false);
return await compressor.Compress(responseBodyStream);
}
private static void WriteResponseStatus(string version, string code, string description,
/// <summary>
/// Write response status
/// </summary>
/// <param name="version"></param>
/// <param name="code"></param>
/// <param name="description"></param>
/// <param name="responseWriter"></param>
/// <returns></returns>
private static async Task WriteResponseStatus(Version version, string code, string description,
StreamWriter responseWriter)
{
responseWriter.WriteLineAsync(string.Format("{0} {1} {2}", version, code, description));
await responseWriter.WriteLineAsync(string.Format("HTTP/{0}.{1} {2} {3}", version.Major, version.Minor, code, description));
}
/// <summary>
/// Write response headers to client
/// </summary>
/// <param name="responseWriter"></param>
/// <param name="headers"></param>
/// <returns></returns>
private static async Task WriteResponseHeaders(StreamWriter responseWriter, List<HttpHeader> headers)
{
if (headers != null)
......@@ -126,6 +149,11 @@ namespace Titanium.Web.Proxy
await responseWriter.WriteLineAsync();
await responseWriter.FlushAsync();
}
/// <summary>
/// Fix the proxy specific headers before sending response headers to client
/// </summary>
/// <param name="headers"></param>
private static void FixResponseProxyHeaders(List<HttpHeader> headers)
{
//If proxy-connection close was returned inform to close the connection
......@@ -139,101 +167,20 @@ namespace Titanium.Web.Proxy
}
else
{
connectionHeader.Value = "close";
connectionHeader.Value = proxyHeader.Value;
}
headers.RemoveAll(x => x.Name.ToLower() == "proxy-connection");
}
private static async Task WriteResponseBody(Stream clientStream, byte[] data, bool isChunked)
{
if (!isChunked)
{
await clientStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
}
else
await WriteResponseBodyChunked(data, clientStream).ConfigureAwait(false);
}
private static async Task WriteResponseBody(CustomBinaryReader inStreamReader, Stream outStream, bool isChunked, long ContentLength)
{
if (!isChunked)
{
//http 1.0
if (ContentLength == -1)
ContentLength = long.MaxValue;
int bytesToRead = Constants.BUFFER_SIZE;
if (ContentLength < Constants.BUFFER_SIZE)
bytesToRead = (int)ContentLength;
var buffer = new byte[Constants.BUFFER_SIZE];
var bytesRead = 0;
var totalBytesRead = 0;
while ((bytesRead += await inStreamReader.BaseStream.ReadAsync(buffer, 0, bytesToRead).ConfigureAwait(false)) > 0)
{
await outStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (totalBytesRead == ContentLength)
break;
bytesRead = 0;
var remainingBytes = (ContentLength - totalBytesRead);
bytesToRead = remainingBytes > (long)Constants.BUFFER_SIZE ? Constants.BUFFER_SIZE : (int)remainingBytes;
}
}
else
await WriteResponseBodyChunked(inStreamReader, outStream).ConfigureAwait(false);
}
//Send chunked response
private static async Task WriteResponseBodyChunked(CustomBinaryReader inStreamReader, Stream outStream)
{
while (true)
{
var chunkHead = await inStreamReader.ReadLineAsync().ConfigureAwait(false);
var chunkSize = int.Parse(chunkHead, NumberStyles.HexNumber);
if (chunkSize != 0)
{
var buffer = await inStreamReader.ReadBytesAsync(chunkSize).ConfigureAwait(false);
var chunkHeadBytes = Encoding.ASCII.GetBytes(chunkSize.ToString("x2"));
await outStream.WriteAsync(chunkHeadBytes, 0, chunkHeadBytes.Length).ConfigureAwait(false);
await outStream.WriteAsync(Constants.NewLineBytes, 0, Constants.NewLineBytes.Length).ConfigureAwait(false);
await outStream.WriteAsync(buffer, 0, chunkSize).ConfigureAwait(false);
await outStream.WriteAsync(Constants.NewLineBytes, 0, Constants.NewLineBytes.Length).ConfigureAwait(false);
await inStreamReader.ReadLineAsync().ConfigureAwait(false);
}
else
{
await inStreamReader.ReadLineAsync().ConfigureAwait(false);
await outStream.WriteAsync(Constants.ChunkEnd, 0, Constants.ChunkEnd.Length).ConfigureAwait(false);
break;
}
}
}
private static async Task WriteResponseBodyChunked(byte[] data, Stream outStream)
{
var chunkHead = Encoding.ASCII.GetBytes(data.Length.ToString("x2"));
await outStream.WriteAsync(chunkHead, 0, chunkHead.Length).ConfigureAwait(false);
await outStream.WriteAsync(Constants.NewLineBytes, 0, Constants.NewLineBytes.Length).ConfigureAwait(false);
await outStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
await outStream.WriteAsync(Constants.NewLineBytes, 0, Constants.NewLineBytes.Length).ConfigureAwait(false);
await outStream.WriteAsync(Constants.ChunkEnd, 0, Constants.ChunkEnd.Length).ConfigureAwait(false);
}
/// <summary>
/// Handle dispose of a client/server session
/// </summary>
/// <param name="client"></param>
/// <param name="clientStream"></param>
/// <param name="clientStreamReader"></param>
/// <param name="clientStreamWriter"></param>
/// <param name="args"></param>
private static void Dispose(TcpClient client, IDisposable clientStream, IDisposable clientStreamReader,
IDisposable clientStreamWriter, IDisposable args)
{
......
using System;
using System.Security.Authentication;
using System.Text;
using System.Text.RegularExpressions;
namespace Titanium.Web.Proxy.Shared
{
/// <summary>
/// Literals shared by Proxy Server
/// </summary>
internal class Constants
{
public static readonly int BUFFER_SIZE = 8192;
internal class ProxyConstants
{
internal static readonly char[] SpaceSplit = { ' ' };
internal static readonly char[] ColonSplit = { ':' };
internal static readonly char[] SemiColonSplit = { ';' };
......@@ -21,6 +18,8 @@ namespace Titanium.Web.Proxy.Shared
internal static readonly byte[] ChunkEnd =
Encoding.ASCII.GetBytes(0.ToString("x2") + Environment.NewLine + Environment.NewLine);
internal static SslProtocols SupportedProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Ssl3;
public static SslProtocols SupportedSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Ssl3;
public static readonly int BUFFER_SIZE = 8192;
}
}
......@@ -49,6 +49,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="CertificateHandler.cs" />
<Compile Include="Compression\CompressionFactory.cs" />
<Compile Include="Compression\DeflateCompression.cs" />
<Compile Include="Compression\GZipCompression.cs" />
......@@ -60,12 +61,15 @@
<Compile Include="Decompression\GZipDecompression.cs" />
<Compile Include="Decompression\IDecompression.cs" />
<Compile Include="Decompression\ZlibDecompression.cs" />
<Compile Include="EventArguments\CertificateSelectionEventArgs.cs" />
<Compile Include="EventArguments\CertificateValidationEventArgs.cs" />
<Compile Include="Extensions\ByteArrayExtensions.cs" />
<Compile Include="Network\CachedCertificate.cs" />
<Compile Include="Network\ProxyClient.cs" />
<Compile Include="Exceptions\BodyNotFoundException.cs" />
<Compile Include="Extensions\HttpWebResponseExtensions.cs" />
<Compile Include="Extensions\HttpWebRequestExtensions.cs" />
<Compile Include="Helpers\CertificateManager.cs" />
<Compile Include="Network\CertificateManager.cs" />
<Compile Include="Helpers\Firefox.cs" />
<Compile Include="Helpers\SystemProxy.cs" />
<Compile Include="Models\EndPoint.cs" />
......@@ -73,7 +77,7 @@
<Compile Include="Http\Request.cs" />
<Compile Include="Http\Response.cs" />
<Compile Include="Models\ExternalProxy.cs" />
<Compile Include="Network\CustomSslStream.cs" />
<Compile Include="Network\TcpConnection.cs" />
<Compile Include="Network\TcpConnectionManager.cs" />
<Compile Include="Models\HttpHeader.cs" />
<Compile Include="Http\HttpWebClient.cs" />
......@@ -87,7 +91,7 @@
<Compile Include="Extensions\StreamExtensions.cs" />
<Compile Include="Http\Responses\OkResponse.cs" />
<Compile Include="Http\Responses\RedirectResponse.cs" />
<Compile Include="Shared\Constants.cs" />
<Compile Include="Shared\ProxyConstants.cs" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
......
......@@ -7,7 +7,7 @@
# - Section names should be unique on each level.
# version format
version: 2.2{build}
version: 2.3000.{build}
shallow_clone: true
......
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