Commit 2316ea2d authored by Jehonathan's avatar Jehonathan Committed by GitHub

Merge pull request #142 from justcoding121/release

Merge Beta with Stable branch
parents 14016520 fa9a02c4
......@@ -13,6 +13,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
public ProxyTestController()
{
proxyServer = new ProxyServer();
proxyServer.TrustRootCertificate = true;
}
public void StartProxy()
......@@ -115,6 +116,9 @@ namespace Titanium.Web.Proxy.Examples.Basic
//read response headers
var responseHeaders = e.WebSession.Response.ResponseHeaders;
// print out process id of current session
Console.WriteLine($"PID: {e.WebSession.ProcessId.Value}");
//if (!e.ProxySession.Request.Host.Equals("medeczane.sgk.gov.tr")) return;
if (e.WebSession.Request.Method == "GET" || e.WebSession.Request.Method == "POST")
{
......
Doneness:
- [ ] Build is okay - I made sure that this change is building successfully.
- [ ] No Bugs - I made sure that this change is working properly as expected. It does'nt have any bugs that you are aware of.
- [ ] Branching - If this is not a hotfix, I am making this request against release branch (aka beta branch)
......@@ -17,6 +17,8 @@ Features
* Safely relays WebSocket requests over Http
* Support mutual SSL authentication
* Fully asynchronous proxy
* Supports proxy authentication
Usage
=====
......@@ -25,18 +27,22 @@ Refer the HTTP Proxy Server library in your project, look up Test project to lea
Install by nuget:
Install-Package Titanium.Web.Proxy
For beta releases on [release branch](https://github.com/justcoding121/Titanium-Web-Proxy/tree/release)
After installing nuget package mark following files to be copied to app directory
Install-Package Titanium.Web.Proxy -Pre
* makecert.exe
For stable releases on [master branch](https://github.com/justcoding121/Titanium-Web-Proxy/tree/master)
Install-Package Titanium.Web.Proxy
Setup HTTP proxy:
```csharp
var proxyServer = new ProxyServer();
//locally trust root certificate used by this proxy
proxyServer.TrustRootCertificate = true;
proxyServer.BeforeRequest += OnRequest;
proxyServer.BeforeResponse += OnResponse;
proxyServer.ServerCertificateValidationCallback += OnCertificateValidation;
......@@ -69,8 +75,8 @@ Setup HTTP proxy:
};
proxyServer.AddEndPoint(transparentEndPoint);
//proxyServer.ExternalHttpProxy = new ExternalProxy() { HostName = "localhost", Port = 8888 };
//proxyServer.ExternalHttpsProxy = new ExternalProxy() { HostName = "localhost", Port = 8888 };
//proxyServer.UpStreamHttpProxy = new ExternalProxy() { HostName = "localhost", Port = 8888 };
//proxyServer.UpStreamHttpsProxy = new ExternalProxy() { HostName = "localhost", Port = 8888 };
foreach (var endPoint in proxyServer.ProxyEndPoints)
Console.WriteLine("Listening on '{0}' endpoint at Ip {1} and port: {2} ",
......@@ -176,7 +182,10 @@ Sample request and response event handlers
```
Future roadmap
============
* Implement Kerberos/NTLM authentication over HTTP protocols for windows domain
* Support Server Name Indication (SNI) for transparent endpoints
* Support HTTP 2.0
* Support upstream AutoProxy detection
* Support SOCKS protocol
* Implement Kerberos/NTLM authentication over HTTP protocols for windows domain
......@@ -20,7 +20,8 @@ namespace Titanium.Web.Proxy.UnitTests
{
var tasks = new List<Task>();
var mgr = new CertificateManager("Titanium","Titanium Root Certificate Authority");
var mgr = new CertificateManager("Titanium", "Titanium Root Certificate Authority",
new Lazy<Action<Exception>>(() => (e => { })).Value);
mgr.ClearIdleCertificates(1);
......@@ -33,9 +34,9 @@ namespace Titanium.Web.Proxy.UnitTests
await Task.Delay(random.Next(0, 10) * 1000);
//get the connection
var certificate = await mgr.CreateCertificate(host, false);
var certificate = mgr.CreateCertificate(host, false);
Assert.IsNotNull(certificate);
Assert.IsNotNull(certificate);
}));
......
using System;
using System.Net;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.UnitTests
{
[TestClass]
public class ProxyServerTests
{
[TestMethod]
public void GivenOneEndpointIsAlreadyAddedToAddress_WhenAddingNewEndpointToExistingAddress_ThenExceptionIsThrown()
{
// Arrange
var proxy = new ProxyServer();
const int port = 9999;
var firstIpAddress = IPAddress.Parse("127.0.0.1");
var secondIpAddress = IPAddress.Parse("127.0.0.1");
proxy.AddEndPoint(new ExplicitProxyEndPoint(firstIpAddress, port, false));
// Act
try
{
proxy.AddEndPoint(new ExplicitProxyEndPoint(secondIpAddress, port, false));
}
catch (Exception exc)
{
// Assert
StringAssert.Contains(exc.Message, "Cannot add another endpoint to same port");
return;
}
Assert.Fail("An exception should be thrown by now");
}
[TestMethod]
public void GivenOneEndpointIsAlreadyAddedToAddress_WhenAddingNewEndpointToExistingAddress_ThenTwoEndpointsExists()
{
// Arrange
var proxy = new ProxyServer();
const int port = 9999;
var firstIpAddress = IPAddress.Parse("127.0.0.1");
var secondIpAddress = IPAddress.Parse("192.168.1.1");
proxy.AddEndPoint(new ExplicitProxyEndPoint(firstIpAddress, port, false));
// Act
proxy.AddEndPoint(new ExplicitProxyEndPoint(secondIpAddress, port, false));
// Assert
Assert.AreEqual(2, proxy.ProxyEndPoints.Count);
}
[TestMethod]
public void GivenOneEndpointIsAlreadyAddedToPort_WhenAddingNewEndpointToExistingPort_ThenExceptionIsThrown()
{
// Arrange
var proxy = new ProxyServer();
const int port = 9999;
proxy.AddEndPoint(new ExplicitProxyEndPoint(IPAddress.Loopback, port, false));
// Act
try
{
proxy.AddEndPoint(new ExplicitProxyEndPoint(IPAddress.Loopback, port, false));
}
catch (Exception exc)
{
// Assert
StringAssert.Contains(exc.Message, "Cannot add another endpoint to same port");
return;
}
Assert.Fail("An exception should be thrown by now");
}
[TestMethod]
public void GivenOneEndpointIsAlreadyAddedToZeroPort_WhenAddingNewEndpointToExistingPort_ThenTwoEndpointsExists()
{
// Arrange
var proxy = new ProxyServer();
const int port = 0;
proxy.AddEndPoint(new ExplicitProxyEndPoint(IPAddress.Loopback, port, false));
// Act
proxy.AddEndPoint(new ExplicitProxyEndPoint(IPAddress.Loopback, port, false));
// Assert
Assert.AreEqual(2, proxy.ProxyEndPoints.Count);
}
}
}
......@@ -52,6 +52,7 @@
<ItemGroup>
<Compile Include="CertificateManagerTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ProxyServerTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj">
......
......@@ -60,11 +60,12 @@ namespace Titanium.Web.Proxy
/// 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>
/// <param name="targetHost"></param>
/// <param name="localCertificates"></param>
/// <param name="remoteCertificate"></param>
/// <param name="acceptableIssuers"></param>
/// <returns></returns>
internal X509Certificate SelectClientCertificate(
internal X509Certificate SelectClientCertificate(
object sender,
string targetHost,
X509CertificateCollection localCertificates,
......@@ -101,11 +102,11 @@ namespace Titanium.Web.Proxy
{
var args = new CertificateSelectionEventArgs();
args.targetHost = targetHost;
args.localCertificates = localCertificates;
args.remoteCertificate = remoteCertificate;
args.acceptableIssuers = acceptableIssuers;
args.clientCertificate = clientCertificate;
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];
......@@ -117,7 +118,7 @@ namespace Titanium.Web.Proxy
Task.WhenAll(handlerTasks).Wait();
return args.clientCertificate;
return args.ClientCertificate;
}
return clientCertificate;
......
......@@ -6,19 +6,15 @@ 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 class CertificateSelectionEventArgs : EventArgs
{
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 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 X509Certificate ClientCertificate { get; set; }
public void Dispose()
{
throw new NotImplementedException();
}
}
}
using System;
using System.IO;
using System.Text;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Decompression;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Http.Responses;
using Titanium.Web.Proxy.Extensions;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Network;
using System.Net;
namespace Titanium.Web.Proxy.EventArguments
{
/// <summary>
/// Holds info related to a single proxy session (single request/response sequence)
/// A proxy session is bounded to a single connection from client
/// A proxy session ends when client terminates connection to proxy
/// or when server terminates connection from proxy
/// </summary>
public class SessionEventArgs : EventArgs, IDisposable
{
/// <summary>
/// Size of Buffers used by this object
/// </summary>
private readonly int bufferSize;
/// <summary>
/// Holds a reference to proxy response handler method
/// </summary>
private readonly Func<SessionEventArgs, Task> httpResponseHandler;
/// <summary>
/// Holds a reference to client
/// </summary>
internal ProxyClient ProxyClient { get; set; }
/// <summary>
/// Does this session uses SSL
/// </summary>
public bool IsHttps => WebSession.Request.RequestUri.Scheme == Uri.UriSchemeHttps;
public IPEndPoint ClientEndPoint => (IPEndPoint)ProxyClient.TcpClient.Client.RemoteEndPoint;
/// <summary>
/// A web session corresponding to a single request/response sequence
/// within a proxy connection
/// </summary>
public HttpWebClient WebSession { get; set; }
/// <summary>
/// Constructor to initialize the proxy
/// </summary>
internal SessionEventArgs(int bufferSize, Func<SessionEventArgs, Task> httpResponseHandler)
{
this.bufferSize = bufferSize;
this.httpResponseHandler = httpResponseHandler;
ProxyClient = new ProxyClient();
WebSession = new HttpWebClient();
}
/// <summary>
/// Read request body content as bytes[] for current session
/// </summary>
private async Task ReadRequestBody()
{
//GET request don't have a request body to read
if ((WebSession.Request.Method.ToUpper() != "POST" && WebSession.Request.Method.ToUpper() != "PUT"))
{
throw new BodyNotFoundException("Request don't have a body." +
"Please verify that this request is a Http POST/PUT and request " +
"content length is greater than zero before accessing the body.");
}
//Caching check
if (WebSession.Request.RequestBody == null)
{
//If chunked then its easy just read the whole body with the content length mentioned in the request header
using (var requestBodyStream = new MemoryStream())
{
//For chunked request we need to read data as they arrive, until we reach a chunk end symbol
if (WebSession.Request.IsChunked)
{
await this.ProxyClient.ClientStreamReader.CopyBytesToStreamChunked(bufferSize, requestBodyStream);
}
else
{
//If not chunked then its easy just read the whole body with the content length mentioned in the request header
if (WebSession.Request.ContentLength > 0)
{
//If not chunked then its easy just read the amount of bytes mentioned in content length header of response
await this.ProxyClient.ClientStreamReader.CopyBytesToStream(bufferSize, requestBodyStream,
WebSession.Request.ContentLength);
}
else if (WebSession.Request.HttpVersion.Major == 1 && WebSession.Request.HttpVersion.Minor == 0)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(bufferSize, requestBodyStream, long.MaxValue);
}
}
WebSession.Request.RequestBody = await GetDecompressedResponseBody(WebSession.Request.ContentEncoding,
requestBodyStream.ToArray());
}
//Now set the flag to true
//So that next time we can deliver body from cache
WebSession.Request.RequestBodyRead = true;
}
}
/// <summary>
/// Read response body as byte[] for current response
/// </summary>
private async Task ReadResponseBody()
{
//If not already read (not cached yet)
if (WebSession.Response.ResponseBody == null)
{
using (var responseBodyStream = new MemoryStream())
{
//If chuncked the read chunk by chunk until we hit chunk end symbol
if (WebSession.Response.IsChunked)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStreamChunked(bufferSize, responseBodyStream);
}
else
{
if (WebSession.Response.ContentLength > 0)
{
//If not chunked then its easy just read the amount of bytes mentioned in content length header of response
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(bufferSize, responseBodyStream,
WebSession.Response.ContentLength);
}
else if (WebSession.Response.HttpVersion.Major == 1 && WebSession.Response.HttpVersion.Minor == 0)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(bufferSize, responseBodyStream, long.MaxValue);
}
}
WebSession.Response.ResponseBody = await GetDecompressedResponseBody(WebSession.Response.ContentEncoding,
responseBodyStream.ToArray());
}
//set this to true for caching
WebSession.Response.ResponseBodyRead = true;
}
}
/// <summary>
/// Gets the request body as bytes
/// </summary>
/// <returns></returns>
public async Task<byte[]> GetRequestBody()
{
if (!WebSession.Request.RequestBodyRead)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
await ReadRequestBody();
}
return WebSession.Request.RequestBody;
}
/// <summary>
/// Gets the request body as string
/// </summary>
/// <returns></returns>
public async Task<string> GetRequestBodyAsString()
{
if (!WebSession.Request.RequestBodyRead)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
await ReadRequestBody();
}
//Use the encoding specified in request to decode the byte[] data to string
return WebSession.Request.RequestBodyString ?? (WebSession.Request.RequestBodyString = WebSession.Request.Encoding.GetString(WebSession.Request.RequestBody));
}
/// <summary>
/// Sets the request body
/// </summary>
/// <param name="body"></param>
public async Task SetRequestBody(byte[] body)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
//syphon out the request body from client before setting the new body
if (!WebSession.Request.RequestBodyRead)
{
await ReadRequestBody();
}
WebSession.Request.RequestBody = body;
if (WebSession.Request.IsChunked == false)
{
WebSession.Request.ContentLength = body.Length;
}
else
{
WebSession.Request.ContentLength = -1;
}
}
/// <summary>
/// Sets the body with the specified string
/// </summary>
/// <param name="body"></param>
public async Task SetRequestBodyString(string body)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
//syphon out the request body from client before setting the new body
if (!WebSession.Request.RequestBodyRead)
{
await ReadRequestBody();
}
await SetRequestBody(WebSession.Request.Encoding.GetBytes(body));
}
/// <summary>
/// Gets the response body as byte array
/// </summary>
/// <returns></returns>
public async Task<byte[]> GetResponseBody()
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
await ReadResponseBody();
return WebSession.Response.ResponseBody;
}
/// <summary>
/// Gets the response body as string
/// </summary>
/// <returns></returns>
public async Task<string> GetResponseBodyAsString()
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
await GetResponseBody();
return WebSession.Response.ResponseBodyString ??
(WebSession.Response.ResponseBodyString = WebSession.Response.Encoding.GetString(WebSession.Response.ResponseBody));
}
/// <summary>
/// Set the response body bytes
/// </summary>
/// <param name="body"></param>
public async Task SetResponseBody(byte[] body)
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
//syphon out the response body from server before setting the new body
if (WebSession.Response.ResponseBody == null)
{
await GetResponseBody();
}
WebSession.Response.ResponseBody = body;
//If there is a content length header update it
if (WebSession.Response.IsChunked == false)
{
WebSession.Response.ContentLength = body.Length;
}
else
{
WebSession.Response.ContentLength = -1;
}
}
/// <summary>
/// Replace the response body with the specified string
/// </summary>
/// <param name="body"></param>
public async Task SetResponseBodyString(string body)
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
//syphon out the response body from server before setting the new body
if (WebSession.Response.ResponseBody == null)
{
await GetResponseBody();
}
var bodyBytes = WebSession.Response.Encoding.GetBytes(body);
await SetResponseBody(bodyBytes);
}
private async Task<byte[]> GetDecompressedResponseBody(string encodingType, byte[] responseBodyStream)
{
var decompressionFactory = new DecompressionFactory();
var decompressor = decompressionFactory.Create(encodingType);
return await decompressor.Decompress(responseBodyStream, bufferSize);
}
/// <summary>
/// Before request is made to server
/// Respond with the specified HTML string to client
/// and ignore the request
/// </summary>
/// <param name="html"></param>
public async Task Ok(string html)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
if (html == null)
{
html = string.Empty;
}
var result = Encoding.Default.GetBytes(html);
await Ok(result);
}
/// <summary>
/// Before request is made to server
/// Respond with the specified byte[] to client
/// and ignore the request
/// </summary>
/// <param name="body"></param>
public async Task Ok(byte[] result)
{
var response = new OkResponse();
response.HttpVersion = WebSession.Request.HttpVersion;
response.ResponseBody = result;
await Respond(response);
WebSession.Request.CancelRequest = true;
}
public async Task Redirect(string url)
{
var response = new RedirectResponse();
response.HttpVersion = WebSession.Request.HttpVersion;
response.ResponseHeaders.Add("Location", new Models.HttpHeader("Location", url));
response.ResponseBody = Encoding.ASCII.GetBytes(string.Empty);
await Respond(response);
WebSession.Request.CancelRequest = true;
}
/// a generic responder method
public async Task Respond(Response response)
{
WebSession.Request.RequestLocked = true;
response.ResponseLocked = true;
response.ResponseBodyRead = true;
WebSession.Response = response;
await httpResponseHandler(this);
}
/// <summary>
/// implement any cleanup here
/// </summary>
public void Dispose()
{
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Decompression;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Http.Responses;
using Titanium.Web.Proxy.Extensions;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Network;
using System.Net;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.EventArguments
{
/// <summary>
/// Holds info related to a single proxy session (single request/response sequence)
/// A proxy session is bounded to a single connection from client
/// A proxy session ends when client terminates connection to proxy
/// or when server terminates connection from proxy
/// </summary>
public class SessionEventArgs : EventArgs, IDisposable
{
/// <summary>
/// Size of Buffers used by this object
/// </summary>
private readonly int bufferSize;
/// <summary>
/// Holds a reference to proxy response handler method
/// </summary>
private readonly Func<SessionEventArgs, Task> httpResponseHandler;
/// <summary>
/// Holds a reference to client
/// </summary>
internal ProxyClient ProxyClient { get; set; }
//Should we send a rerequest
public bool ReRequest
{
get;
set;
}
/// <summary>
/// Does this session uses SSL
/// </summary>
public bool IsHttps => WebSession.Request.RequestUri.Scheme == Uri.UriSchemeHttps;
public IPEndPoint ClientEndPoint => (IPEndPoint)ProxyClient.TcpClient.Client.RemoteEndPoint;
/// <summary>
/// A web session corresponding to a single request/response sequence
/// within a proxy connection
/// </summary>
public HttpWebClient WebSession { get; set; }
public ExternalProxy CustomUpStreamHttpProxyUsed { get; set; }
public ExternalProxy CustomUpStreamHttpsProxyUsed { get; set; }
/// <summary>
/// Constructor to initialize the proxy
/// </summary>
internal SessionEventArgs(int bufferSize, Func<SessionEventArgs, Task> httpResponseHandler)
{
this.bufferSize = bufferSize;
this.httpResponseHandler = httpResponseHandler;
ProxyClient = new ProxyClient();
WebSession = new HttpWebClient();
}
/// <summary>
/// Read request body content as bytes[] for current session
/// </summary>
private async Task ReadRequestBody()
{
//GET request don't have a request body to read
if ((WebSession.Request.Method.ToUpper() != "POST" && WebSession.Request.Method.ToUpper() != "PUT"))
{
throw new BodyNotFoundException("Request don't have a body." +
"Please verify that this request is a Http POST/PUT and request " +
"content length is greater than zero before accessing the body.");
}
//Caching check
if (WebSession.Request.RequestBody == null)
{
//If chunked then its easy just read the whole body with the content length mentioned in the request header
using (var requestBodyStream = new MemoryStream())
{
//For chunked request we need to read data as they arrive, until we reach a chunk end symbol
if (WebSession.Request.IsChunked)
{
await this.ProxyClient.ClientStreamReader.CopyBytesToStreamChunked(bufferSize, requestBodyStream);
}
else
{
//If not chunked then its easy just read the whole body with the content length mentioned in the request header
if (WebSession.Request.ContentLength > 0)
{
//If not chunked then its easy just read the amount of bytes mentioned in content length header of response
await this.ProxyClient.ClientStreamReader.CopyBytesToStream(bufferSize, requestBodyStream,
WebSession.Request.ContentLength);
}
else if (WebSession.Request.HttpVersion.Major == 1 && WebSession.Request.HttpVersion.Minor == 0)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(bufferSize, requestBodyStream, long.MaxValue);
}
}
WebSession.Request.RequestBody = await GetDecompressedResponseBody(WebSession.Request.ContentEncoding,
requestBodyStream.ToArray());
}
//Now set the flag to true
//So that next time we can deliver body from cache
WebSession.Request.RequestBodyRead = true;
}
}
/// <summary>
/// Read response body as byte[] for current response
/// </summary>
private async Task ReadResponseBody()
{
//If not already read (not cached yet)
if (WebSession.Response.ResponseBody == null)
{
using (var responseBodyStream = new MemoryStream())
{
//If chuncked the read chunk by chunk until we hit chunk end symbol
if (WebSession.Response.IsChunked)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStreamChunked(bufferSize, responseBodyStream);
}
else
{
if (WebSession.Response.ContentLength > 0)
{
//If not chunked then its easy just read the amount of bytes mentioned in content length header of response
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(bufferSize, responseBodyStream,
WebSession.Response.ContentLength);
}
else if ((WebSession.Response.HttpVersion.Major == 1 && WebSession.Response.HttpVersion.Minor == 0) || WebSession.Response.ContentLength == -1)
{
await WebSession.ServerConnection.StreamReader.CopyBytesToStream(bufferSize, responseBodyStream, long.MaxValue);
}
}
WebSession.Response.ResponseBody = await GetDecompressedResponseBody(WebSession.Response.ContentEncoding,
responseBodyStream.ToArray());
}
//set this to true for caching
WebSession.Response.ResponseBodyRead = true;
}
}
/// <summary>
/// Gets the request body as bytes
/// </summary>
/// <returns></returns>
public async Task<byte[]> GetRequestBody()
{
if (!WebSession.Request.RequestBodyRead)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
await ReadRequestBody();
}
return WebSession.Request.RequestBody;
}
/// <summary>
/// Gets the request body as string
/// </summary>
/// <returns></returns>
public async Task<string> GetRequestBodyAsString()
{
if (!WebSession.Request.RequestBodyRead)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
await ReadRequestBody();
}
//Use the encoding specified in request to decode the byte[] data to string
return WebSession.Request.RequestBodyString ?? (WebSession.Request.RequestBodyString = WebSession.Request.Encoding.GetString(WebSession.Request.RequestBody));
}
/// <summary>
/// Sets the request body
/// </summary>
/// <param name="body"></param>
public async Task SetRequestBody(byte[] body)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
//syphon out the request body from client before setting the new body
if (!WebSession.Request.RequestBodyRead)
{
await ReadRequestBody();
}
WebSession.Request.RequestBody = body;
if (WebSession.Request.IsChunked == false)
{
WebSession.Request.ContentLength = body.Length;
}
else
{
WebSession.Request.ContentLength = -1;
}
}
/// <summary>
/// Sets the body with the specified string
/// </summary>
/// <param name="body"></param>
public async Task SetRequestBodyString(string body)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
//syphon out the request body from client before setting the new body
if (!WebSession.Request.RequestBodyRead)
{
await ReadRequestBody();
}
await SetRequestBody(WebSession.Request.Encoding.GetBytes(body));
}
/// <summary>
/// Gets the response body as byte array
/// </summary>
/// <returns></returns>
public async Task<byte[]> GetResponseBody()
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
await ReadResponseBody();
return WebSession.Response.ResponseBody;
}
/// <summary>
/// Gets the response body as string
/// </summary>
/// <returns></returns>
public async Task<string> GetResponseBodyAsString()
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
await GetResponseBody();
return WebSession.Response.ResponseBodyString ??
(WebSession.Response.ResponseBodyString = WebSession.Response.Encoding.GetString(WebSession.Response.ResponseBody));
}
/// <summary>
/// Set the response body bytes
/// </summary>
/// <param name="body"></param>
public async Task SetResponseBody(byte[] body)
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
//syphon out the response body from server before setting the new body
if (WebSession.Response.ResponseBody == null)
{
await GetResponseBody();
}
WebSession.Response.ResponseBody = body;
//If there is a content length header update it
if (WebSession.Response.IsChunked == false)
{
WebSession.Response.ContentLength = body.Length;
}
else
{
WebSession.Response.ContentLength = -1;
}
}
/// <summary>
/// Replace the response body with the specified string
/// </summary>
/// <param name="body"></param>
public async Task SetResponseBodyString(string body)
{
if (!WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
//syphon out the response body from server before setting the new body
if (WebSession.Response.ResponseBody == null)
{
await GetResponseBody();
}
var bodyBytes = WebSession.Response.Encoding.GetBytes(body);
await SetResponseBody(bodyBytes);
}
private async Task<byte[]> GetDecompressedResponseBody(string encodingType, byte[] responseBodyStream)
{
var decompressionFactory = new DecompressionFactory();
var decompressor = decompressionFactory.Create(encodingType);
return await decompressor.Decompress(responseBodyStream, bufferSize);
}
/// <summary>
/// Before request is made to server
/// Respond with the specified HTML string to client
/// and ignore the request
/// </summary>
/// <param name="html"></param>
public async Task Ok(string html)
{
await Ok(html, null);
}
/// <summary>
/// Before request is made to server
/// Respond with the specified HTML string to client
/// and ignore the request
/// </summary>
/// <param name="html"></param>
/// <param name="headers"></param>
public async Task Ok(string html, Dictionary<string, HttpHeader> headers)
{
if (WebSession.Request.RequestLocked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
if (html == null)
{
html = string.Empty;
}
var result = Encoding.Default.GetBytes(html);
await Ok(result, headers);
}
/// <summary>
/// Before request is made to server
/// Respond with the specified byte[] to client
/// and ignore the request
/// </summary>
/// <param name="result"></param>
public async Task Ok(byte[] result)
{
await Ok(result, null);
}
/// <summary>
/// Before request is made to server
/// Respond with the specified byte[] to client
/// and ignore the request
/// </summary>
/// <param name="result"></param>
/// <param name="headers"></param>
public async Task Ok(byte[] result, Dictionary<string, HttpHeader> headers)
{
var response = new OkResponse();
if (headers != null && headers.Count > 0)
{
response.ResponseHeaders = headers;
}
response.HttpVersion = WebSession.Request.HttpVersion;
response.ResponseBody = result;
await Respond(response);
WebSession.Request.CancelRequest = true;
}
public async Task Redirect(string url)
{
var response = new RedirectResponse();
response.HttpVersion = WebSession.Request.HttpVersion;
response.ResponseHeaders.Add("Location", new Models.HttpHeader("Location", url));
response.ResponseBody = Encoding.ASCII.GetBytes(string.Empty);
await Respond(response);
WebSession.Request.CancelRequest = true;
}
/// a generic responder method
public async Task Respond(Response response)
{
WebSession.Request.RequestLocked = true;
response.ResponseLocked = true;
response.ResponseBodyRead = true;
WebSession.Response = response;
await httpResponseHandler(this);
}
/// <summary>
/// implement any cleanup here
/// </summary>
public void Dispose()
{
}
}
}
\ No newline at end of file
......@@ -5,7 +5,7 @@ namespace Titanium.Web.Proxy.Exceptions
/// <summary>
/// An expception thrown when body is unexpectedly empty
/// </summary>
public class BodyNotFoundException : Exception
public class BodyNotFoundException : ProxyException
{
public BodyNotFoundException(string message)
: base(message)
......
using System;
using System.Collections.Generic;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Proxy authorization exception
/// </summary>
public class ProxyAuthorizationException : ProxyException
{
/// <summary>
/// Instantiate new instance
/// </summary>
/// <param name="message">Exception message</param>
/// <param name="innerException">Inner exception associated to upstream proxy authorization</param>
/// <param name="headers">Http's headers associated</param>
public ProxyAuthorizationException(string message, Exception innerException, IEnumerable<HttpHeader> headers) : base(message, innerException)
{
Headers = headers;
}
/// <summary>
/// Headers associated with the authorization exception
/// </summary>
public IEnumerable<HttpHeader> Headers { get; }
}
}
\ No newline at end of file
using System;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Base class exception associated with this proxy implementation
/// </summary>
public abstract class ProxyException : Exception
{
/// <summary>
/// Instantiate a new instance of this exception - must be invoked by derived classes' constructors
/// </summary>
/// <param name="message">Exception message</param>
protected ProxyException(string message) : base(message)
{
}
/// <summary>
/// Instantiate this exception - must be invoked by derived classes' constructors
/// </summary>
/// <param name="message">Excception message</param>
/// <param name="innerException">Inner exception associated</param>
protected ProxyException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
\ No newline at end of file
using System;
using Titanium.Web.Proxy.EventArguments;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Proxy HTTP exception
/// </summary>
public class ProxyHttpException : ProxyException
{
/// <summary>
/// Instantiate new instance
/// </summary>
/// <param name="message">Message for this exception</param>
/// <param name="innerException">Associated inner exception</param>
/// <param name="sessionEventArgs">Instance of <see cref="EventArguments.SessionEventArgs"/> associated to the exception</param>
public ProxyHttpException(string message, Exception innerException, SessionEventArgs sessionEventArgs) : base(message, innerException)
{
SessionEventArgs = sessionEventArgs;
}
/// <summary>
/// Gets session info associated to the exception
/// </summary>
/// <remarks>
/// This object should not be edited
/// </remarks>
public SessionEventArgs SessionEventArgs { get; }
}
}
\ No newline at end of file
......@@ -29,27 +29,20 @@ namespace Titanium.Web.Proxy.Extensions
await input.CopyToAsync(output);
}
/// <summary>
/// copies the specified bytes to the stream from the input stream
/// </summary>
/// <param name="streamReader"></param>
/// <param name="stream"></param>
/// <param name="totalBytesToRead"></param>
/// <returns></returns>
internal static async Task CopyBytesToStream(this CustomBinaryReader streamReader, int bufferSize, Stream stream, long totalBytesToRead)
/// <summary>
/// copies the specified bytes to the stream from the input stream
/// </summary>
/// <param name="streamReader"></param>
/// <param name="bufferSize"></param>
/// <param name="stream"></param>
/// <param name="totalBytesToRead"></param>
/// <returns></returns>
internal static async Task CopyBytesToStream(this CustomBinaryReader streamReader, int bufferSize, Stream stream, long totalBytesToRead)
{
var totalbytesRead = 0;
long bytesToRead;
if (totalBytesToRead < bufferSize)
{
bytesToRead = totalBytesToRead;
}
else
{
bytesToRead = bufferSize;
}
long bytesToRead = totalBytesToRead < bufferSize ? totalBytesToRead : bufferSize;
while (totalbytesRead < totalBytesToRead)
{
......@@ -72,13 +65,14 @@ namespace Titanium.Web.Proxy.Extensions
}
}
/// <summary>
/// Copies the stream chunked
/// </summary>
/// <param name="clientStreamReader"></param>
/// <param name="stream"></param>
/// <returns></returns>
internal static async Task CopyBytesToStreamChunked(this CustomBinaryReader clientStreamReader, int bufferSize, Stream stream)
/// <summary>
/// Copies the stream chunked
/// </summary>
/// <param name="clientStreamReader"></param>
/// <param name="bufferSize"></param>
/// <param name="stream"></param>
/// <returns></returns>
internal static async Task CopyBytesToStreamChunked(this CustomBinaryReader clientStreamReader, int bufferSize, Stream stream)
{
while (true)
{
......@@ -117,30 +111,32 @@ namespace Titanium.Web.Proxy.Extensions
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, int bufferSize, Stream outStream, bool isChunked, long ContentLength)
/// <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="bufferSize"></param>
/// <param name="outStream"></param>
/// <param name="isChunked"></param>
/// <param name="contentLength"></param>
/// <returns></returns>
internal static async Task WriteResponseBody(this CustomBinaryReader inStreamReader, int bufferSize, Stream outStream, bool isChunked, long contentLength)
{
if (!isChunked)
{
//http 1.0
if (ContentLength == -1)
if (contentLength == -1)
{
ContentLength = long.MaxValue;
contentLength = long.MaxValue;
}
int bytesToRead = bufferSize;
if (ContentLength < bufferSize)
if (contentLength < bufferSize)
{
bytesToRead = (int)ContentLength;
bytesToRead = (int)contentLength;
}
var buffer = new byte[bufferSize];
......@@ -153,11 +149,11 @@ namespace Titanium.Web.Proxy.Extensions
await outStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
if (totalBytesRead == ContentLength)
if (totalBytesRead == contentLength)
break;
bytesRead = 0;
var remainingBytes = (ContentLength - totalBytesRead);
var remainingBytes = (contentLength - totalBytesRead);
bytesToRead = remainingBytes > (long)bufferSize ? bufferSize : (int)remainingBytes;
}
}
......@@ -167,13 +163,14 @@ namespace Titanium.Web.Proxy.Extensions
}
}
/// <summary>
/// Copies the streams chunked
/// </summary>
/// <param name="inStreamReader"></param>
/// <param name="outStream"></param>
/// <returns></returns>
internal static async Task WriteResponseBodyChunked(this CustomBinaryReader inStreamReader, int bufferSize, Stream outStream)
/// <summary>
/// Copies the streams chunked
/// </summary>
/// <param name="inStreamReader"></param>
/// <param name="bufferSize"></param>
/// <param name="outStream"></param>
/// <returns></returns>
internal static async Task WriteResponseBodyChunked(this CustomBinaryReader inStreamReader, int bufferSize, Stream outStream)
{
while (true)
{
......
......@@ -37,37 +37,30 @@ namespace Titanium.Web.Proxy.Helpers
{
using (var readBuffer = new MemoryStream())
{
try
{
var lastChar = default(char);
var buffer = new byte[1];
while ((await this.stream.ReadAsync(buffer, 0, 1)) > 0)
{
//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];
}
return encoding.GetString(readBuffer.ToArray());
}
catch (IOException)
{
throw;
}
var lastChar = default(char);
var buffer = new byte[1];
while ((await this.stream.ReadAsync(buffer, 0, 1)) > 0)
{
//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];
}
return encoding.GetString(readBuffer.ToArray());
}
}
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Helpers
{
internal class NetworkHelper
{
private static int FindProcessIdFromLocalPort(int port, IpVersion ipVersion)
{
var tcpRow = TcpHelper.GetExtendedTcpTable(ipVersion).FirstOrDefault(
row => row.LocalEndPoint.Port == port);
return tcpRow?.ProcessId ?? 0;
}
internal static int GetProcessIdFromPort(int port, bool ipV6Enabled)
{
var processId = FindProcessIdFromLocalPort(port, IpVersion.Ipv4);
if (processId > 0 && !ipV6Enabled)
{
return processId;
}
return FindProcessIdFromLocalPort(port, IpVersion.Ipv6);
}
/// <summary>
/// Adapated from below link
/// http://stackoverflow.com/questions/11834091/how-to-check-if-localhost
/// </summary>
/// <param name="address></param>
/// <returns></returns>
internal static bool IsLocalIpAddress(IPAddress address)
{
try
{
// get local IP addresses
IPAddress[] localIPs = Dns.GetHostAddresses(Dns.GetHostName());
// test if any host IP equals to any local IP or to localhost
// is localhost
if (IPAddress.IsLoopback(address)) return true;
// is local address
foreach (IPAddress localIP in localIPs)
{
if (address.Equals(localIP)) return true;
}
}
catch { }
return false;
}
}
}
......@@ -4,14 +4,20 @@ using Microsoft.Win32;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
/// <summary>
/// Helper classes for setting system proxy settings
/// </summary>
namespace Titanium.Web.Proxy.Helpers
{
internal class NativeMethods
internal enum ProxyProtocolType
{
Http,
Https,
}
internal partial class NativeMethods
{
[DllImport("wininet.dll")]
internal static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer,
......@@ -26,16 +32,10 @@ namespace Titanium.Web.Proxy.Helpers
public override string ToString()
{
if (!IsHttps)
{
return "http=" + HostName + ":" + Port;
}
else
{
return "https=" + HostName + ":" + Port;
}
return $"{(IsHttps ? "https" : "http")}={HostName}:{Port}";
}
}
/// <summary>
/// Manage system proxy settings
/// </summary>
......@@ -44,62 +44,17 @@ namespace Titanium.Web.Proxy.Helpers
internal const int InternetOptionSettingsChanged = 39;
internal const int InternetOptionRefresh = 37;
internal void SetHttpProxy(string hostname, int port)
internal void SetHttpProxy(string hostname, int port)
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
if (reg != null)
{
prepareRegistry(reg);
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => !x.IsHttps);
existingSystemProxyValues.Add(new HttpSystemProxyValue()
{
HostName = hostname,
IsHttps = false,
Port = port
});
reg.SetValue("ProxyEnable", 1);
reg.SetValue("ProxyServer", String.Join(";", existingSystemProxyValues.Select(x => x.ToString()).ToArray()));
}
Refresh();
SetProxy(hostname, port, ProxyProtocolType.Http);
}
/// <summary>
/// Remove the http proxy setting from current machine
/// </summary>
internal void RemoveHttpProxy()
internal void RemoveHttpProxy()
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
if (reg != null)
{
if (reg.GetValue("ProxyServer") != null)
{
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => !x.IsHttps);
if (!(existingSystemProxyValues.Count() == 0))
{
reg.SetValue("ProxyEnable", 1);
reg.SetValue("ProxyServer", String.Join(";", existingSystemProxyValues.Select(x => x.ToString()).ToArray()));
}
else
{
reg.SetValue("ProxyEnable", 0);
reg.SetValue("ProxyServer", string.Empty);
}
}
}
Refresh();
RemoveProxy(ProxyProtocolType.Http);
}
/// <summary>
......@@ -107,60 +62,65 @@ namespace Titanium.Web.Proxy.Helpers
/// </summary>
/// <param name="hostname"></param>
/// <param name="port"></param>
internal void SetHttpsProxy(string hostname, int port)
internal void SetHttpsProxy(string hostname, int port)
{
SetProxy(hostname, port, ProxyProtocolType.Https);
}
/// <summary>
/// Removes the https proxy setting to nothing
/// </summary>
internal void RemoveHttpsProxy()
{
RemoveProxy(ProxyProtocolType.Https);
}
private void SetProxy(string hostname, int port, ProxyProtocolType protocolType)
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
if (reg != null)
{
prepareRegistry(reg);
PrepareRegistry(reg);
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => x.IsHttps);
existingSystemProxyValues.RemoveAll(x => protocolType == ProxyProtocolType.Https ? x.IsHttps : !x.IsHttps);
existingSystemProxyValues.Add(new HttpSystemProxyValue()
{
HostName = hostname,
IsHttps = true,
IsHttps = protocolType == ProxyProtocolType.Https,
Port = port
});
reg.SetValue("ProxyEnable", 1);
reg.SetValue("ProxyServer", String.Join(";", existingSystemProxyValues.Select(x => x.ToString()).ToArray()));
reg.SetValue("ProxyServer", string.Join(";", existingSystemProxyValues.Select(x => x.ToString()).ToArray()));
}
Refresh();
}
/// <summary>
/// Removes the https proxy setting to nothing
/// </summary>
internal void RemoveHttpsProxy()
private void RemoveProxy(ProxyProtocolType protocolType)
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
if (reg != null)
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
if (reg?.GetValue("ProxyServer") != null)
{
if (reg.GetValue("ProxyServer") != null)
{
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => x.IsHttps);
if (!(existingSystemProxyValues.Count() == 0))
{
reg.SetValue("ProxyEnable", 1);
reg.SetValue("ProxyServer", String.Join(";", existingSystemProxyValues.Select(x => x.ToString()).ToArray()));
}
else
{
reg.SetValue("ProxyEnable", 0);
reg.SetValue("ProxyServer", string.Empty);
}
var exisitingContent = reg.GetValue("ProxyServer") as string;
var existingSystemProxyValues = GetSystemProxyValues(exisitingContent);
existingSystemProxyValues.RemoveAll(x => protocolType == ProxyProtocolType.Https ? x.IsHttps : !x.IsHttps);
if (existingSystemProxyValues.Count != 0)
{
reg.SetValue("ProxyEnable", 1);
reg.SetValue("ProxyServer", string.Join(";", existingSystemProxyValues.Select(x => x.ToString()).ToArray()));
}
else
{
reg.SetValue("ProxyEnable", 0);
reg.SetValue("ProxyServer", string.Empty);
}
}
......@@ -170,7 +130,7 @@ namespace Titanium.Web.Proxy.Helpers
/// <summary>
/// Removes all types of proxy settings (both http & https)
/// </summary>
internal void DisableAllProxy()
internal void DisableAllProxy()
{
var reg = Registry.CurrentUser.OpenSubKey(
"Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", true);
......@@ -189,7 +149,7 @@ namespace Titanium.Web.Proxy.Helpers
/// </summary>
/// <param name="prevServerValue"></param>
/// <returns></returns>
private List<HttpSystemProxyValue> GetSystemProxyValues(string prevServerValue)
private List<HttpSystemProxyValue> GetSystemProxyValues(string prevServerValue)
{
var result = new List<HttpSystemProxyValue>();
......@@ -200,16 +160,11 @@ namespace Titanium.Web.Proxy.Helpers
if (proxyValues.Length > 0)
{
foreach (var value in proxyValues)
{
var parsedValue = parseProxyValue(value);
if (parsedValue != null)
result.Add(parsedValue);
}
result.AddRange(proxyValues.Select(ParseProxyValue).Where(parsedValue => parsedValue != null));
}
else
{
var parsedValue = parseProxyValue(prevServerValue);
var parsedValue = ParseProxyValue(prevServerValue);
if (parsedValue != null)
result.Add(parsedValue);
}
......@@ -222,29 +177,21 @@ namespace Titanium.Web.Proxy.Helpers
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private HttpSystemProxyValue parseProxyValue(string value)
private HttpSystemProxyValue ParseProxyValue(string value)
{
var tmp = Regex.Replace(value, @"\s+", " ").Trim().ToLower();
if (tmp.StartsWith("http="))
{
var endPoint = tmp.Substring(5);
return new HttpSystemProxyValue()
{
HostName = endPoint.Split(':')[0],
Port = int.Parse(endPoint.Split(':')[1]),
IsHttps = false
};
}
else if (tmp.StartsWith("https="))
if (tmp.StartsWith("http=") || tmp.StartsWith("https="))
{
var endPoint = tmp.Substring(5);
return new HttpSystemProxyValue()
{
HostName = endPoint.Split(':')[0],
Port = int.Parse(endPoint.Split(':')[1]),
IsHttps = true
};
IsHttps = tmp.StartsWith("https=")
};
}
return null;
}
......@@ -252,7 +199,7 @@ namespace Titanium.Web.Proxy.Helpers
/// Prepares the proxy server registry (create empty values if they don't exist)
/// </summary>
/// <param name="reg"></param>
private void prepareRegistry(RegistryKey reg)
private static void PrepareRegistry(RegistryKey reg)
{
if (reg.GetValue("ProxyEnable") == null)
{
......@@ -269,7 +216,7 @@ namespace Titanium.Web.Proxy.Helpers
/// <summary>
/// Refresh the settings so that the system know about a change in proxy setting
/// </summary>
private void Refresh()
private void Refresh()
{
NativeMethods.InternetSetOption(IntPtr.Zero, InternetOptionSettingsChanged, IntPtr.Zero, 0);
NativeMethods.InternetSetOption(IntPtr.Zero, InternetOptionRefresh, IntPtr.Zero, 0);
......
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network;
namespace Titanium.Web.Proxy.Helpers
{
internal class TcpHelper
{
/// <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="bufferSize"></param>
/// <param name="connectionTimeOutSeconds"></param>
/// <param name="remoteHostName"></param>
/// <param name="httpCmd"></param>
/// <param name="httpVersion"></param>
/// <param name="requestHeaders"></param>
/// <param name="isHttps"></param>
/// <param name="remotePort"></param>
/// <param name="supportedProtocols"></param>
/// <param name="remoteCertificateValidationCallback"></param>
/// <param name="localCertificateSelectionCallback"></param>
/// <param name="clientStream"></param>
/// <param name="tcpConnectionFactory"></param>
/// <returns></returns>
internal static async Task SendRaw(int bufferSize, int connectionTimeOutSeconds,
string remoteHostName, int remotePort, string httpCmd, Version httpVersion, Dictionary<string, HttpHeader> requestHeaders,
bool isHttps, SslProtocols supportedProtocols,
RemoteCertificateValidationCallback remoteCertificateValidationCallback, LocalCertificateSelectionCallback localCertificateSelectionCallback,
Stream clientStream, TcpConnectionFactory tcpConnectionFactory)
{
//prepare the prefix content
StringBuilder sb = null;
if (httpCmd != null || requestHeaders != null)
{
sb = new StringBuilder();
if (httpCmd != null)
{
sb.Append(httpCmd);
sb.Append(Environment.NewLine);
}
if (requestHeaders != null)
{
foreach (var header in requestHeaders.Select(t => t.Value.ToString()))
{
sb.Append(header);
sb.Append(Environment.NewLine);
}
}
sb.Append(Environment.NewLine);
}
var tcpConnection = await tcpConnectionFactory.CreateClient(bufferSize, connectionTimeOutSeconds,
remoteHostName, remotePort,
httpVersion, isHttps,
supportedProtocols, remoteCertificateValidationCallback, localCertificateSelectionCallback,
null, null, clientStream);
try
{
TcpClient tunnelClient = tcpConnection.TcpClient;
Stream tunnelStream = tcpConnection.Stream;
Task sendRelay;
//Now async relay all server=>client & client=>server data
if (sb != null)
{
sendRelay = clientStream.CopyToAsync(sb.ToString(), tunnelStream);
}
else
{
sendRelay = clientStream.CopyToAsync(string.Empty, tunnelStream);
}
var receiveRelay = tunnelStream.CopyToAsync(string.Empty, clientStream);
await Task.WhenAll(sendRelay, receiveRelay);
}
catch
{
throw;
}
finally
{
tcpConnection.Dispose();
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network;
using Titanium.Web.Proxy.Tcp;
namespace Titanium.Web.Proxy.Helpers
{
internal enum IpVersion
{
Ipv4 = 1,
Ipv6 = 2,
}
internal partial class NativeMethods
{
internal const int AfInet = 2;
internal const int AfInet6 = 23;
internal enum TcpTableType
{
BasicListener,
BasicConnections,
BasicAll,
OwnerPidListener,
OwnerPidConnections,
OwnerPidAll,
OwnerModuleListener,
OwnerModuleConnections,
OwnerModuleAll,
}
/// <summary>
/// <see cref="http://msdn2.microsoft.com/en-us/library/aa366921.aspx"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct TcpTable
{
public uint length;
public TcpRow row;
}
/// <summary>
/// <see cref="http://msdn2.microsoft.com/en-us/library/aa366913.aspx"/>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct TcpRow
{
public TcpState state;
public uint localAddr;
public byte localPort1;
public byte localPort2;
public byte localPort3;
public byte localPort4;
public uint remoteAddr;
public byte remotePort1;
public byte remotePort2;
public byte remotePort3;
public byte remotePort4;
public int owningPid;
}
/// <summary>
/// <see cref="http://msdn2.microsoft.com/en-us/library/aa365928.aspx"/>
/// </summary>
[DllImport("iphlpapi.dll", SetLastError = true)]
internal static extern uint GetExtendedTcpTable(IntPtr tcpTable, ref int size, bool sort, int ipVersion, int tableClass, int reserved);
}
internal class TcpHelper
{
/// <summary>
/// Gets the extended TCP table.
/// </summary>
/// <returns>Collection of <see cref="TcpRow"/>.</returns>
internal static TcpTable GetExtendedTcpTable(IpVersion ipVersion)
{
List<TcpRow> tcpRows = new List<TcpRow>();
IntPtr tcpTable = IntPtr.Zero;
int tcpTableLength = 0;
var ipVersionValue = ipVersion == IpVersion.Ipv4 ? NativeMethods.AfInet : NativeMethods.AfInet6;
if (NativeMethods.GetExtendedTcpTable(tcpTable, ref tcpTableLength, false, ipVersionValue, (int)NativeMethods.TcpTableType.OwnerPidAll, 0) != 0)
{
try
{
tcpTable = Marshal.AllocHGlobal(tcpTableLength);
if (NativeMethods.GetExtendedTcpTable(tcpTable, ref tcpTableLength, true, ipVersionValue, (int)NativeMethods.TcpTableType.OwnerPidAll, 0) == 0)
{
NativeMethods.TcpTable table = (NativeMethods.TcpTable)Marshal.PtrToStructure(tcpTable, typeof(NativeMethods.TcpTable));
IntPtr rowPtr = (IntPtr)((long)tcpTable + Marshal.SizeOf(table.length));
for (int i = 0; i < table.length; ++i)
{
tcpRows.Add(new TcpRow((NativeMethods.TcpRow)Marshal.PtrToStructure(rowPtr, typeof(NativeMethods.TcpRow))));
rowPtr = (IntPtr)((long)rowPtr + Marshal.SizeOf(typeof(NativeMethods.TcpRow)));
}
}
}
finally
{
if (tcpTable != IntPtr.Zero)
{
Marshal.FreeHGlobal(tcpTable);
}
}
}
return new TcpTable(tcpRows);
}
/// <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="bufferSize"></param>
/// <param name="connectionTimeOutSeconds"></param>
/// <param name="remoteHostName"></param>
/// <param name="httpCmd"></param>
/// <param name="httpVersion"></param>
/// <param name="requestHeaders"></param>
/// <param name="isHttps"></param>
/// <param name="remotePort"></param>
/// <param name="supportedProtocols"></param>
/// <param name="remoteCertificateValidationCallback"></param>
/// <param name="localCertificateSelectionCallback"></param>
/// <param name="clientStream"></param>
/// <param name="tcpConnectionFactory"></param>
/// <returns></returns>
internal static async Task SendRaw(int bufferSize, int connectionTimeOutSeconds,
string remoteHostName, int remotePort, string httpCmd, Version httpVersion, Dictionary<string, HttpHeader> requestHeaders,
bool isHttps, SslProtocols supportedProtocols,
RemoteCertificateValidationCallback remoteCertificateValidationCallback, LocalCertificateSelectionCallback localCertificateSelectionCallback,
Stream clientStream, TcpConnectionFactory tcpConnectionFactory)
{
//prepare the prefix content
StringBuilder sb = null;
if (httpCmd != null || requestHeaders != null)
{
sb = new StringBuilder();
if (httpCmd != null)
{
sb.Append(httpCmd);
sb.Append(Environment.NewLine);
}
if (requestHeaders != null)
{
foreach (var header in requestHeaders.Select(t => t.Value.ToString()))
{
sb.Append(header);
sb.Append(Environment.NewLine);
}
}
sb.Append(Environment.NewLine);
}
var tcpConnection = await tcpConnectionFactory.CreateClient(bufferSize, connectionTimeOutSeconds,
remoteHostName, remotePort,
httpVersion, isHttps,
supportedProtocols, remoteCertificateValidationCallback, localCertificateSelectionCallback,
null, null, clientStream);
try
{
Stream tunnelStream = tcpConnection.Stream;
//Now async relay all server=>client & client=>server data
var sendRelay = clientStream.CopyToAsync(sb?.ToString() ?? string.Empty, tunnelStream);
var receiveRelay = tunnelStream.CopyToAsync(string.Empty, clientStream);
await Task.WhenAll(sendRelay, receiveRelay);
}
finally
{
tcpConnection.Dispose();
}
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Http
{
/// <summary>
/// Used to communicate with the server over HTTP(S)
/// </summary>
public class HttpWebClient
{
/// <summary>
/// Connection to server
/// </summary>
internal TcpConnection ServerConnection { get; set; }
public Request Request { get; set; }
public Response Response { get; set; }
/// <summary>
/// Is Https?
/// </summary>
public bool IsHttps
{
get
{
return this.Request.RequestUri.Scheme == Uri.UriSchemeHttps;
}
}
/// <summary>
/// Set the tcp connection to server used by this webclient
/// </summary>
/// <param name="Connection"></param>
internal void SetConnection(TcpConnection Connection)
{
ServerConnection = Connection;
}
internal HttpWebClient()
{
this.Request = new Request();
this.Response = new Response();
}
/// <summary>
/// Prepare & send the http(s) request
/// </summary>
/// <returns></returns>
internal async Task SendRequest(bool enable100ContinueBehaviour)
{
Stream stream = ServerConnection.Stream;
StringBuilder requestLines = new StringBuilder();
//prepare the request & headers
requestLines.AppendLine(string.Join(" ", new string[3]
{
this.Request.Method,
this.Request.RequestUri.PathAndQuery,
string.Format("HTTP/{0}.{1}",this.Request.HttpVersion.Major, this.Request.HttpVersion.Minor)
}));
//write request headers
foreach (var headerItem in this.Request.RequestHeaders)
{
var header = headerItem.Value;
requestLines.AppendLine(header.Name + ':' + header.Value);
}
//write non unique request headers
foreach (var headerItem in this.Request.NonUniqueRequestHeaders)
{
var headers = headerItem.Value;
foreach (var header in headers)
{
requestLines.AppendLine(header.Name + ':' + header.Value);
}
}
requestLines.AppendLine();
string request = requestLines.ToString();
byte[] requestBytes = Encoding.ASCII.GetBytes(request);
await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
await stream.FlushAsync();
if (enable100ContinueBehaviour)
{
if (this.Request.ExpectContinue)
{
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
var responseStatusCode = httpResult[1].Trim();
var responseStatusDescription = httpResult[2].Trim();
//find if server is willing for expect continue
if (responseStatusCode.Equals("100")
&& responseStatusDescription.ToLower().Equals("continue"))
{
this.Request.Is100Continue = true;
await ServerConnection.StreamReader.ReadLineAsync();
}
else if (responseStatusCode.Equals("417")
&& responseStatusDescription.ToLower().Equals("expectation failed"))
{
this.Request.ExpectationFailed = true;
await ServerConnection.StreamReader.ReadLineAsync();
}
}
}
}
/// <summary>
/// Receive & parse the http response from server
/// </summary>
/// <returns></returns>
internal async Task ReceiveResponse()
{
//return if this is already read
if (this.Response.ResponseStatusCode != null) return;
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
if (string.IsNullOrEmpty(httpResult[0]))
{
//Empty content in first-line, try again
httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
}
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 = version;
this.Response.ResponseStatusCode = httpResult[1].Trim();
this.Response.ResponseStatusDescription = httpResult[2].Trim();
//For HTTP 1.1 comptibility server may send expect-continue even if not asked for it in request
if (this.Response.ResponseStatusCode.Equals("100")
&& this.Response.ResponseStatusDescription.ToLower().Equals("continue"))
{
//Read the next line after 100-continue
this.Response.Is100Continue = true;
this.Response.ResponseStatusCode = null;
await ServerConnection.StreamReader.ReadLineAsync();
//now receive response
await ReceiveResponse();
return;
}
else if (this.Response.ResponseStatusCode.Equals("417")
&& this.Response.ResponseStatusDescription.ToLower().Equals("expectation failed"))
{
//read next line after expectation failed response
this.Response.ExpectationFailed = true;
this.Response.ResponseStatusCode = null;
await ServerConnection.StreamReader.ReadLineAsync();
//now receive response
await ReceiveResponse();
return;
}
//Read the Response headers
//Read the response headers in to unique and non-unique header collections
string tmpLine;
while (!string.IsNullOrEmpty(tmpLine = await ServerConnection.StreamReader.ReadLineAsync()))
{
var header = tmpLine.Split(ProxyConstants.ColonSplit, 2);
var newHeader = new HttpHeader(header[0], header[1]);
//if header exist in non-unique header collection add it there
if (Response.NonUniqueResponseHeaders.ContainsKey(newHeader.Name))
{
Response.NonUniqueResponseHeaders[newHeader.Name].Add(newHeader);
}
//if header is alread in unique header collection then move both to non-unique collection
else if (Response.ResponseHeaders.ContainsKey(newHeader.Name))
{
var existing = Response.ResponseHeaders[newHeader.Name];
var nonUniqueHeaders = new List<HttpHeader>();
nonUniqueHeaders.Add(existing);
nonUniqueHeaders.Add(newHeader);
Response.NonUniqueResponseHeaders.Add(newHeader.Name, nonUniqueHeaders);
Response.ResponseHeaders.Remove(newHeader.Name);
}
//add to unique header collection
else
{
Response.ResponseHeaders.Add(newHeader.Name, newHeader);
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Http
{
/// <summary>
/// Used to communicate with the server over HTTP(S)
/// </summary>
public class HttpWebClient
{
/// <summary>
/// Connection to server
/// </summary>
internal TcpConnection ServerConnection { get; set; }
public Guid RequestId { get; private set; }
public List<HttpHeader> ConnectHeaders { get; set; }
public Request Request { get; set; }
public Response Response { get; set; }
/// <summary>
/// PID of the process that is created the current session when client is running in this machine
/// If client is remote then this will return
/// </summary>
public Lazy<int> ProcessId { get; internal set; }
/// <summary>
/// Is Https?
/// </summary>
public bool IsHttps => this.Request.RequestUri.Scheme == Uri.UriSchemeHttps;
internal HttpWebClient()
{
this.RequestId = Guid.NewGuid();
this.Request = new Request();
this.Response = new Response();
}
/// <summary>
/// Set the tcp connection to server used by this webclient
/// </summary>
/// <param name="connection">Instance of <see cref="TcpConnection"/></param>
internal void SetConnection(TcpConnection connection)
{
connection.LastAccess = DateTime.Now;
ServerConnection = connection;
}
/// <summary>
/// Prepare & send the http(s) request
/// </summary>
/// <returns></returns>
internal async Task SendRequest(bool enable100ContinueBehaviour)
{
Stream stream = ServerConnection.Stream;
StringBuilder requestLines = new StringBuilder();
//prepare the request & headers
if ((ServerConnection.UpStreamHttpProxy != null && ServerConnection.IsHttps == false) || (ServerConnection.UpStreamHttpsProxy != null && ServerConnection.IsHttps == true))
{
requestLines.AppendLine(string.Join(" ", this.Request.Method, this.Request.RequestUri.AbsoluteUri, $"HTTP/{this.Request.HttpVersion.Major}.{this.Request.HttpVersion.Minor}"));
}
else
{
requestLines.AppendLine(string.Join(" ", this.Request.Method, this.Request.RequestUri.PathAndQuery, $"HTTP/{this.Request.HttpVersion.Major}.{this.Request.HttpVersion.Minor}"));
}
//Send Authentication to Upstream proxy if needed
if (ServerConnection.UpStreamHttpProxy != null && ServerConnection.IsHttps == false && !string.IsNullOrEmpty(ServerConnection.UpStreamHttpProxy.UserName) && ServerConnection.UpStreamHttpProxy.Password != null)
{
requestLines.AppendLine("Proxy-Connection: keep-alive");
requestLines.AppendLine("Proxy-Authorization" + ": Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(ServerConnection.UpStreamHttpProxy.UserName + ":" + ServerConnection.UpStreamHttpProxy.Password)));
}
//write request headers
foreach (var headerItem in this.Request.RequestHeaders)
{
var header = headerItem.Value;
if (headerItem.Key != "Proxy-Authorization")
{
requestLines.AppendLine(header.Name + ':' + header.Value);
}
}
//write non unique request headers
foreach (var headerItem in this.Request.NonUniqueRequestHeaders)
{
var headers = headerItem.Value;
foreach (var header in headers)
{
if (headerItem.Key != "Proxy-Authorization")
{
requestLines.AppendLine(header.Name + ':' + header.Value);
}
}
}
requestLines.AppendLine();
string request = requestLines.ToString();
byte[] requestBytes = Encoding.ASCII.GetBytes(request);
await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
await stream.FlushAsync();
if (enable100ContinueBehaviour)
{
if (this.Request.ExpectContinue)
{
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
var responseStatusCode = httpResult[1].Trim();
var responseStatusDescription = httpResult[2].Trim();
//find if server is willing for expect continue
if (responseStatusCode.Equals("100")
&& responseStatusDescription.ToLower().Equals("continue"))
{
this.Request.Is100Continue = true;
await ServerConnection.StreamReader.ReadLineAsync();
}
else if (responseStatusCode.Equals("417")
&& responseStatusDescription.ToLower().Equals("expectation failed"))
{
this.Request.ExpectationFailed = true;
await ServerConnection.StreamReader.ReadLineAsync();
}
}
}
}
/// <summary>
/// Receive & parse the http response from server
/// </summary>
/// <returns></returns>
internal async Task ReceiveResponse()
{
//return if this is already read
if (this.Response.ResponseStatusCode != null) return;
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
if (string.IsNullOrEmpty(httpResult[0]))
{
//Empty content in first-line, try again
httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
}
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 = version;
this.Response.ResponseStatusCode = httpResult[1].Trim();
this.Response.ResponseStatusDescription = httpResult[2].Trim();
//For HTTP 1.1 comptibility server may send expect-continue even if not asked for it in request
if (this.Response.ResponseStatusCode.Equals("100")
&& this.Response.ResponseStatusDescription.ToLower().Equals("continue"))
{
//Read the next line after 100-continue
this.Response.Is100Continue = true;
this.Response.ResponseStatusCode = null;
await ServerConnection.StreamReader.ReadLineAsync();
//now receive response
await ReceiveResponse();
return;
}
else if (this.Response.ResponseStatusCode.Equals("417")
&& this.Response.ResponseStatusDescription.ToLower().Equals("expectation failed"))
{
//read next line after expectation failed response
this.Response.ExpectationFailed = true;
this.Response.ResponseStatusCode = null;
await ServerConnection.StreamReader.ReadLineAsync();
//now receive response
await ReceiveResponse();
return;
}
//Read the Response headers
//Read the response headers in to unique and non-unique header collections
string tmpLine;
while (!string.IsNullOrEmpty(tmpLine = await ServerConnection.StreamReader.ReadLineAsync()))
{
var header = tmpLine.Split(ProxyConstants.ColonSplit, 2);
var newHeader = new HttpHeader(header[0], header[1]);
//if header exist in non-unique header collection add it there
if (Response.NonUniqueResponseHeaders.ContainsKey(newHeader.Name))
{
Response.NonUniqueResponseHeaders[newHeader.Name].Add(newHeader);
}
//if header is alread in unique header collection then move both to non-unique collection
else if (Response.ResponseHeaders.ContainsKey(newHeader.Name))
{
var existing = Response.ResponseHeaders[newHeader.Name];
var nonUniqueHeaders = new List<HttpHeader> {existing, newHeader};
Response.NonUniqueResponseHeaders.Add(newHeader.Name, nonUniqueHeaders);
Response.ResponseHeaders.Remove(newHeader.Name);
}
//add to unique header collection
else
{
Response.ResponseHeaders.Add(newHeader.Name, newHeader);
}
}
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Extensions;
......@@ -234,13 +233,14 @@ namespace Titanium.Web.Proxy.Http
/// <summary>
/// Request Url
/// </summary>
public string Url { get { return RequestUri.OriginalString; } }
public string Url => RequestUri.OriginalString;
/// <summary>
/// <summary>
/// Encoding for this request
/// </summary>
internal Encoding Encoding { get { return this.GetEncoding(); } }
/// <summary>
internal Encoding Encoding => this.GetEncoding();
/// <summary>
/// Terminates the underlying Tcp Connection to client after current request
/// </summary>
internal bool CancelRequest { get; set; }
......
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Extensions;
......@@ -16,9 +15,9 @@ namespace Titanium.Web.Proxy.Http
public string ResponseStatusCode { get; set; }
public string ResponseStatusDescription { get; set; }
internal Encoding Encoding { get { return this.GetResponseCharacterEncoding(); } }
internal Encoding Encoding => this.GetResponseCharacterEncoding();
/// <summary>
/// <summary>
/// Content encoding for this response
/// </summary>
internal string ContentEncoding
......@@ -205,7 +204,7 @@ namespace Titanium.Web.Proxy.Http
/// <summary>
/// Response network stream
/// </summary>
internal Stream ResponseStream { get; set; }
public Stream ResponseStream { get; set; }
/// <summary>
/// response body contenst as byte array
......
......@@ -3,7 +3,7 @@
/// <summary>
/// 200 Ok response
/// </summary>
public class OkResponse : Response
public sealed class OkResponse : Response
{
public OkResponse()
{
......
using Titanium.Web.Proxy.Network;
namespace Titanium.Web.Proxy.Http.Responses
namespace Titanium.Web.Proxy.Http.Responses
{
/// <summary>
/// Redirect response
/// </summary>
public class RedirectResponse : Response
public sealed class RedirectResponse : Response
{
public RedirectResponse()
{
......
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
namespace Titanium.Web.Proxy.Models
{
/// <summary>
/// An abstract endpoint where the proxy listens
/// </summary>
public abstract class ProxyEndPoint
{
public ProxyEndPoint(IPAddress IpAddress, int Port, bool EnableSsl)
{
this.IpAddress = IpAddress;
this.Port = Port;
this.EnableSsl = EnableSsl;
}
public IPAddress IpAddress { get; internal set; }
public int Port { get; internal set; }
public bool EnableSsl { get; internal 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
{
internal bool IsSystemHttpProxy { get; set; }
internal bool IsSystemHttpsProxy { get; set; }
public List<string> ExcludedHttpsHostNameRegex { get; set; }
public ExplicitProxyEndPoint(IPAddress IpAddress, int Port, bool EnableSsl)
: base(IpAddress, Port, EnableSsl)
{
}
}
/// <summary>
/// A proxy end point client is not aware of
/// Usefull when requests are redirected to this proxy end point through port forwarding
/// </summary>
public class TransparentProxyEndPoint : ProxyEndPoint
{
//Name of the Certificate need to be sent (same as the hostname we want to proxy)
//This is valid only when UseServerNameIndication is set to false
public string GenericCertificateName { get; set; }
// public bool UseServerNameIndication { get; set; }
public TransparentProxyEndPoint(IPAddress IpAddress, int Port, bool EnableSsl)
: base(IpAddress, Port, EnableSsl)
{
this.GenericCertificateName = "localhost";
}
}
}
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
namespace Titanium.Web.Proxy.Models
{
/// <summary>
/// An abstract endpoint where the proxy listens
/// </summary>
public abstract class ProxyEndPoint
{
public ProxyEndPoint(IPAddress IpAddress, int Port, bool EnableSsl)
{
this.IpAddress = IpAddress;
this.Port = Port;
this.EnableSsl = EnableSsl;
}
public IPAddress IpAddress { get; internal set; }
public int Port { get; internal set; }
public bool EnableSsl { get; internal set; }
public bool IpV6Enabled => IpAddress == IPAddress.IPv6Any
|| IpAddress == IPAddress.IPv6Loopback
|| IpAddress == IPAddress.IPv6None;
internal TcpListener listener { get; set; }
}
/// <summary>
/// A proxy endpoint that the client is aware of
/// So client application know that it is communicating with a proxy server
/// </summary>
public class ExplicitProxyEndPoint : ProxyEndPoint
{
internal bool IsSystemHttpProxy { get; set; }
internal bool IsSystemHttpsProxy { get; set; }
public List<string> ExcludedHttpsHostNameRegex { get; set; }
public ExplicitProxyEndPoint(IPAddress IpAddress, int Port, bool EnableSsl)
: base(IpAddress, Port, EnableSsl)
{
}
}
/// <summary>
/// A proxy end point client is not aware of
/// Usefull when requests are redirected to this proxy end point through port forwarding
/// </summary>
public class TransparentProxyEndPoint : ProxyEndPoint
{
//Name of the Certificate need to be sent (same as the hostname we want to proxy)
//This is valid only when UseServerNameIndication is set to false
public string GenericCertificateName { get; set; }
// public bool UseServerNameIndication { get; set; }
public TransparentProxyEndPoint(IPAddress IpAddress, int Port, bool EnableSsl)
: base(IpAddress, Port, EnableSsl)
{
this.GenericCertificateName = "localhost";
}
}
}
namespace Titanium.Web.Proxy.Models
{
/// <summary>
/// An upstream proxy this proxy uses if any
/// </summary>
public class ExternalProxy
{
public string HostName { get; set; }
public int Port { get; set; }
}
}
using System;
using System.Net;
namespace Titanium.Web.Proxy.Models
{
/// <summary>
/// An upstream proxy this proxy uses if any
/// </summary>
public class ExternalProxy
{
private static readonly Lazy<NetworkCredential> DefaultCredentials = new Lazy<NetworkCredential>(() => CredentialCache.DefaultNetworkCredentials);
private string userName;
private string password;
public bool UseDefaultCredentials { get; set; }
public string UserName {
get { return UseDefaultCredentials ? DefaultCredentials.Value.UserName : userName; }
set
{
userName = value;
if (DefaultCredentials.Value.UserName != userName)
{
UseDefaultCredentials = false;
}
}
}
public string Password
{
get { return UseDefaultCredentials ? DefaultCredentials.Value.Password : password; }
set
{
password = value;
if (DefaultCredentials.Value.Password != password)
{
UseDefaultCredentials = false;
}
}
}
public string HostName { get; set; }
public int Port { get; set; }
}
}
using System;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
namespace Titanium.Web.Proxy.Network
{
public class CertificateMaker
{
private Type typeX500DN;
private Type typeX509PrivateKey;
private Type typeOID;
private Type typeOIDS;
private Type typeKUExt;
private Type typeEKUExt;
private Type typeRequestCert;
private Type typeX509Extensions;
private Type typeBasicConstraints;
private Type typeSignerCertificate;
private Type typeX509Enrollment;
private Type typeAlternativeName;
private Type typeAlternativeNames;
private Type typeAlternativeNamesExt;
private string sProviderName = "Microsoft Enhanced Cryptographic Provider v1.0";
private object _SharedPrivateKey;
public CertificateMaker()
{
this.typeX500DN = Type.GetTypeFromProgID("X509Enrollment.CX500DistinguishedName", true);
this.typeX509PrivateKey = Type.GetTypeFromProgID("X509Enrollment.CX509PrivateKey", true);
this.typeOID = Type.GetTypeFromProgID("X509Enrollment.CObjectId", true);
this.typeOIDS = Type.GetTypeFromProgID("X509Enrollment.CObjectIds.1", true);
this.typeEKUExt = Type.GetTypeFromProgID("X509Enrollment.CX509ExtensionEnhancedKeyUsage");
this.typeKUExt = Type.GetTypeFromProgID("X509Enrollment.CX509ExtensionKeyUsage");
this.typeRequestCert = Type.GetTypeFromProgID("X509Enrollment.CX509CertificateRequestCertificate");
this.typeX509Extensions = Type.GetTypeFromProgID("X509Enrollment.CX509Extensions");
this.typeBasicConstraints = Type.GetTypeFromProgID("X509Enrollment.CX509ExtensionBasicConstraints");
this.typeSignerCertificate = Type.GetTypeFromProgID("X509Enrollment.CSignerCertificate");
this.typeX509Enrollment = Type.GetTypeFromProgID("X509Enrollment.CX509Enrollment");
this.typeAlternativeName = Type.GetTypeFromProgID("X509Enrollment.CAlternativeName");
this.typeAlternativeNames = Type.GetTypeFromProgID("X509Enrollment.CAlternativeNames");
this.typeAlternativeNamesExt = Type.GetTypeFromProgID("X509Enrollment.CX509ExtensionAlternativeNames");
}
public X509Certificate2 MakeCertificate(string sSubjectCN, bool isRoot,X509Certificate2 signingCert=null)
{
return this.MakeCertificateInternal(sSubjectCN, isRoot, true, signingCert);
}
private X509Certificate2 MakeCertificate(bool IsRoot, string SubjectCN, string FullSubject, int PrivateKeyLength, string HashAlg, DateTime ValidFrom, DateTime ValidTo, X509Certificate2 SigningCertificate)
{
X509Certificate2 cert;
if (IsRoot != (null == SigningCertificate))
{
throw new ArgumentException("You must specify a Signing Certificate if and only if you are not creating a root.", "oSigningCertificate");
}
object x500DN = Activator.CreateInstance(this.typeX500DN);
object[] subject = new object[] { FullSubject, 0 };
this.typeX500DN.InvokeMember("Encode", BindingFlags.InvokeMethod, null, x500DN, subject);
object x500DN2 = Activator.CreateInstance(this.typeX500DN);
if (!IsRoot)
{
subject[0] = SigningCertificate.Subject;
}
this.typeX500DN.InvokeMember("Encode", BindingFlags.InvokeMethod, null, x500DN2, subject);
object sharedPrivateKey = null;
if (!IsRoot)
{
sharedPrivateKey = this._SharedPrivateKey;
}
if (sharedPrivateKey == null)
{
sharedPrivateKey = Activator.CreateInstance(this.typeX509PrivateKey);
subject = new object[] { this.sProviderName };
this.typeX509PrivateKey.InvokeMember("ProviderName", BindingFlags.PutDispProperty, null, sharedPrivateKey, subject);
subject[0] = 2;
this.typeX509PrivateKey.InvokeMember("ExportPolicy", BindingFlags.PutDispProperty, null, sharedPrivateKey, subject);
subject = new object[] { (IsRoot ? 2 : 1) };
this.typeX509PrivateKey.InvokeMember("KeySpec", BindingFlags.PutDispProperty, null, sharedPrivateKey, subject);
if (!IsRoot)
{
subject = new object[] { 176 };
this.typeX509PrivateKey.InvokeMember("KeyUsage", BindingFlags.PutDispProperty, null, sharedPrivateKey, subject);
}
subject[0] = PrivateKeyLength;
this.typeX509PrivateKey.InvokeMember("Length", BindingFlags.PutDispProperty, null, sharedPrivateKey, subject);
this.typeX509PrivateKey.InvokeMember("Create", BindingFlags.InvokeMethod, null, sharedPrivateKey, null);
if (!IsRoot)
{
this._SharedPrivateKey = sharedPrivateKey;
}
}
subject = new object[1];
object obj3 = Activator.CreateInstance(this.typeOID);
subject[0] = "1.3.6.1.5.5.7.3.1";
this.typeOID.InvokeMember("InitializeFromValue", BindingFlags.InvokeMethod, null, obj3, subject);
object obj4 = Activator.CreateInstance(this.typeOIDS);
subject[0] = obj3;
this.typeOIDS.InvokeMember("Add", BindingFlags.InvokeMethod, null, obj4, subject);
object obj5 = Activator.CreateInstance(this.typeEKUExt);
subject[0] = obj4;
this.typeEKUExt.InvokeMember("InitializeEncode", BindingFlags.InvokeMethod, null, obj5, subject);
object obj6 = Activator.CreateInstance(this.typeRequestCert);
subject = new object[] { 1, sharedPrivateKey, string.Empty };
this.typeRequestCert.InvokeMember("InitializeFromPrivateKey", BindingFlags.InvokeMethod, null, obj6, subject);
subject = new object[] { x500DN };
this.typeRequestCert.InvokeMember("Subject", BindingFlags.PutDispProperty, null, obj6, subject);
subject[0] = x500DN;
this.typeRequestCert.InvokeMember("Issuer", BindingFlags.PutDispProperty, null, obj6, subject);
subject[0] = ValidFrom;
this.typeRequestCert.InvokeMember("NotBefore", BindingFlags.PutDispProperty, null, obj6, subject);
subject[0] = ValidTo;
this.typeRequestCert.InvokeMember("NotAfter", BindingFlags.PutDispProperty, null, obj6, subject);
object obj7 = Activator.CreateInstance(this.typeKUExt);
subject[0] = 176;
this.typeKUExt.InvokeMember("InitializeEncode", BindingFlags.InvokeMethod, null, obj7, subject);
object obj8 = this.typeRequestCert.InvokeMember("X509Extensions", BindingFlags.GetProperty, null, obj6, null);
subject = new object[1];
if (!IsRoot)
{
subject[0] = obj7;
this.typeX509Extensions.InvokeMember("Add", BindingFlags.InvokeMethod, null, obj8, subject);
}
subject[0] = obj5;
this.typeX509Extensions.InvokeMember("Add", BindingFlags.InvokeMethod, null, obj8, subject);
if (!IsRoot)
{
object obj12 = Activator.CreateInstance(this.typeSignerCertificate);
subject = new object[] { 0, 0, 12, SigningCertificate.Thumbprint };
this.typeSignerCertificate.InvokeMember("Initialize", BindingFlags.InvokeMethod, null, obj12, subject);
subject = new object[] { obj12 };
this.typeRequestCert.InvokeMember("SignerCertificate", BindingFlags.PutDispProperty, null, obj6, subject);
}
else
{
object obj13 = Activator.CreateInstance(this.typeBasicConstraints);
subject = new object[] { "true", "0" };
this.typeBasicConstraints.InvokeMember("InitializeEncode", BindingFlags.InvokeMethod, null, obj13, subject);
subject = new object[] { obj13 };
this.typeX509Extensions.InvokeMember("Add", BindingFlags.InvokeMethod, null, obj8, subject);
}
object obj14 = Activator.CreateInstance(this.typeOID);
subject = new object[] { 1, 0, 0, HashAlg };
this.typeOID.InvokeMember("InitializeFromAlgorithmName", BindingFlags.InvokeMethod, null, obj14, subject);
subject = new object[] { obj14 };
this.typeRequestCert.InvokeMember("HashAlgorithm", BindingFlags.PutDispProperty, null, obj6, subject);
this.typeRequestCert.InvokeMember("Encode", BindingFlags.InvokeMethod, null, obj6, null);
object obj15 = Activator.CreateInstance(this.typeX509Enrollment);
subject[0] = obj6;
this.typeX509Enrollment.InvokeMember("InitializeFromRequest", BindingFlags.InvokeMethod, null, obj15, subject);
if (IsRoot)
{
subject[0] = "DO_NOT_TRUST_TitaniumProxy-CE";
this.typeX509Enrollment.InvokeMember("CertificateFriendlyName", BindingFlags.PutDispProperty, null, obj15, subject);
}
subject[0] = 0;
object obj16 = this.typeX509Enrollment.InvokeMember("CreateRequest", BindingFlags.InvokeMethod, null, obj15, subject);
subject = new object[] { 2, obj16, 0, string.Empty };
this.typeX509Enrollment.InvokeMember("InstallResponse", BindingFlags.InvokeMethod, null, obj15, subject);
subject = new object[] { null, 0, 1 };
string empty = string.Empty;
try
{
empty = (string)this.typeX509Enrollment.InvokeMember("CreatePFX", BindingFlags.InvokeMethod, null, obj15, subject);
return new X509Certificate2(Convert.FromBase64String(empty), string.Empty, X509KeyStorageFlags.Exportable);
}
catch (Exception exception1)
{
Exception exception = exception1;
cert = null;
}
return cert;
}
private X509Certificate2 MakeCertificateInternal(string sSubjectCN, bool isRoot, bool switchToMTAIfNeeded,X509Certificate2 signingCert=null)
{
X509Certificate2 rCert=null;
if (switchToMTAIfNeeded && Thread.CurrentThread.GetApartmentState() != ApartmentState.MTA)
{
ManualResetEvent manualResetEvent = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem((object o) =>
{
rCert = this.MakeCertificateInternal(sSubjectCN, isRoot, false,signingCert);
manualResetEvent.Set();
});
manualResetEvent.WaitOne();
manualResetEvent.Close();
return rCert;
}
string fullSubject = string.Format("CN={0}{1}", sSubjectCN, "");//Subject
string HashAlgo = "SHA256"; //Sig Algo
int GraceDays = -366; //Grace Days
int ValidDays = 1825; //ValiDays
int keyLength = 2048; //KeyLength
DateTime graceTime = DateTime.Now.AddDays((double)GraceDays);
DateTime now = DateTime.Now;
try
{
if (!isRoot)
{
rCert = this.MakeCertificate(false, sSubjectCN, fullSubject, keyLength, HashAlgo, graceTime, now.AddDays((double)ValidDays), signingCert);
}
else
{
rCert = this.MakeCertificate(true, sSubjectCN, fullSubject, keyLength, HashAlgo, graceTime, now.AddDays((double)ValidDays), null);
}
}
catch (Exception e)
{
throw e;
}
return rCert;
}
}
}
using System;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Reflection;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
using System.Collections.Concurrent;
using System.IO;
namespace Titanium.Web.Proxy.Network
{
......@@ -15,74 +13,86 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
internal class CertificateManager : IDisposable
{
private const string CertCreateFormat =
"-ss {0} -n \"CN={1}, O={2}\" -sky {3} -cy {4} -m 120 -a sha256 -eku 1.3.6.1.5.5.7.3.1 {5}";
private CertificateMaker certEngine = null;
private bool clearCertificates { get; set; }
/// <summary>
/// Cache dictionary
/// </summary>
private readonly IDictionary<string, CachedCertificate> certificateCache;
/// <summary>
/// A lock to manage concurrency
/// </summary>
private SemaphoreSlim semaphoreLock = new SemaphoreSlim(1);
private Action<Exception> exceptionFunc;
internal string Issuer { get; private set; }
internal string RootCertificateName { get; private set; }
internal X509Store MyStore { get; private set; }
internal X509Store RootStore { get; private set; }
internal X509Certificate2 rootCertificate { get; set; }
internal CertificateManager(string issuer, string rootCertificateName)
internal CertificateManager(string issuer, string rootCertificateName, Action<Exception> exceptionFunc)
{
Issuer = issuer;
RootCertificateName = rootCertificateName;
this.exceptionFunc = exceptionFunc;
MyStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
RootStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
certEngine = new CertificateMaker();
certificateCache = new Dictionary<string, CachedCertificate>();
Issuer = issuer;
RootCertificateName = rootCertificateName;
certificateCache = new ConcurrentDictionary<string, CachedCertificate>();
}
/// <summary>
/// Attempts to move a self-signed certificate to the root store.
/// </summary>
/// <returns>true if succeeded, else false</returns>
internal async Task<bool> CreateTrustedRootCertificate()
internal X509Certificate2 GetRootCertificate()
{
X509Certificate2 rootCertificate =
await CreateCertificate(RootStore, RootCertificateName, true);
var fileName = Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "rootCert.pfx");
return rootCertificate != null;
if (File.Exists(fileName))
{
try
{
return new X509Certificate2(fileName, string.Empty, X509KeyStorageFlags.Exportable);
}
catch (Exception e)
{
exceptionFunc(e);
return null;
}
}
return null;
}
/// <summary>
/// Attempts to remove the self-signed certificate from the root store.
/// Attempts to create a RootCertificate
/// </summary>
/// <returns>true if succeeded, else false</returns>
internal bool DestroyTrustedRootCertificate()
{
return DestroyCertificate(RootStore, RootCertificateName, false);
}
internal X509Certificate2Collection FindCertificates(string certificateSubject)
internal bool CreateTrustedRootCertificate()
{
return FindCertificates(MyStore, certificateSubject);
}
protected virtual X509Certificate2Collection FindCertificates(X509Store store, string certificateSubject)
{
X509Certificate2Collection discoveredCertificates = store.Certificates
.Find(X509FindType.FindBySubjectDistinguishedName, certificateSubject, false);
return discoveredCertificates.Count > 0 ?
discoveredCertificates : null;
}
internal async Task<X509Certificate2> CreateCertificate(string certificateName, bool isRootCertificate)
{
return await CreateCertificate(MyStore, certificateName, isRootCertificate);
rootCertificate = GetRootCertificate();
if (rootCertificate != null)
{
return true;
}
try
{
rootCertificate = CreateCertificate(RootCertificateName, true);
}
catch(Exception e)
{
exceptionFunc(e);
}
if (rootCertificate != null)
{
try
{
var fileName = Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "rootCert.pfx");
File.WriteAllBytes(fileName, rootCertificate.Export(X509ContentType.Pkcs12));
}
catch(Exception e)
{
exceptionFunc(e);
}
}
return rootCertificate != null;
}
/// <summary>
/// Create an SSL certificate
/// </summary>
......@@ -90,10 +100,8 @@ namespace Titanium.Web.Proxy.Network
/// <param name="certificateName"></param>
/// <param name="isRootCertificate"></param>
/// <returns></returns>
protected async virtual Task<X509Certificate2> CreateCertificate(X509Store store, string certificateName, bool isRootCertificate)
internal virtual X509Certificate2 CreateCertificate(string certificateName, bool isRootCertificate)
{
await semaphoreLock.WaitAsync();
try
{
if (certificateCache.ContainsKey(certificateName))
......@@ -102,167 +110,46 @@ namespace Titanium.Web.Proxy.Network
cached.LastAccess = DateTime.Now;
return cached.Certificate;
}
}
catch
{
X509Certificate2 certificate = null;
store.Open(OpenFlags.ReadWrite);
string certificateSubject = string.Format("CN={0}, O={1}", certificateName, Issuer);
X509Certificate2Collection certificates;
if (isRootCertificate)
}
X509Certificate2 certificate = null;
lock (string.Intern(certificateName))
{
if (certificateCache.ContainsKey(certificateName) == false)
{
certificates = FindCertificates(store, certificateSubject);
if (certificates != null)
try
{
certificate = certificates[0];
certificate = certEngine.MakeCertificate(certificateName, isRootCertificate, rootCertificate);
}
}
if (certificate == null)
{
string[] args = new[] {
GetCertificateCreateArgs(store, certificateName) };
await CreateCertificate(args);
certificates = FindCertificates(store, certificateSubject);
//remove it from store
if (!isRootCertificate)
catch(Exception e)
{
DestroyCertificate(certificateName);
exceptionFunc(e);
}
if (certificates != null)
if (certificate != null && !certificateCache.ContainsKey(certificateName))
{
certificate = certificates[0];
certificateCache.Add(certificateName, new CachedCertificate() { Certificate = certificate });
}
}
store.Close();
if (certificate != null && !certificateCache.ContainsKey(certificateName))
else
{
certificateCache.Add(certificateName, new CachedCertificate() { Certificate = certificate });
if (certificateCache.ContainsKey(certificateName))
{
var cached = certificateCache[certificateName];
cached.LastAccess = DateTime.Now;
return cached.Certificate;
}
}
return certificate;
}
finally
{
semaphoreLock.Release();
}
}
/// <summary>
/// Create certificate using makecert.exe
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
protected virtual Task<int> CreateCertificate(string[] args)
{
// there is no non-generic TaskCompletionSource
var tcs = new TaskCompletionSource<int>();
var process = new Process();
string file = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "makecert.exe");
if (!File.Exists(file))
{
throw new Exception("Unable to locate 'makecert.exe'.");
}
process.StartInfo.Verb = "runas";
process.StartInfo.Arguments = args != null ? args[0] : string.Empty;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = file;
process.EnableRaisingEvents = true;
process.Exited += (sender, processArgs) =>
{
tcs.SetResult(process.ExitCode);
process.Dispose();
};
bool started = process.Start();
if (!started)
{
//you may allow for the process to be re-used (started = false)
//but I'm not sure about the guarantees of the Exited event in such a case
throw new InvalidOperationException("Could not start process: " + process);
}
return tcs.Task;
}
/// <summary>
/// Destroy an SSL certificate from local store
/// </summary>
/// <param name="certificateName"></param>
/// <returns></returns>
internal bool DestroyCertificate(string certificateName)
{
return DestroyCertificate(MyStore, certificateName, false);
}
/// <summary>
/// Destroy certificate from the specified store
/// optionally also remove from proxy certificate cache
/// </summary>
/// <param name="store"></param>
/// <param name="certificateName"></param>
/// <param name="removeFromCache"></param>
/// <returns></returns>
protected virtual bool DestroyCertificate(X509Store store, string certificateName, bool removeFromCache)
{
X509Certificate2Collection certificates = null;
store.Open(OpenFlags.ReadWrite);
string certificateSubject = string.Format("CN={0}, O={1}", certificateName, Issuer);
certificates = FindCertificates(store, certificateSubject);
return certificate;
if (certificates != null)
{
store.RemoveRange(certificates);
certificates = FindCertificates(store, certificateSubject);
}
store.Close();
if (removeFromCache &&
certificateCache.ContainsKey(certificateName))
{
certificateCache.Remove(certificateName);
}
return certificates == null;
}
/// <summary>
/// Create the neccessary arguments for makecert.exe to create the required certificate
/// </summary>
/// <param name="store"></param>
/// <param name="certificateName"></param>
/// <returns></returns>
protected virtual string GetCertificateCreateArgs(X509Store store, string certificateName)
{
bool isRootCertificate =
(certificateName == RootCertificateName);
string certCreatArgs = string.Format(CertCreateFormat,
store.Name, certificateName, Issuer,
isRootCertificate ? "signature" : "exchange",
isRootCertificate ? "authority" : "end",
isRootCertificate ? "-h 1 -r" : string.Format("-pe -in \"{0}\" -is Root", RootCertificateName));
return certCreatArgs;
}
private bool clearCertificates { get; set; }
/// <summary>
/// Stops the certificate cache clear process
/// </summary>
......@@ -279,7 +166,6 @@ namespace Titanium.Web.Proxy.Network
clearCertificates = true;
while (clearCertificates)
{
await semaphoreLock.WaitAsync();
try
{
......@@ -292,26 +178,49 @@ namespace Titanium.Web.Proxy.Network
foreach (var cache in outdated)
certificateCache.Remove(cache.Key);
}
finally {
semaphoreLock.Release();
finally
{
}
//after a minute come back to check for outdated certificates in cache
await Task.Delay(1000 * 60);
}
}
public void Dispose()
internal bool TrustRootCertificate()
{
if (MyStore != null)
if (rootCertificate == null)
{
MyStore.Close();
return false;
}
try
{
X509Store x509RootStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
var x509PersonalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
x509RootStore.Open(OpenFlags.ReadWrite);
x509PersonalStore.Open(OpenFlags.ReadWrite);
if (RootStore != null)
try
{
x509RootStore.Add(rootCertificate);
x509PersonalStore.Add(rootCertificate);
}
finally
{
x509RootStore.Close();
x509PersonalStore.Close();
}
return true;
}
catch
{
RootStore.Close();
return false;
}
}
public void Dispose()
{
}
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@
using System.IO;
using System.Net.Sockets;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Network
{
......@@ -10,11 +11,20 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
public class TcpConnection : IDisposable
{
internal ExternalProxy UpStreamHttpProxy { get; set; }
internal ExternalProxy UpStreamHttpsProxy { get; set; }
internal string HostName { get; set; }
internal int port { 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>
......@@ -27,6 +37,16 @@ namespace Titanium.Web.Proxy.Network
/// </summary>
internal Stream Stream { get; set; }
/// <summary>
/// Last time this connection was used
/// </summary>
internal DateTime LastAccess { get; set; }
internal TcpConnection()
{
LastAccess = DateTime.Now;
}
public void Dispose()
{
Stream.Close();
......
......@@ -7,6 +7,7 @@ using System.Net.Security;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Models;
using System.Security.Authentication;
using System.Linq;
namespace Titanium.Web.Proxy.Network
{
......@@ -34,7 +35,7 @@ namespace Titanium.Web.Proxy.Network
/// <param name="clientStream"></param>
/// <returns></returns>
internal async Task<TcpConnection> CreateClient(int bufferSize, int connectionTimeOutSeconds,
string remoteHostName, int remotePort, Version httpVersion,
string remoteHostName, int remotePort, Version httpVersion,
bool isHttps, SslProtocols supportedSslProtocols,
RemoteCertificateValidationCallback remoteCertificateValidationCallback, LocalCertificateSelectionCallback localCertificateSelectionCallback,
ExternalProxy externalHttpProxy, ExternalProxy externalHttpsProxy,
......@@ -48,16 +49,22 @@ namespace Titanium.Web.Proxy.Network
SslStream sslStream = null;
//If this proxy uses another external proxy then create a tunnel request for HTTPS connections
if (externalHttpsProxy != null)
if (externalHttpsProxy != null && externalHttpsProxy.HostName != remoteHostName)
{
client = new TcpClient(externalHttpsProxy.HostName, externalHttpsProxy.Port);
stream = client.GetStream();
using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize, true))
{
await writer.WriteLineAsync(string.Format("CONNECT {0}:{1} {2}", remoteHostName, remotePort, httpVersion));
await writer.WriteLineAsync(string.Format("Host: {0}:{1}", remoteHostName, remotePort));
await writer.WriteLineAsync($"CONNECT {remoteHostName}:{remotePort} HTTP/{httpVersion}");
await writer.WriteLineAsync($"Host: {remoteHostName}:{remotePort}");
await writer.WriteLineAsync("Connection: Keep-Alive");
if (!string.IsNullOrEmpty(externalHttpsProxy.UserName) && externalHttpsProxy.Password != null)
{
await writer.WriteLineAsync("Proxy-Connection: keep-alive");
await writer.WriteLineAsync("Proxy-Authorization" + ": Basic " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(externalHttpsProxy.UserName + ":" + externalHttpsProxy.Password)));
}
await writer.WriteLineAsync();
await writer.FlushAsync();
writer.Close();
......@@ -67,7 +74,8 @@ namespace Titanium.Web.Proxy.Network
{
var result = await reader.ReadLineAsync();
if (!result.ToLower().Contains("200 connection established"))
if (!new string[] { "200 OK", "connection established" }.Any(s=> result.ToLower().Contains(s.ToLower())))
{
throw new Exception("Upstream proxy failed to create a secure tunnel");
}
......@@ -92,17 +100,14 @@ namespace Titanium.Web.Proxy.Network
}
catch
{
if (sslStream != null)
{
sslStream.Dispose();
}
sslStream?.Dispose();
throw;
}
}
else
{
if (externalHttpProxy != null)
if (externalHttpProxy != null && externalHttpProxy.HostName != remoteHostName)
{
client = new TcpClient(externalHttpProxy.HostName, externalHttpProxy.Port);
stream = client.GetStream();
......@@ -119,15 +124,18 @@ namespace Titanium.Web.Proxy.Network
stream.ReadTimeout = connectionTimeOutSeconds * 1000;
stream.WriteTimeout = connectionTimeOutSeconds * 1000;
return new TcpConnection()
{
UpStreamHttpProxy = externalHttpProxy,
UpStreamHttpsProxy = externalHttpsProxy,
HostName = remoteHostName,
port = remotePort,
Port = remotePort,
IsHttps = isHttps,
TcpClient = client,
StreamReader = new CustomBinaryReader(stream),
Stream = stream
Stream = stream,
Version = httpVersion
};
}
}
......
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy
{
public partial class ProxyServer
{
private async Task<bool> CheckAuthorization(StreamWriter clientStreamWriter, IEnumerable<HttpHeader> Headers)
{
if (AuthenticateUserFunc == null)
{
return true;
}
var httpHeaders = Headers as HttpHeader[] ?? Headers.ToArray();
try
{
if (!httpHeaders.Where(t => t.Name == "Proxy-Authorization").Any())
{
await WriteResponseStatus(new Version(1, 1), "407",
"Proxy Authentication Required", clientStreamWriter);
var response = new Response();
response.ResponseHeaders = new Dictionary<string, HttpHeader>();
response.ResponseHeaders.Add("Proxy-Authenticate", new HttpHeader("Proxy-Authenticate", "Basic realm=\"TitaniumProxy\""));
response.ResponseHeaders.Add("Proxy-Connection", new HttpHeader("Proxy-Connection", "close"));
await WriteResponseHeaders(clientStreamWriter, response);
await clientStreamWriter.WriteLineAsync();
return false;
}
else
{
var headerValue = httpHeaders.Where(t => t.Name == "Proxy-Authorization").FirstOrDefault().Value.Trim();
if (!headerValue.ToLower().StartsWith("basic"))
{
//Return not authorized
await WriteResponseStatus(new Version(1, 1), "407",
"Proxy Authentication Invalid", clientStreamWriter);
var response = new Response();
response.ResponseHeaders = new Dictionary<string, HttpHeader>();
response.ResponseHeaders.Add("Proxy-Authenticate", new HttpHeader("Proxy-Authenticate", "Basic realm=\"TitaniumProxy\""));
response.ResponseHeaders.Add("Proxy-Connection", new HttpHeader("Proxy-Connection", "close"));
await WriteResponseHeaders(clientStreamWriter, response);
await clientStreamWriter.WriteLineAsync();
return false;
}
headerValue = headerValue.Substring(5).Trim();
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue));
if (decoded.Contains(":") == false)
{
//Return not authorized
await WriteResponseStatus(new Version(1, 1), "407",
"Proxy Authentication Invalid", clientStreamWriter);
var response = new Response();
response.ResponseHeaders = new Dictionary<string, HttpHeader>();
response.ResponseHeaders.Add("Proxy-Authenticate", new HttpHeader("Proxy-Authenticate", "Basic realm=\"TitaniumProxy\""));
response.ResponseHeaders.Add("Proxy-Connection", new HttpHeader("Proxy-Connection", "close"));
await WriteResponseHeaders(clientStreamWriter, response);
await clientStreamWriter.WriteLineAsync();
return false;
}
var username = decoded.Substring(0, decoded.IndexOf(':'));
var password = decoded.Substring(decoded.IndexOf(':') + 1);
return await AuthenticateUserFunc(username, password).ConfigureAwait(false);
}
}
catch (Exception e)
{
ExceptionFunc(new ProxyAuthorizationException("Error whilst authorizing request", e, httpHeaders));
//Return not authorized
await WriteResponseStatus(new Version(1, 1), "407",
"Proxy Authentication Invalid", clientStreamWriter);
var response = new Response();
response.ResponseHeaders = new Dictionary<string, HttpHeader>();
response.ResponseHeaders.Add("Proxy-Authenticate", new HttpHeader("Proxy-Authenticate", "Basic realm=\"TitaniumProxy\""));
response.ResponseHeaders.Add("Proxy-Connection", new HttpHeader("Proxy-Connection", "close"));
await WriteResponseHeaders(clientStreamWriter, response);
await clientStreamWriter.WriteLineAsync();
return false;
}
}
}
}
......@@ -17,10 +17,11 @@ namespace Titanium.Web.Proxy
/// </summary>
public partial class ProxyServer : IDisposable
{
/// <summary>
/// Does the root certificate used by this proxy is trusted by the machine?
/// Is the root certificate used by this proxy is valid?
/// </summary>
private bool certTrusted { get; set; }
private bool certValidated { get; set; }
/// <summary>
/// Is the proxy currently running
......@@ -32,6 +33,16 @@ namespace Titanium.Web.Proxy
/// </summary>
private CertificateManager certificateCacheManager { get; set; }
/// <summary>
/// An default exception log func
/// </summary>
private readonly Lazy<Action<Exception>> defaultExceptionFunc = new Lazy<Action<Exception>>(() => (e => { }));
/// <summary>
/// backing exception func for exposed public property
/// </summary>
private Action<Exception> exceptionFunc;
/// <summary>
/// A object that creates tcp connection to server
/// </summary>
......@@ -56,9 +67,19 @@ namespace Titanium.Web.Proxy
/// <summary>
/// Name of the root certificate
/// If no certificate is provided then a default Root Certificate will be created and used
/// The provided root certificate has to be in the proxy exe directory with the private key
/// The root certificate file should be named as "rootCert.pfx"
/// </summary>
public string RootCertificateName { get; set; }
/// <summary>
/// Trust the RootCertificate used by this proxy server
/// Note that this do not make the client trust the certificate!
/// This would import the root certificate to the certificate store of machine that runs this proxy server
/// </summary>
public bool TrustRootCertificate { 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
......@@ -88,12 +109,12 @@ namespace Titanium.Web.Proxy
/// <summary>
/// External proxy for Http
/// </summary>
public ExternalProxy ExternalHttpProxy { get; set; }
public ExternalProxy UpStreamHttpProxy { get; set; }
/// <summary>
/// External proxy for Https
/// External proxy for Http
/// </summary>
public ExternalProxy ExternalHttpsProxy { get; set; }
public ExternalProxy UpStreamHttpsProxy { get; set; }
/// <summary>
/// Verifies the remote Secure Sockets Layer (SSL) certificate used for authentication
......@@ -105,6 +126,51 @@ namespace Titanium.Web.Proxy
/// </summary>
public event Func<object, CertificateSelectionEventArgs, Task> ClientCertificateSelectionCallback;
/// <summary>
/// Callback for error events in proxy
/// </summary>
public Action<Exception> ExceptionFunc
{
get
{
return exceptionFunc ?? defaultExceptionFunc.Value;
}
set
{
exceptionFunc = value;
}
}
/// <summary>
/// A callback to authenticate clients
/// Parameters are username, password provided by client
/// return true for successful authentication
/// </summary>
public Func<string, string, Task<bool>> AuthenticateUserFunc
{
get;
set;
}
/// <summary>
/// A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP requests
/// return the ExternalProxy object with valid credentials
/// </summary>
public Func<SessionEventArgs, Task<ExternalProxy>> GetCustomUpStreamHttpProxyFunc
{
get;
set;
}
/// <summary>
/// A callback to provide authentication credentials for up stream proxy this proxy is using for HTTPS requests
/// return the ExternalProxy object with valid credentials
public Func<SessionEventArgs, Task<ExternalProxy>> GetCustomUpStreamHttpsProxyFunc
{
get;
set;
}
/// <summary>
/// A list of IpAddress & port this proxy is listening to
/// </summary>
......@@ -115,11 +181,25 @@ namespace Titanium.Web.Proxy
/// </summary>
public SslProtocols SupportedSslProtocols { get; set; } = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Ssl3;
/// <summary>
/// Is the proxy currently running
/// </summary>
public bool ProxyRunning => proxyRunning;
/// <summary>
/// Gets or sets a value indicating whether requests will be chained to upstream gateway.
/// </summary>
public bool ForwardToUpstreamGateway { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ProxyServer()
public ProxyServer() : this(null, null) { }
public ProxyServer(string rootCertificateName, string rootCertificateIssuerName)
{
RootCertificateName = rootCertificateName;
RootCertificateIssuerName = rootCertificateIssuerName;
//default values
ConnectionTimeOutSeconds = 120;
CertificateCacheTimeOutMinutes = 60;
......@@ -131,9 +211,6 @@ namespace Titanium.Web.Proxy
RootCertificateName = RootCertificateName ?? "Titanium Root Certificate Authority";
RootCertificateIssuerName = RootCertificateIssuerName ?? "Titanium";
certificateCacheManager = new CertificateManager(RootCertificateIssuerName,
RootCertificateName);
}
/// <summary>
......@@ -142,7 +219,7 @@ namespace Titanium.Web.Proxy
/// <param name="endPoint"></param>
public void AddEndPoint(ProxyEndPoint endPoint)
{
if (ProxyEndPoints.Any(x => x.IpAddress == endPoint.IpAddress && x.Port == endPoint.Port))
if (ProxyEndPoints.Any(x => x.IpAddress.Equals(endPoint.IpAddress) && endPoint.Port != 0 && x.Port == endPoint.Port))
{
throw new Exception("Cannot add another endpoint to same port & ip address");
}
......@@ -216,13 +293,14 @@ namespace Titanium.Web.Proxy
//If certificate was trusted by the machine
if (certTrusted)
if (certValidated)
{
systemProxySettingsManager.SetHttpsProxy(
Equals(endPoint.IpAddress, IPAddress.Any) | Equals(endPoint.IpAddress, IPAddress.Loopback) ? "127.0.0.1" : endPoint.IpAddress.ToString(),
endPoint.Port);
}
endPoint.IsSystemHttpsProxy = true;
#if !DEBUG
......@@ -265,7 +343,21 @@ namespace Titanium.Web.Proxy
throw new Exception("Proxy is already running.");
}
certTrusted = certificateCacheManager.CreateTrustedRootCertificate().Result;
certificateCacheManager = new CertificateManager(RootCertificateIssuerName,
RootCertificateName, ExceptionFunc);
certValidated = certificateCacheManager.CreateTrustedRootCertificate();
if (TrustRootCertificate)
{
certificateCacheManager.TrustRootCertificate();
}
if (ForwardToUpstreamGateway && GetCustomUpStreamHttpProxyFunc == null && GetCustomUpStreamHttpsProxyFunc == null)
{
GetCustomUpStreamHttpProxyFunc = GetSystemUpStreamProxy;
GetCustomUpStreamHttpsProxyFunc = GetSystemUpStreamProxy;
}
foreach (var endPoint in ProxyEndPoints)
{
......@@ -277,6 +369,28 @@ namespace Titanium.Web.Proxy
proxyRunning = true;
}
/// <summary>
/// Gets the system up stream proxy.
/// </summary>
/// <param name="sessionEventArgs">The <see cref="SessionEventArgs"/> instance containing the event data.</param>
/// <returns><see cref="ExternalProxy"/> instance containing valid proxy configuration from PAC/WAPD scripts if any exists.</returns>
private Task<ExternalProxy> GetSystemUpStreamProxy(SessionEventArgs sessionEventArgs)
{
// Use built-in WebProxy class to handle PAC/WAPD scripts.
var systemProxyResolver = new WebProxy();
var systemProxyUri = systemProxyResolver.GetProxy(sessionEventArgs.WebSession.Request.RequestUri);
// TODO: Apply authorization
var systemProxy = new ExternalProxy
{
HostName = systemProxyUri.Host,
Port = systemProxyUri.Port
};
return Task.FromResult(systemProxy);
}
/// <summary>
/// Stop this proxy server
/// </summary>
......@@ -304,7 +418,7 @@ namespace Titanium.Web.Proxy
ProxyEndPoints.Clear();
certificateCacheManager.StopClearIdleCertificates();
certificateCacheManager?.StopClearIdleCertificates();
proxyRunning = false;
}
......@@ -378,32 +492,40 @@ namespace Titanium.Web.Proxy
//Other errors are discarded to keep proxy running
}
if (tcpClient != null)
{
{
Task.Run(async () =>
{
try
{
{
if (endPoint.GetType() == typeof(TransparentProxyEndPoint))
{
await HandleClient(endPoint as TransparentProxyEndPoint, tcpClient);
}
else
{
await HandleClient(endPoint as ExplicitProxyEndPoint, tcpClient);
}
}
finally
{
if (tcpClient != null)
{
//This line is important!
//contributors please don't remove it without discussion
//It helps to avoid eventual deterioration of performance due to TCP port exhaustion
//due to default TCP CLOSE_WAIT timeout for 4 minutes
tcpClient.LingerState = new LingerOption(true, 0);
tcpClient.Client.Shutdown(SocketShutdown.Both);
tcpClient.Client.Close();
tcpClient.Client.Dispose();
tcpClient.Close();
}
}
});
......@@ -420,7 +542,7 @@ namespace Titanium.Web.Proxy
Stop();
}
certificateCacheManager.Dispose();
certificateCacheManager?.Dispose();
}
}
}
\ No newline at end of file
......@@ -2,10 +2,12 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Text.RegularExpressions;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Network;
......@@ -23,11 +25,14 @@ namespace Titanium.Web.Proxy
/// </summary>
partial class ProxyServer
{
//This is called when client is aware of proxy
//So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy
private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient client)
private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient tcpClient)
{
Stream clientStream = client.GetStream();
Stream clientStream = tcpClient.GetStream();
clientStream.ReadTimeout = ConnectionTimeOutSeconds * 1000;
clientStream.WriteTimeout = ConnectionTimeOutSeconds * 1000;
......@@ -77,11 +82,28 @@ namespace Titanium.Web.Proxy
var excluded = endPoint.ExcludedHttpsHostNameRegex != null ?
endPoint.ExcludedHttpsHostNameRegex.Any(x => Regex.IsMatch(httpRemoteUri.Host, x)) : false;
List<HttpHeader> connectRequestHeaders = null;
//Client wants to create a secure tcp tunnel (its a HTTPS request)
if (httpVerb.ToUpper() == "CONNECT" && !excluded && httpRemoteUri.Port != 80)
{
httpRemoteUri = new Uri("https://" + httpCmdSplit[1]);
await clientStreamReader.ReadAllLinesAsync();
string tmpLine = null;
connectRequestHeaders = new List<HttpHeader>();
while (!string.IsNullOrEmpty(tmpLine = await clientStreamReader.ReadLineAsync()))
{
var header = tmpLine.Split(ProxyConstants.ColonSplit, 2);
var newHeader = new HttpHeader(header[0], header[1]);
connectRequestHeaders.Add(newHeader);
}
if (await CheckAuthorization(clientStreamWriter, connectRequestHeaders) == false)
{
Dispose(clientStream, clientStreamReader, clientStreamWriter, null);
return;
}
await WriteConnectResponse(clientStreamWriter, version);
......@@ -91,7 +113,7 @@ namespace Titanium.Web.Proxy
{
sslStream = new SslStream(clientStream, true);
var certificate = await certificateCacheManager.CreateCertificate(httpRemoteUri.Host, false);
var certificate = certificateCacheManager.CreateCertificate(httpRemoteUri.Host, false);
//Successfully managed to authenticate the client using the fake certificate
await sslStream.AuthenticateAsServerAsync(certificate, false,
SupportedSslProtocols, false);
......@@ -125,7 +147,6 @@ namespace Titanium.Web.Proxy
//write back successfull CONNECT response
await WriteConnectResponse(clientStreamWriter, version);
await TcpHelper.SendRaw(BUFFER_SIZE, ConnectionTimeOutSeconds, httpRemoteUri.Host, httpRemoteUri.Port,
httpCmd, version, null,
false, SupportedSslProtocols,
......@@ -136,12 +157,11 @@ namespace Titanium.Web.Proxy
Dispose(clientStream, clientStreamReader, clientStreamWriter, null);
return;
}
//Now create the request
await HandleHttpSessionRequest(client, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
httpRemoteUri.Scheme == Uri.UriSchemeHttps ? httpRemoteUri.Host : null);
await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
httpRemoteUri.Scheme == Uri.UriSchemeHttps ? httpRemoteUri.Host : null, endPoint, connectRequestHeaders, null, null);
}
catch
catch (Exception)
{
Dispose(clientStream, clientStreamReader, clientStreamWriter, null);
}
......@@ -151,6 +171,7 @@ namespace Titanium.Web.Proxy
//So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client
private async Task HandleClient(TransparentProxyEndPoint endPoint, TcpClient tcpClient)
{
Stream clientStream = tcpClient.GetStream();
clientStream.ReadTimeout = ConnectionTimeOutSeconds * 1000;
......@@ -165,7 +186,7 @@ namespace Titanium.Web.Proxy
var sslStream = new SslStream(clientStream, true);
//implement in future once SNI supported by SSL stream, for now use the same certificate
certificate = await certificateCacheManager.CreateCertificate(endPoint.GenericCertificateName, false);
certificate = certificateCacheManager.CreateCertificate(endPoint.GenericCertificateName, false);
try
{
......@@ -180,10 +201,7 @@ namespace Titanium.Web.Proxy
}
catch (Exception)
{
if (sslStream != null)
{
sslStream.Dispose();
}
sslStream.Dispose();
Dispose(sslStream, clientStreamReader, clientStreamWriter, null);
return;
......@@ -193,6 +211,7 @@ namespace Titanium.Web.Proxy
else
{
clientStreamReader = new CustomBinaryReader(clientStream);
clientStreamWriter = new StreamWriter(clientStream);
}
//now read the request line
......@@ -200,8 +219,134 @@ namespace Titanium.Web.Proxy
//Now create the request
await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
endPoint.EnableSsl ? endPoint.GenericCertificateName : null);
endPoint.EnableSsl ? endPoint.GenericCertificateName : null, endPoint, null);
}
private async Task HandleHttpSessionRequestInternal(TcpConnection connection, SessionEventArgs args, ExternalProxy customUpStreamHttpProxy, ExternalProxy customUpStreamHttpsProxy, bool CloseConnection)
{
try
{
if (connection == null)
{
if (args.WebSession.Request.RequestUri.Scheme == "http")
{
if (GetCustomUpStreamHttpProxyFunc != null)
{
customUpStreamHttpProxy = await GetCustomUpStreamHttpProxyFunc(args).ConfigureAwait(false);
}
}
else
{
if (GetCustomUpStreamHttpsProxyFunc != null)
{
customUpStreamHttpsProxy = await GetCustomUpStreamHttpsProxyFunc(args).ConfigureAwait(false);
}
}
args.CustomUpStreamHttpProxyUsed = customUpStreamHttpProxy;
args.CustomUpStreamHttpsProxyUsed = customUpStreamHttpsProxy;
connection = await tcpConnectionFactory.CreateClient(BUFFER_SIZE, ConnectionTimeOutSeconds,
args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, args.WebSession.Request.HttpVersion,
args.IsHttps, SupportedSslProtocols,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
new LocalCertificateSelectionCallback(SelectClientCertificate),
customUpStreamHttpProxy ?? UpStreamHttpProxy, customUpStreamHttpsProxy ?? UpStreamHttpsProxy, args.ProxyClient.ClientStream);
}
args.WebSession.Request.RequestLocked = true;
//If request was cancelled by user then dispose the client
if (args.WebSession.Request.CancelRequest)
{
Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader, args.ProxyClient.ClientStreamWriter, args);
return;
}
//if expect continue is enabled then send the headers first
//and see if server would return 100 conitinue
if (args.WebSession.Request.ExpectContinue)
{
args.WebSession.SetConnection(connection);
await args.WebSession.SendRequest(Enable100ContinueBehaviour);
}
//If 100 continue was the response inform that to the client
if (Enable100ContinueBehaviour)
{
if (args.WebSession.Request.Is100Continue)
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100",
"Continue", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
else if (args.WebSession.Request.ExpectationFailed)
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
}
//If expect continue is not enabled then set the connectio and send request headers
if (!args.WebSession.Request.ExpectContinue)
{
args.WebSession.SetConnection(connection);
await args.WebSession.SendRequest(Enable100ContinueBehaviour);
}
//If request was modified by user
if (args.WebSession.Request.RequestBodyRead)
{
if (args.WebSession.Request.ContentEncoding != null)
{
args.WebSession.Request.RequestBody = await GetCompressedResponseBody(args.WebSession.Request.ContentEncoding, args.WebSession.Request.RequestBody);
}
//chunked send is not supported as of now
args.WebSession.Request.ContentLength = args.WebSession.Request.RequestBody.Length;
var newStream = args.WebSession.ServerConnection.Stream;
await newStream.WriteAsync(args.WebSession.Request.RequestBody, 0, args.WebSession.Request.RequestBody.Length);
}
else
{
if (!args.WebSession.Request.ExpectationFailed)
{
//If its a post/put request, then read the client html body and send it to server
if (args.WebSession.Request.Method.ToUpper() == "POST" || args.WebSession.Request.Method.ToUpper() == "PUT")
{
await SendClientRequestBody(args);
}
}
}
//If not expectation failed response was returned by server then parse response
if (!args.WebSession.Request.ExpectationFailed)
{
await HandleHttpSessionResponse(args);
}
//if connection is closing exit
if (args.WebSession.Response.ResponseKeepAlive == false)
{
Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader, args.ProxyClient.ClientStreamWriter, args);
return;
}
}
catch (Exception e)
{
ExceptionFunc(new ProxyHttpException("Error occured whilst handling session request (internal)", e, args));
Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader, args.ProxyClient.ClientStreamWriter, args);
return;
}
if (CloseConnection)
{
//dispose
connection?.Dispose();
}
}
/// <summary>
/// This is the core request handler method for a particular connection from client
/// </summary>
......@@ -213,7 +358,7 @@ namespace Titanium.Web.Proxy
/// <param name="httpsHostName"></param>
/// <returns></returns>
private async Task HandleHttpSessionRequest(TcpClient client, string httpCmd, Stream clientStream,
CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, string httpsHostName)
CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, string httpsHostName, ProxyEndPoint endPoint, List<HttpHeader> connectHeaders, ExternalProxy customUpStreamHttpProxy = null, ExternalProxy customUpStreamHttpsProxy = null)
{
TcpConnection connection = null;
......@@ -229,7 +374,22 @@ namespace Titanium.Web.Proxy
var args = new SessionEventArgs(BUFFER_SIZE, HandleHttpSessionResponse);
args.ProxyClient.TcpClient = client;
args.WebSession.ConnectHeaders = connectHeaders;
args.WebSession.ProcessId = new Lazy<int>(() =>
{
var remoteEndPoint = (IPEndPoint)args.ProxyClient.TcpClient.Client.RemoteEndPoint;
//If client is localhost get the process id
if (NetworkHelper.IsLocalIpAddress(remoteEndPoint.Address))
{
return NetworkHelper.GetProcessIdFromPort(remoteEndPoint.Port, endPoint.IpV6Enabled);
}
//can't access process Id of remote request from remote machine
return -1;
});
try
{
//break up the line into three components (method, remote URL & Http Version)
......@@ -249,6 +409,7 @@ namespace Titanium.Web.Proxy
}
}
//Read the request headers in to unique and non-unique header collections
string tmpLine;
while (!string.IsNullOrEmpty(tmpLine = await clientStreamReader.ReadLineAsync()))
......@@ -294,6 +455,13 @@ namespace Titanium.Web.Proxy
args.ProxyClient.ClientStreamReader = clientStreamReader;
args.ProxyClient.ClientStreamWriter = clientStreamWriter;
if (httpsHostName == null && (await CheckAuthorization(clientStreamWriter, args.WebSession.Request.RequestHeaders.Values) == false))
{
Dispose(clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
PrepareRequestHeaders(args.WebSession.Request.RequestHeaders, args.WebSession);
args.WebSession.Request.Host = args.WebSession.Request.RequestUri.Authority;
......@@ -325,92 +493,17 @@ namespace Titanium.Web.Proxy
}
//construct the web request that we are going to issue on behalf of the client.
if (connection == null)
{
connection = await tcpConnectionFactory.CreateClient(BUFFER_SIZE, ConnectionTimeOutSeconds,
args.WebSession.Request.RequestUri.Host, args.WebSession.Request.RequestUri.Port, httpVersion,
args.IsHttps, SupportedSslProtocols,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
new LocalCertificateSelectionCallback(SelectClientCertificate),
ExternalHttpProxy, ExternalHttpsProxy, clientStream);
}
args.WebSession.Request.RequestLocked = true;
await HandleHttpSessionRequestInternal(connection, args, customUpStreamHttpProxy, customUpStreamHttpsProxy, false).ConfigureAwait(false);
//If request was cancelled by user then dispose the client
if (args.WebSession.Request.CancelRequest)
{
Dispose(clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
//if expect continue is enabled then send the headers first
//and see if server would return 100 conitinue
if (args.WebSession.Request.ExpectContinue)
{
args.WebSession.SetConnection(connection);
await args.WebSession.SendRequest(Enable100ContinueBehaviour);
}
//If 100 continue was the response inform that to the client
if (Enable100ContinueBehaviour)
{
if (args.WebSession.Request.Is100Continue)
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100",
"Continue", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
else if (args.WebSession.Request.ExpectationFailed)
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
}
//If expect continue is not enabled then set the connectio and send request headers
if (!args.WebSession.Request.ExpectContinue)
{
args.WebSession.SetConnection(connection);
await args.WebSession.SendRequest(Enable100ContinueBehaviour);
}
//If request was modified by user
if (args.WebSession.Request.RequestBodyRead)
{
if (args.WebSession.Request.ContentEncoding != null)
{
args.WebSession.Request.RequestBody = await GetCompressedResponseBody(args.WebSession.Request.ContentEncoding, args.WebSession.Request.RequestBody);
}
//chunked send is not supported as of now
args.WebSession.Request.ContentLength = args.WebSession.Request.RequestBody.Length;
var newStream = args.WebSession.ServerConnection.Stream;
await newStream.WriteAsync(args.WebSession.Request.RequestBody, 0, args.WebSession.Request.RequestBody.Length);
}
else
{
if (!args.WebSession.Request.ExpectationFailed)
{
//If its a post/put request, then read the client html body and send it to server
if (httpMethod.ToUpper() == "POST" || httpMethod.ToUpper() == "PUT")
{
await SendClientRequestBody(args);
}
}
}
//If not expectation failed response was returned by server then parse response
if (!args.WebSession.Request.ExpectationFailed)
{
await HandleHttpSessionResponse(args);
}
//if connection is closing exit
if (args.WebSession.Response.ResponseKeepAlive == false)
{
Dispose(clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
......@@ -418,8 +511,9 @@ namespace Titanium.Web.Proxy
httpCmd = await clientStreamReader.ReadLineAsync();
}
catch
catch (Exception e)
{
ExceptionFunc(new ProxyHttpException("Error occured whilst handling session request", e, args));
Dispose(clientStream, clientStreamReader, clientStreamWriter, args);
break;
}
......@@ -475,7 +569,6 @@ namespace Titanium.Web.Proxy
webRequest.Request.RequestHeaders = requestHeaders;
}
/// <summary>
/// This is called when the request is PUT/POST to read the body
/// </summary>
......@@ -497,9 +590,7 @@ namespace Titanium.Web.Proxy
else if (args.WebSession.Request.IsChunked)
{
await args.ProxyClient.ClientStreamReader.CopyBytesToStreamChunked(BUFFER_SIZE, postStream);
}
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.IO;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Compression;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Helpers;
namespace Titanium.Web.Proxy
{
/// <summary>
/// Handle the response from server
/// </summary>
partial class ProxyServer
{
//Called asynchronously when a request was successfully and we received the response
public async Task HandleHttpSessionResponse(SessionEventArgs args)
{
//read response & headers from server
await args.WebSession.ReceiveResponse();
try
{
if (!args.WebSession.Response.ResponseBodyRead)
{
args.WebSession.Response.ResponseStream = args.WebSession.ServerConnection.Stream;
}
//If user requested call back then do it
if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked)
{
Delegate[] invocationList = BeforeResponse.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, SessionEventArgs, Task>)invocationList[i])(null, args);
}
await Task.WhenAll(handlerTasks);
}
args.WebSession.Response.ResponseLocked = true;
//Write back to client 100-conitinue response if that's what server returned
if (args.WebSession.Response.Is100Continue)
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100",
"Continue", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
else if (args.WebSession.Response.ExpectationFailed)
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
//Write back response status to client
await WriteResponseStatus(args.WebSession.Response.HttpVersion, args.WebSession.Response.ResponseStatusCode,
args.WebSession.Response.ResponseStatusDescription, args.ProxyClient.ClientStreamWriter);
if (args.WebSession.Response.ResponseBodyRead)
{
var isChunked = args.WebSession.Response.IsChunked;
var contentEncoding = args.WebSession.Response.ContentEncoding;
if (contentEncoding != null)
{
args.WebSession.Response.ResponseBody = await GetCompressedResponseBody(contentEncoding, args.WebSession.Response.ResponseBody);
if (isChunked == false)
{
args.WebSession.Response.ContentLength = args.WebSession.Response.ResponseBody.Length;
}
else
{
args.WebSession.Response.ContentLength = -1;
}
}
await WriteResponseHeaders(args.ProxyClient.ClientStreamWriter, args.WebSession.Response);
await args.ProxyClient.ClientStream.WriteResponseBody(args.WebSession.Response.ResponseBody, isChunked);
}
else
{
await WriteResponseHeaders(args.ProxyClient.ClientStreamWriter, args.WebSession.Response);
//Write body only if response is chunked or content length >0
//Is none are true then check if connection:close header exist, if so write response until server or client terminates the connection
if (args.WebSession.Response.IsChunked || args.WebSession.Response.ContentLength > 0
|| !args.WebSession.Response.ResponseKeepAlive)
{
await args.WebSession.ServerConnection.StreamReader
.WriteResponseBody(BUFFER_SIZE, args.ProxyClient.ClientStream, args.WebSession.Response.IsChunked,
args.WebSession.Response.ContentLength);
}
//write response if connection:keep-alive header exist and when version is http/1.0
//Because in Http 1.0 server can return a response without content-length (expectation being client would read until end of stream)
else if (args.WebSession.Response.ResponseKeepAlive && args.WebSession.Response.HttpVersion.Minor == 0)
{
await args.WebSession.ServerConnection.StreamReader
.WriteResponseBody(BUFFER_SIZE, args.ProxyClient.ClientStream, args.WebSession.Response.IsChunked,
args.WebSession.Response.ContentLength);
}
}
await args.ProxyClient.ClientStream.FlushAsync();
}
catch
{
Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader,
args.ProxyClient.ClientStreamWriter, args);
}
finally
{
args.Dispose();
}
}
/// <summary>
/// get the compressed response body from give response bytes
/// </summary>
/// <param name="encodingType"></param>
/// <param name="responseBodyStream"></param>
/// <returns></returns>
private async Task<byte[]> GetCompressedResponseBody(string encodingType, byte[] responseBodyStream)
{
var compressionFactory = new CompressionFactory();
var compressor = compressionFactory.Create(encodingType);
return await compressor.Compress(responseBodyStream);
}
/// <summary>
/// Write response status
/// </summary>
/// <param name="version"></param>
/// <param name="code"></param>
/// <param name="description"></param>
/// <param name="responseWriter"></param>
/// <returns></returns>
private async Task WriteResponseStatus(Version version, string code, string description,
StreamWriter responseWriter)
{
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 async Task WriteResponseHeaders(StreamWriter responseWriter, Response response)
{
FixProxyHeaders(response.ResponseHeaders);
foreach (var header in response.ResponseHeaders)
{
await responseWriter.WriteLineAsync(header.Value.ToString());
}
//write non unique request headers
foreach (var headerItem in response.NonUniqueResponseHeaders)
{
var headers = headerItem.Value;
foreach (var header in headers)
{
await responseWriter.WriteLineAsync(header.ToString());
}
}
await responseWriter.WriteLineAsync();
await responseWriter.FlushAsync();
}
/// <summary>
/// Fix proxy specific headers
/// </summary>
/// <param name="headers"></param>
private void FixProxyHeaders(Dictionary<string, HttpHeader> headers)
{
//If proxy-connection close was returned inform to close the connection
var hasProxyHeader = headers.ContainsKey("proxy-connection");
var hasConnectionheader = headers.ContainsKey("connection");
if (hasProxyHeader)
{
var proxyHeader = headers["proxy-connection"];
if (hasConnectionheader == false)
{
headers.Add("connection", new HttpHeader("connection", proxyHeader.Value));
}
else
{
var connectionHeader = headers["connection"];
connectionHeader.Value = proxyHeader.Value;
}
headers.Remove("proxy-connection");
}
}
/// <summary>
/// Handle dispose of a client/server session
/// </summary>
/// <param name="tcpClient"></param>
/// <param name="clientStream"></param>
/// <param name="clientStreamReader"></param>
/// <param name="clientStreamWriter"></param>
/// <param name="args"></param>
private void Dispose(Stream clientStream, CustomBinaryReader clientStreamReader,
StreamWriter clientStreamWriter, IDisposable args)
{
if (clientStream != null)
{
clientStream.Close();
clientStream.Dispose();
}
if (args != null)
{
args.Dispose();
}
if (clientStreamReader != null)
{
clientStreamReader.Dispose();
}
if (clientStreamWriter != null)
{
clientStreamWriter.Close();
clientStreamWriter.Dispose();
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Compression;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Helpers;
namespace Titanium.Web.Proxy
{
/// <summary>
/// Handle the response from server
/// </summary>
partial class ProxyServer
{
//Called asynchronously when a request was successfully and we received the response
public async Task HandleHttpSessionResponse(SessionEventArgs args)
{
//read response & headers from server
await args.WebSession.ReceiveResponse();
try
{
if (!args.WebSession.Response.ResponseBodyRead)
{
args.WebSession.Response.ResponseStream = args.WebSession.ServerConnection.Stream;
}
args.ReRequest = false;
//If user requested call back then do it
if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked)
{
Delegate[] invocationList = BeforeResponse.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, SessionEventArgs, Task>)invocationList[i])(this, args);
}
await Task.WhenAll(handlerTasks);
}
if(args.ReRequest)
{
await HandleHttpSessionRequestInternal(null, args, null, null, true).ConfigureAwait(false);
return;
}
args.WebSession.Response.ResponseLocked = true;
//Write back to client 100-conitinue response if that's what server returned
if (args.WebSession.Response.Is100Continue)
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100",
"Continue", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
else if (args.WebSession.Response.ExpectationFailed)
{
await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417",
"Expectation Failed", args.ProxyClient.ClientStreamWriter);
await args.ProxyClient.ClientStreamWriter.WriteLineAsync();
}
//Write back response status to client
await WriteResponseStatus(args.WebSession.Response.HttpVersion, args.WebSession.Response.ResponseStatusCode,
args.WebSession.Response.ResponseStatusDescription, args.ProxyClient.ClientStreamWriter);
if (args.WebSession.Response.ResponseBodyRead)
{
var isChunked = args.WebSession.Response.IsChunked;
var contentEncoding = args.WebSession.Response.ContentEncoding;
if (contentEncoding != null)
{
args.WebSession.Response.ResponseBody = await GetCompressedResponseBody(contentEncoding, args.WebSession.Response.ResponseBody);
if (isChunked == false)
{
args.WebSession.Response.ContentLength = args.WebSession.Response.ResponseBody.Length;
}
else
{
args.WebSession.Response.ContentLength = -1;
}
}
await WriteResponseHeaders(args.ProxyClient.ClientStreamWriter, args.WebSession.Response);
await args.ProxyClient.ClientStream.WriteResponseBody(args.WebSession.Response.ResponseBody, isChunked);
}
else
{
await WriteResponseHeaders(args.ProxyClient.ClientStreamWriter, args.WebSession.Response);
//Write body only if response is chunked or content length >0
//Is none are true then check if connection:close header exist, if so write response until server or client terminates the connection
if (args.WebSession.Response.IsChunked || args.WebSession.Response.ContentLength > 0
|| !args.WebSession.Response.ResponseKeepAlive)
{
await args.WebSession.ServerConnection.StreamReader
.WriteResponseBody(BUFFER_SIZE, args.ProxyClient.ClientStream, args.WebSession.Response.IsChunked,
args.WebSession.Response.ContentLength);
}
//write response if connection:keep-alive header exist and when version is http/1.0
//Because in Http 1.0 server can return a response without content-length (expectation being client would read until end of stream)
else if (args.WebSession.Response.ResponseKeepAlive && args.WebSession.Response.HttpVersion.Minor == 0)
{
await args.WebSession.ServerConnection.StreamReader
.WriteResponseBody(BUFFER_SIZE, args.ProxyClient.ClientStream, args.WebSession.Response.IsChunked,
args.WebSession.Response.ContentLength);
}
}
await args.ProxyClient.ClientStream.FlushAsync();
}
catch(Exception e)
{
ExceptionFunc(new ProxyHttpException("Error occured wilst handling session response", e, args));
Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader,
args.ProxyClient.ClientStreamWriter, args);
}
finally
{
args.Dispose();
}
}
/// <summary>
/// get the compressed response body from give response bytes
/// </summary>
/// <param name="encodingType"></param>
/// <param name="responseBodyStream"></param>
/// <returns></returns>
private async Task<byte[]> GetCompressedResponseBody(string encodingType, byte[] responseBodyStream)
{
var compressionFactory = new CompressionFactory();
var compressor = compressionFactory.Create(encodingType);
return await compressor.Compress(responseBodyStream);
}
/// <summary>
/// Write response status
/// </summary>
/// <param name="version"></param>
/// <param name="code"></param>
/// <param name="description"></param>
/// <param name="responseWriter"></param>
/// <returns></returns>
private async Task WriteResponseStatus(Version version, string code, string description,
StreamWriter responseWriter)
{
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 async Task WriteResponseHeaders(StreamWriter responseWriter, Response response)
{
FixProxyHeaders(response.ResponseHeaders);
foreach (var header in response.ResponseHeaders)
{
await responseWriter.WriteLineAsync(header.Value.ToString());
}
//write non unique request headers
foreach (var headerItem in response.NonUniqueResponseHeaders)
{
var headers = headerItem.Value;
foreach (var header in headers)
{
await responseWriter.WriteLineAsync(header.ToString());
}
}
await responseWriter.WriteLineAsync();
await responseWriter.FlushAsync();
}
/// <summary>
/// Fix proxy specific headers
/// </summary>
/// <param name="headers"></param>
private void FixProxyHeaders(Dictionary<string, HttpHeader> headers)
{
//If proxy-connection close was returned inform to close the connection
var hasProxyHeader = headers.ContainsKey("proxy-connection");
var hasConnectionheader = headers.ContainsKey("connection");
if (hasProxyHeader)
{
var proxyHeader = headers["proxy-connection"];
if (hasConnectionheader == false)
{
headers.Add("connection", new HttpHeader("connection", proxyHeader.Value));
}
else
{
var connectionHeader = headers["connection"];
connectionHeader.Value = proxyHeader.Value;
}
headers.Remove("proxy-connection");
}
}
/// <summary>
/// Handle dispose of a client/server session
/// </summary>
/// <param name="tcpClient"></param>
/// <param name="clientStream"></param>
/// <param name="clientStreamReader"></param>
/// <param name="clientStreamWriter"></param>
/// <param name="args"></param>
private void Dispose(Stream clientStream, CustomBinaryReader clientStreamReader,
StreamWriter clientStreamWriter, IDisposable args)
{
if (clientStream != null)
{
clientStream.Close();
clientStream.Dispose();
}
if (args != null)
{
args.Dispose();
}
if (clientStreamReader != null)
{
clientStreamReader.Dispose();
}
if (clientStreamWriter != null)
{
clientStreamWriter.Close();
clientStreamWriter.Dispose();
}
}
}
}
\ No newline at end of file
using System.Net;
using Titanium.Web.Proxy.Helpers;
namespace Titanium.Web.Proxy.Tcp
{
/// <summary>
/// Represents a managed interface of IP Helper API TcpRow struct
/// <see cref="http://msdn2.microsoft.com/en-us/library/aa366913.aspx"/>
/// </summary>
internal class TcpRow
{
/// <summary>
/// Initializes a new instance of the <see cref="TcpRow"/> class.
/// </summary>
/// <param name="tcpRow">TcpRow struct.</param>
public TcpRow(NativeMethods.TcpRow tcpRow)
{
ProcessId = tcpRow.owningPid;
int localPort = (tcpRow.localPort1 << 8) + (tcpRow.localPort2) + (tcpRow.localPort3 << 24) + (tcpRow.localPort4 << 16);
long localAddress = tcpRow.localAddr;
LocalEndPoint = new IPEndPoint(localAddress, localPort);
int remotePort = (tcpRow.remotePort1 << 8) + (tcpRow.remotePort2) + (tcpRow.remotePort3 << 24) + (tcpRow.remotePort4 << 16);
long remoteAddress = tcpRow.remoteAddr;
RemoteEndPoint = new IPEndPoint(remoteAddress, remotePort);
}
/// <summary>
/// Gets the local end point.
/// </summary>
public IPEndPoint LocalEndPoint { get; private set; }
/// <summary>
/// Gets the remote end point.
/// </summary>
public IPEndPoint RemoteEndPoint { get; private set; }
/// <summary>
/// Gets the process identifier.
/// </summary>
public int ProcessId { get; private set; }
}
}
\ No newline at end of file
using System.Collections;
using System.Collections.Generic;
namespace Titanium.Web.Proxy.Tcp
{
/// <summary>
/// Represents collection of TcpRows
/// </summary>
/// <seealso cref="System.Collections.Generic.IEnumerable{Titanium.Web.Proxy.Tcp.TcpRow}" />
internal class TcpTable : IEnumerable<TcpRow>
{
private readonly IEnumerable<TcpRow> tcpRows;
/// <summary>
/// Initializes a new instance of the <see cref="TcpTable"/> class.
/// </summary>
/// <param name="tcpRows">TcpRow collection to initialize with.</param>
public TcpTable(IEnumerable<TcpRow> tcpRows)
{
this.tcpRows = tcpRows;
}
/// <summary>
/// Gets the TCP rows.
/// </summary>
public IEnumerable<TcpRow> TcpRows => tcpRows;
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns>
public IEnumerator<TcpRow> GetEnumerator()
{
return tcpRows.GetEnumerator();
}
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
\ No newline at end of file
......@@ -25,6 +25,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<Prefer32Bit>false</Prefer32Bit>
<DocumentationFile>bin\Debug\Titanium.Web.Proxy.XML</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
<PlatformTarget>AnyCPU</PlatformTarget>
......@@ -32,6 +33,7 @@
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<DefineConstants>NET45</DefineConstants>
<Prefer32Bit>false</Prefer32Bit>
<DocumentationFile>bin\Release\Titanium.Web.Proxy.XML</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="Ionic.Zip, Version=1.9.8.0, Culture=neutral, PublicKeyToken=6583c7c814667745, processorArchitecture=MSIL">
......@@ -64,9 +66,14 @@
<Compile Include="EventArguments\CertificateSelectionEventArgs.cs" />
<Compile Include="EventArguments\CertificateValidationEventArgs.cs" />
<Compile Include="Extensions\ByteArrayExtensions.cs" />
<Compile Include="Helpers\Network.cs" />
<Compile Include="Network\CachedCertificate.cs" />
<Compile Include="Network\CertificateMaker.cs" />
<Compile Include="Network\ProxyClient.cs" />
<Compile Include="Exceptions\BodyNotFoundException.cs" />
<Compile Include="Exceptions\ProxyAuthorizationException.cs" />
<Compile Include="Exceptions\ProxyException.cs" />
<Compile Include="Exceptions\ProxyHttpException.cs" />
<Compile Include="Extensions\HttpWebResponseExtensions.cs" />
<Compile Include="Extensions\HttpWebRequestExtensions.cs" />
<Compile Include="Network\CertificateManager.cs" />
......@@ -82,6 +89,7 @@
<Compile Include="Models\HttpHeader.cs" />
<Compile Include="Http\HttpWebClient.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ProxyAuthorizationHandler.cs" />
<Compile Include="RequestHandler.cs" />
<Compile Include="ResponseHandler.cs" />
<Compile Include="Helpers\CustomBinaryReader.cs" />
......@@ -92,16 +100,23 @@
<Compile Include="Http\Responses\OkResponse.cs" />
<Compile Include="Http\Responses\RedirectResponse.cs" />
<Compile Include="Shared\ProxyConstants.cs" />
<Compile Include="Tcp\TcpRow.cs" />
<Compile Include="Tcp\TcpTable.cs" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
<COMReference Include="CERTENROLLLib">
<Guid>{728AB348-217D-11DA-B2A4-000E7BBB2B09}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<None Include="makecert.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
......
......@@ -19,6 +19,5 @@
</metadata>
<files>
<file src="bin\$configuration$\Titanium.Web.Proxy.dll" target="lib\net45" />
<file src="bin\$configuration$\makecert.exe" target="content" />
</files>
</package>
\ No newline at end of file
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