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