Commit 75a10f90 authored by justcoding121's avatar justcoding121

move source under src\ directory

parent 67bfa3bc
......@@ -2,7 +2,7 @@ $PSake.use_exit_on_error = $true
$Here = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$SolutionRoot = (Split-Path -parent $Here)
$SolutionRoot = "$(Split-Path -parent $Here)\src"
$ProjectName = "Titanium.Web.Proxy"
$GitHubProjectName = "Titanium-Web-Proxy"
......
......@@ -6,7 +6,7 @@ param (
function Install-Chocolatey()
{
if(-not $env:ChocolateyInstall -or -not (Test-Path "$env:ChocolateyInstall"))
if(-not $env:ChocolateyInstall -or -not (Test-Path "$env:ChocolateyInstall\*"))
{
Write-Output "Chocolatey Not Found, Installing..."
iex ((new-object net.webclient).DownloadString('http://chocolatey.org/install.ps1'))
......
baseurl: /Titanium-Web-Proxy
baseurl: /src
......@@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj" />
<ProjectReference Include="..\..\src\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj" />
</ItemGroup>
</Project>
\ No newline at end of file
......@@ -116,12 +116,6 @@
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj">
<Project>{8d73a1be-868c-42d2-9ece-f32cc1a02906}</Project>
<Name>Titanium.Web.Proxy</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
......@@ -129,5 +123,11 @@
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj">
<Project>{91018b6d-a7a9-45be-9cb3-79cbb8b169a6}</Project>
<Name>Titanium.Web.Proxy</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
\ No newline at end of file
......@@ -5,8 +5,6 @@ VisualStudioVersion = 15.0.26906.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{B6DBABDC-C985-4872-9C38-B4E5079CBC4B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Titanium.Web.Proxy", "Titanium.Web.Proxy\Titanium.Web.Proxy.csproj", "{8D73A1BE-868C-42D2-9ECE-F32CC1A02906}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{6FD3B84B-9283-4E9C-8C43-A234E9AA3EAA}"
ProjectSection(SolutionItems) = preProject
.nuget\NuGet.Config = .nuget\NuGet.Config
......@@ -29,13 +27,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{AC9AE37A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BC1E0789-D348-49CF-8B67-5E99D50EDF64}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Titanium.Web.Proxy.UnitTests", "Tests\Titanium.Web.Proxy.UnitTests\Titanium.Web.Proxy.UnitTests.csproj", "{B517E3D0-D03B-436F-AB03-34BA0D5321AF}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Titanium.Web.Proxy", "Titanium.Web.Proxy\Titanium.Web.Proxy.csproj", "{91018B6D-A7A9-45BE-9CB3-79CBB8B169A6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Titanium.Web.Proxy.UnitTests", "..\tests\Titanium.Web.Proxy.UnitTests\Titanium.Web.Proxy.UnitTests.csproj", "{B517E3D0-D03B-436F-AB03-34BA0D5321AF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Titanium.Web.Proxy.IntegrationTests", "Tests\Titanium.Web.Proxy.IntegrationTests\Titanium.Web.Proxy.IntegrationTests.csproj", "{32231301-B0FB-4F9E-98DF-B3E8A88F4C16}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Titanium.Web.Proxy.IntegrationTests", "..\tests\Titanium.Web.Proxy.IntegrationTests\Titanium.Web.Proxy.IntegrationTests.csproj", "{32231301-B0FB-4F9E-98DF-B3E8A88F4C16}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Titanium.Web.Proxy.Examples.Wpf", "Examples\Titanium.Web.Proxy.Examples.Wpf\Titanium.Web.Proxy.Examples.Wpf.csproj", "{4406CE17-9A39-4F28-8363-6169A4F799C1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Titanium.Web.Proxy.Examples.Basic", "..\examples\Titanium.Web.Proxy.Examples.Basic\Titanium.Web.Proxy.Examples.Basic.csproj", "{1FAC4205-4445-4F2B-BB8F-618E8A0C15FD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Titanium.Web.Proxy.Examples.Basic", "Examples\Titanium.Web.Proxy.Examples.Basic\Titanium.Web.Proxy.Examples.Basic.csproj", "{9A2C6980-90D1-4082-AD60-B2428F3D6197}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Titanium.Web.Proxy.Examples.Wpf", "..\examples\Titanium.Web.Proxy.Examples.Wpf\Titanium.Web.Proxy.Examples.Wpf.csproj", "{4406CE17-9A39-4F28-8363-6169A4F799C1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
......@@ -43,10 +43,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8D73A1BE-868C-42D2-9ECE-F32CC1A02906}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D73A1BE-868C-42D2-9ECE-F32CC1A02906}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D73A1BE-868C-42D2-9ECE-F32CC1A02906}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D73A1BE-868C-42D2-9ECE-F32CC1A02906}.Release|Any CPU.Build.0 = Release|Any CPU
{91018B6D-A7A9-45BE-9CB3-79CBB8B169A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91018B6D-A7A9-45BE-9CB3-79CBB8B169A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91018B6D-A7A9-45BE-9CB3-79CBB8B169A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91018B6D-A7A9-45BE-9CB3-79CBB8B169A6}.Release|Any CPU.Build.0 = Release|Any CPU
{B517E3D0-D03B-436F-AB03-34BA0D5321AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B517E3D0-D03B-436F-AB03-34BA0D5321AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B517E3D0-D03B-436F-AB03-34BA0D5321AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
......@@ -55,14 +55,14 @@ Global
{32231301-B0FB-4F9E-98DF-B3E8A88F4C16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32231301-B0FB-4F9E-98DF-B3E8A88F4C16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32231301-B0FB-4F9E-98DF-B3E8A88F4C16}.Release|Any CPU.Build.0 = Release|Any CPU
{1FAC4205-4445-4F2B-BB8F-618E8A0C15FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FAC4205-4445-4F2B-BB8F-618E8A0C15FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FAC4205-4445-4F2B-BB8F-618E8A0C15FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FAC4205-4445-4F2B-BB8F-618E8A0C15FD}.Release|Any CPU.Build.0 = Release|Any CPU
{4406CE17-9A39-4F28-8363-6169A4F799C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4406CE17-9A39-4F28-8363-6169A4F799C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4406CE17-9A39-4F28-8363-6169A4F799C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4406CE17-9A39-4F28-8363-6169A4F799C1}.Release|Any CPU.Build.0 = Release|Any CPU
{9A2C6980-90D1-4082-AD60-B2428F3D6197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A2C6980-90D1-4082-AD60-B2428F3D6197}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A2C6980-90D1-4082-AD60-B2428F3D6197}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A2C6980-90D1-4082-AD60-B2428F3D6197}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -70,8 +70,8 @@ Global
GlobalSection(NestedProjects) = preSolution
{B517E3D0-D03B-436F-AB03-34BA0D5321AF} = {BC1E0789-D348-49CF-8B67-5E99D50EDF64}
{32231301-B0FB-4F9E-98DF-B3E8A88F4C16} = {BC1E0789-D348-49CF-8B67-5E99D50EDF64}
{1FAC4205-4445-4F2B-BB8F-618E8A0C15FD} = {B6DBABDC-C985-4872-9C38-B4E5079CBC4B}
{4406CE17-9A39-4F28-8363-6169A4F799C1} = {B6DBABDC-C985-4872-9C38-B4E5079CBC4B}
{9A2C6980-90D1-4082-AD60-B2428F3D6197} = {B6DBABDC-C985-4872-9C38-B4E5079CBC4B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
EnterpriseLibraryConfigurationToolBinariesPath = .1.505.2\lib\NET35
......
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
using Titanium.Web.Proxy.Compression;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Http.Responses;
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 : SessionEventArgsBase
{
private static readonly byte[] emptyData = new byte[0];
/// <summary>
/// Backing field for corresponding public property
/// </summary>
private bool reRequest;
/// <summary>
/// Constructor to initialize the proxy
/// </summary>
internal SessionEventArgs(ProxyServer server, ProxyEndPoint endPoint,
CancellationTokenSource cancellationTokenSource)
: this(server, endPoint, null, cancellationTokenSource)
{
}
protected SessionEventArgs(ProxyServer server, ProxyEndPoint endPoint,
Request request, CancellationTokenSource cancellationTokenSource)
: base(server, endPoint, cancellationTokenSource, request)
{
}
private bool hasMulipartEventSubscribers => MultipartRequestPartSent != null;
/// <summary>
/// Should we send the request again ?
/// </summary>
public bool ReRequest
{
get => reRequest;
set
{
if (WebSession.Response.StatusCode == 0)
{
throw new Exception("Response status code is empty. Cannot request again a request " + "which was never send to server.");
}
reRequest = value;
}
}
/// <summary>
/// Occurs when multipart request part sent.
/// </summary>
public event EventHandler<MultipartRequestPartSentEventArgs> MultipartRequestPartSent;
private ICustomStreamReader getStreamReader(bool isRequest)
{
return isRequest ? ProxyClient.ClientStream : WebSession.ServerConnection.Stream;
}
private HttpWriter getStreamWriter(bool isRequest)
{
return isRequest ? (HttpWriter)ProxyClient.ClientStreamWriter : WebSession.ServerConnection.StreamWriter;
}
/// <summary>
/// Read request body content as bytes[] for current session
/// </summary>
private async Task readRequestBodyAsync(CancellationToken cancellationToken)
{
WebSession.Request.EnsureBodyAvailable(false);
var request = WebSession.Request;
// If not already read (not cached yet)
if (!request.IsBodyRead)
{
var body = await readBodyAsync(true, cancellationToken);
request.Body = body;
// Now set the flag to true
// So that next time we can deliver body from cache
request.IsBodyRead = true;
OnDataSent(body, 0, body.Length);
}
}
/// <summary>
/// reinit response object
/// </summary>
internal async Task ClearResponse(CancellationToken cancellationToken)
{
// syphon out the response body from server
await SyphonOutBodyAsync(false, cancellationToken);
WebSession.Response = new Response();
}
internal void OnMultipartRequestPartSent(string boundary, HeaderCollection headers)
{
try
{
MultipartRequestPartSent?.Invoke(this, new MultipartRequestPartSentEventArgs(boundary, headers));
}
catch (Exception ex)
{
exceptionFunc(new Exception("Exception thrown in user event", ex));
}
}
/// <summary>
/// Read response body as byte[] for current response
/// </summary>
private async Task readResponseBodyAsync(CancellationToken cancellationToken)
{
if (!WebSession.Request.Locked)
{
throw new Exception("You cannot read the response body before request is made to server.");
}
var response = WebSession.Response;
if (!response.HasBody)
{
return;
}
// If not already read (not cached yet)
if (!response.IsBodyRead)
{
var body = await readBodyAsync(false, cancellationToken);
response.Body = body;
// Now set the flag to true
// So that next time we can deliver body from cache
response.IsBodyRead = true;
OnDataReceived(body, 0, body.Length);
}
}
private async Task<byte[]> readBodyAsync(bool isRequest, CancellationToken cancellationToken)
{
using (var bodyStream = new MemoryStream())
{
var writer = new HttpWriter(bodyStream, bufferPool, bufferSize);
if (isRequest)
{
await CopyRequestBodyAsync(writer, TransformationMode.Uncompress, cancellationToken);
}
else
{
await CopyResponseBodyAsync(writer, TransformationMode.Uncompress, cancellationToken);
}
return bodyStream.ToArray();
}
}
/// <summary>
/// Syphon out any left over data in given request/response from backing tcp connection.
/// When user modifies the response/request we need to do this to reuse tcp connections.
/// </summary>
/// <param name="isRequest"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
internal async Task SyphonOutBodyAsync(bool isRequest, CancellationToken cancellationToken)
{
var requestResponse = isRequest ? (RequestResponseBase)WebSession.Request : WebSession.Response;
if (requestResponse.OriginalIsBodyRead || !requestResponse.OriginalHasBody)
{
return;
}
using (var bodyStream = new MemoryStream())
{
var writer = new HttpWriter(bodyStream, bufferPool, bufferSize);
await copyBodyAsync(isRequest, true, writer, TransformationMode.None, null, cancellationToken);
}
}
/// <summary>
/// This is called when the request is PUT/POST/PATCH to read the body
/// </summary>
/// <returns></returns>
internal async Task CopyRequestBodyAsync(HttpWriter writer, TransformationMode transformation, CancellationToken cancellationToken)
{
var request = WebSession.Request;
long contentLength = request.ContentLength;
// send the request body bytes to server
if (contentLength > 0 && hasMulipartEventSubscribers && request.IsMultipartFormData)
{
var reader = getStreamReader(true);
string boundary = HttpHelper.GetBoundaryFromContentType(request.ContentType);
using (var copyStream = new CopyStream(reader, writer, bufferPool, bufferSize))
{
while (contentLength > copyStream.ReadBytes)
{
long read = await readUntilBoundaryAsync(copyStream, contentLength, boundary, cancellationToken);
if (read == 0)
{
break;
}
if (contentLength > copyStream.ReadBytes)
{
var headers = new HeaderCollection();
await HeaderParser.ReadHeaders(copyStream, headers, cancellationToken);
OnMultipartRequestPartSent(boundary, headers);
}
}
await copyStream.FlushAsync(cancellationToken);
}
}
else
{
await copyBodyAsync(true, false, writer, transformation, OnDataSent, cancellationToken);
}
}
internal async Task CopyResponseBodyAsync(HttpWriter writer, TransformationMode transformation, CancellationToken cancellationToken)
{
await copyBodyAsync(false, false, writer, transformation, OnDataReceived, cancellationToken);
}
private async Task copyBodyAsync(bool isRequest, bool useOriginalHeaderValues, HttpWriter writer, TransformationMode transformation, Action<byte[], int, int> onCopy, CancellationToken cancellationToken)
{
var stream = getStreamReader(isRequest);
var requestResponse = isRequest ? (RequestResponseBase)WebSession.Request : WebSession.Response;
bool isChunked = useOriginalHeaderValues? requestResponse.OriginalIsChunked : requestResponse.IsChunked;
long contentLength = useOriginalHeaderValues ? requestResponse.OriginalContentLength : requestResponse.ContentLength;
if (transformation == TransformationMode.None)
{
await writer.CopyBodyAsync(stream, isChunked, contentLength, onCopy, cancellationToken);
return;
}
LimitedStream limitedStream;
Stream decompressStream = null;
string contentEncoding = useOriginalHeaderValues ? requestResponse.OriginalContentEncoding : requestResponse.ContentEncoding;
Stream s = limitedStream = new LimitedStream(stream, bufferPool, isChunked, contentLength);
if (transformation == TransformationMode.Uncompress && contentEncoding != null)
{
s = decompressStream = DecompressionFactory.Create(contentEncoding, s);
}
try
{
using (var bufStream = new CustomBufferedStream(s, bufferPool, bufferSize, true))
{
await writer.CopyBodyAsync(bufStream, false, -1, onCopy, cancellationToken);
}
}
finally
{
decompressStream?.Dispose();
await limitedStream.Finish();
limitedStream.Dispose();
}
}
/// <summary>
/// Read a line from the byte stream
/// </summary>
/// <returns></returns>
private async Task<long> readUntilBoundaryAsync(ICustomStreamReader reader, long totalBytesToRead, string boundary, CancellationToken cancellationToken)
{
int bufferDataLength = 0;
var buffer = bufferPool.GetBuffer(bufferSize);
try
{
int boundaryLength = boundary.Length + 4;
long bytesRead = 0;
while (bytesRead < totalBytesToRead && (reader.DataAvailable || await reader.FillBufferAsync(cancellationToken)))
{
byte newChar = reader.ReadByteFromBuffer();
buffer[bufferDataLength] = newChar;
bufferDataLength++;
bytesRead++;
if (bufferDataLength >= boundaryLength)
{
int startIdx = bufferDataLength - boundaryLength;
if (buffer[startIdx] == '-' && buffer[startIdx + 1] == '-')
{
startIdx += 2;
bool ok = true;
for (int i = 0; i < boundary.Length; i++)
{
if (buffer[startIdx + i] != boundary[i])
{
ok = false;
break;
}
}
if (ok)
{
break;
}
}
}
if (bufferDataLength == buffer.Length)
{
// boundary is not longer than 70 bytes according to the specification, so keeping the last 100 (minimum 74) bytes is enough
const int bytesToKeep = 100;
Buffer.BlockCopy(buffer, buffer.Length - bytesToKeep, buffer, 0, bytesToKeep);
bufferDataLength = bytesToKeep;
}
}
return bytesRead;
}
finally
{
bufferPool.ReturnBuffer(buffer);
}
}
/// <summary>
/// Gets the request body as bytes.
/// </summary>
/// <param name="cancellationToken">Optional cancellation token for this async task.</param>
/// <returns>The body as bytes.</returns>
public async Task<byte[]> GetRequestBody(CancellationToken cancellationToken = default)
{
if (!WebSession.Request.IsBodyRead)
{
await readRequestBodyAsync(cancellationToken);
}
return WebSession.Request.Body;
}
/// <summary>
/// Gets the request body as string.
/// </summary>
/// <param name="cancellationToken">Optional cancellation token for this async task.</param>
/// <returns>The body as string.</returns>
public async Task<string> GetRequestBodyAsString(CancellationToken cancellationToken = default)
{
if (!WebSession.Request.IsBodyRead)
{
await readRequestBodyAsync(cancellationToken);
}
return WebSession.Request.BodyString;
}
/// <summary>
/// Sets the request body.
/// </summary>
/// <param name="body">The request body bytes.</param>
public void SetRequestBody(byte[] body)
{
var request = WebSession.Request;
if (request.Locked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
request.Body = body;
}
/// <summary>
/// Sets the body with the specified string.
/// </summary>
/// <param name="body">The request body string to set.</param>
public void SetRequestBodyString(string body)
{
if (WebSession.Request.Locked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
SetRequestBody(WebSession.Request.Encoding.GetBytes(body));
}
/// <summary>
/// Gets the response body as bytes.
/// </summary>
/// <param name="cancellationToken">Optional cancellation token for this async task.</param>
/// <returns>The resulting bytes.</returns>
public async Task<byte[]> GetResponseBody(CancellationToken cancellationToken = default)
{
if (!WebSession.Response.IsBodyRead)
{
await readResponseBodyAsync(cancellationToken);
}
return WebSession.Response.Body;
}
/// <summary>
/// Gets the response body as string.
/// </summary>
/// <param name="cancellationToken">Optional cancellation token for this async task.</param>
/// <returns>The string body.</returns>
public async Task<string> GetResponseBodyAsString(CancellationToken cancellationToken = default)
{
if (!WebSession.Response.IsBodyRead)
{
await readResponseBodyAsync(cancellationToken);
}
return WebSession.Response.BodyString;
}
/// <summary>
/// Set the response body bytes.
/// </summary>
/// <param name="body">The body bytes to set.</param>
public void SetResponseBody(byte[] body)
{
if (!WebSession.Request.Locked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
var response = WebSession.Response;
response.Body = body;
}
/// <summary>
/// Replace the response body with the specified string.
/// </summary>
/// <param name="body">The body string to set.</param>
public void SetResponseBodyString(string body)
{
if (!WebSession.Request.Locked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
var bodyBytes = WebSession.Response.Encoding.GetBytes(body);
SetResponseBody(bodyBytes);
}
/// <summary>
/// Before request is made to server respond with the specified HTML string to client
/// and ignore the request.
/// </summary>
/// <param name="html">HTML content to sent.</param>
/// <param name="headers">HTTP response headers.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void Ok(string html, Dictionary<string, HttpHeader> headers = null,
bool closeServerConnection = false)
{
var response = new OkResponse();
if (headers != null)
{
response.Headers.AddHeaders(headers);
}
response.HttpVersion = WebSession.Request.HttpVersion;
response.Body = response.Encoding.GetBytes(html ?? string.Empty);
Respond(response, closeServerConnection);
}
/// <summary>
/// Before request is made to server respond with the specified byte[] to client
/// and ignore the request.
/// </summary>
/// <param name="result">The html content bytes.</param>
/// <param name="headers">The HTTP headers.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void Ok(byte[] result, Dictionary<string, HttpHeader> headers = null,
bool closeServerConnection = false)
{
var response = new OkResponse();
response.Headers.AddHeaders(headers);
response.HttpVersion = WebSession.Request.HttpVersion;
response.Body = result;
Respond(response, closeServerConnection);
}
/// <summary>
/// Before request is made to server 
/// respond with the specified HTML string and the specified status to client.
/// And then ignore the request. 
/// </summary>
/// <param name="html">The html content.</param>
/// <param name="status">The HTTP status code.</param>
/// <param name="headers">The HTTP headers.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void GenericResponse(string html, HttpStatusCode status,
Dictionary<string, HttpHeader> headers = null, bool closeServerConnection = false)
{
var response = new GenericResponse(status);
response.HttpVersion = WebSession.Request.HttpVersion;
response.Headers.AddHeaders(headers);
response.Body = response.Encoding.GetBytes(html ?? string.Empty);
Respond(response, closeServerConnection);
}
/// <summary>
/// Before request is made to server respond with the specified byte[],
/// the specified status to client. And then ignore the request.
/// </summary>
/// <param name="result">The bytes to sent.</param>
/// <param name="status">The HTTP status code.</param>
/// <param name="headers">The HTTP headers.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void GenericResponse(byte[] result, HttpStatusCode status,
Dictionary<string, HttpHeader> headers, bool closeServerConnection = false)
{
var response = new GenericResponse(status);
response.HttpVersion = WebSession.Request.HttpVersion;
response.Headers.AddHeaders(headers);
response.Body = result;
Respond(response, closeServerConnection);
}
/// <summary>
/// Redirect to provided URL.
/// </summary>
/// <param name="url">The URL to redirect.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void Redirect(string url, bool closeServerConnection = false)
{
var response = new RedirectResponse();
response.HttpVersion = WebSession.Request.HttpVersion;
response.Headers.AddHeader(KnownHeaders.Location, url);
response.Body = emptyData;
Respond(response, closeServerConnection);
}
/// <summary>
/// Respond with given response object to client.
/// </summary>
/// <param name="response">The response object.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void Respond(Response response, bool closeServerConnection = false)
{
//request already send/ready to be sent.
if (WebSession.Request.Locked)
{
//response already received from server and ready to be sent to client.
if (WebSession.Response.Locked)
{
throw new Exception("You cannot call this function after response is sent to the client.");
}
//cleanup original response.
if (closeServerConnection)
{
//no need to cleanup original connection.
//it will be closed any way.
TerminateServerConnection();
}
response.SetOriginalHeaders(WebSession.Response);
//response already received from server but not yet ready to sent to client.
WebSession.Response = response;
WebSession.Response.Locked = true;
}
//request not yet sent/not yet ready to be sent.
else
{
WebSession.Request.Locked = true;
WebSession.Request.CancelRequest = true;
//set new response.
WebSession.Response = response;
WebSession.Response.Locked = true;
}
}
/// <summary>
/// Terminate the connection to server at the end of this HTTP request/response session.
/// </summary>
public void TerminateServerConnection()
{
WebSession.CloseServerConnection = true;
}
/// <summary>
/// Implement any cleanup here
/// </summary>
public override void Dispose()
{
MultipartRequestPartSent = null;
base.Dispose();
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
using Titanium.Web.Proxy.Compression;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Http.Responses;
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 : SessionEventArgsBase
{
private static readonly byte[] emptyData = new byte[0];
/// <summary>
/// Backing field for corresponding public property
/// </summary>
private bool reRequest;
/// <summary>
/// Constructor to initialize the proxy
/// </summary>
internal SessionEventArgs(ProxyServer server, ProxyEndPoint endPoint,
CancellationTokenSource cancellationTokenSource)
: this(server, endPoint, null, cancellationTokenSource)
{
}
protected SessionEventArgs(ProxyServer server, ProxyEndPoint endPoint,
Request request, CancellationTokenSource cancellationTokenSource)
: base(server, endPoint, cancellationTokenSource, request)
{
}
private bool hasMulipartEventSubscribers => MultipartRequestPartSent != null;
/// <summary>
/// Should we send the request again ?
/// </summary>
public bool ReRequest
{
get => reRequest;
set
{
if (WebSession.Response.StatusCode == 0)
{
throw new Exception("Response status code is empty. Cannot request again a request " + "which was never send to server.");
}
reRequest = value;
}
}
/// <summary>
/// Occurs when multipart request part sent.
/// </summary>
public event EventHandler<MultipartRequestPartSentEventArgs> MultipartRequestPartSent;
private ICustomStreamReader getStreamReader(bool isRequest)
{
return isRequest ? ProxyClient.ClientStream : WebSession.ServerConnection.Stream;
}
private HttpWriter getStreamWriter(bool isRequest)
{
return isRequest ? (HttpWriter)ProxyClient.ClientStreamWriter : WebSession.ServerConnection.StreamWriter;
}
/// <summary>
/// Read request body content as bytes[] for current session
/// </summary>
private async Task readRequestBodyAsync(CancellationToken cancellationToken)
{
WebSession.Request.EnsureBodyAvailable(false);
var request = WebSession.Request;
// If not already read (not cached yet)
if (!request.IsBodyRead)
{
var body = await readBodyAsync(true, cancellationToken);
request.Body = body;
// Now set the flag to true
// So that next time we can deliver body from cache
request.IsBodyRead = true;
OnDataSent(body, 0, body.Length);
}
}
/// <summary>
/// reinit response object
/// </summary>
internal async Task ClearResponse(CancellationToken cancellationToken)
{
// syphon out the response body from server
await SyphonOutBodyAsync(false, cancellationToken);
WebSession.Response = new Response();
}
internal void OnMultipartRequestPartSent(string boundary, HeaderCollection headers)
{
try
{
MultipartRequestPartSent?.Invoke(this, new MultipartRequestPartSentEventArgs(boundary, headers));
}
catch (Exception ex)
{
exceptionFunc(new Exception("Exception thrown in user event", ex));
}
}
/// <summary>
/// Read response body as byte[] for current response
/// </summary>
private async Task readResponseBodyAsync(CancellationToken cancellationToken)
{
if (!WebSession.Request.Locked)
{
throw new Exception("You cannot read the response body before request is made to server.");
}
var response = WebSession.Response;
if (!response.HasBody)
{
return;
}
// If not already read (not cached yet)
if (!response.IsBodyRead)
{
var body = await readBodyAsync(false, cancellationToken);
response.Body = body;
// Now set the flag to true
// So that next time we can deliver body from cache
response.IsBodyRead = true;
OnDataReceived(body, 0, body.Length);
}
}
private async Task<byte[]> readBodyAsync(bool isRequest, CancellationToken cancellationToken)
{
using (var bodyStream = new MemoryStream())
{
var writer = new HttpWriter(bodyStream, bufferPool, bufferSize);
if (isRequest)
{
await CopyRequestBodyAsync(writer, TransformationMode.Uncompress, cancellationToken);
}
else
{
await CopyResponseBodyAsync(writer, TransformationMode.Uncompress, cancellationToken);
}
return bodyStream.ToArray();
}
}
/// <summary>
/// Syphon out any left over data in given request/response from backing tcp connection.
/// When user modifies the response/request we need to do this to reuse tcp connections.
/// </summary>
/// <param name="isRequest"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
internal async Task SyphonOutBodyAsync(bool isRequest, CancellationToken cancellationToken)
{
var requestResponse = isRequest ? (RequestResponseBase)WebSession.Request : WebSession.Response;
if (requestResponse.OriginalIsBodyRead || !requestResponse.OriginalHasBody)
{
return;
}
using (var bodyStream = new MemoryStream())
{
var writer = new HttpWriter(bodyStream, bufferPool, bufferSize);
await copyBodyAsync(isRequest, true, writer, TransformationMode.None, null, cancellationToken);
}
}
/// <summary>
/// This is called when the request is PUT/POST/PATCH to read the body
/// </summary>
/// <returns></returns>
internal async Task CopyRequestBodyAsync(HttpWriter writer, TransformationMode transformation, CancellationToken cancellationToken)
{
var request = WebSession.Request;
long contentLength = request.ContentLength;
// send the request body bytes to server
if (contentLength > 0 && hasMulipartEventSubscribers && request.IsMultipartFormData)
{
var reader = getStreamReader(true);
string boundary = HttpHelper.GetBoundaryFromContentType(request.ContentType);
using (var copyStream = new CopyStream(reader, writer, bufferPool, bufferSize))
{
while (contentLength > copyStream.ReadBytes)
{
long read = await readUntilBoundaryAsync(copyStream, contentLength, boundary, cancellationToken);
if (read == 0)
{
break;
}
if (contentLength > copyStream.ReadBytes)
{
var headers = new HeaderCollection();
await HeaderParser.ReadHeaders(copyStream, headers, cancellationToken);
OnMultipartRequestPartSent(boundary, headers);
}
}
await copyStream.FlushAsync(cancellationToken);
}
}
else
{
await copyBodyAsync(true, false, writer, transformation, OnDataSent, cancellationToken);
}
}
internal async Task CopyResponseBodyAsync(HttpWriter writer, TransformationMode transformation, CancellationToken cancellationToken)
{
await copyBodyAsync(false, false, writer, transformation, OnDataReceived, cancellationToken);
}
private async Task copyBodyAsync(bool isRequest, bool useOriginalHeaderValues, HttpWriter writer, TransformationMode transformation, Action<byte[], int, int> onCopy, CancellationToken cancellationToken)
{
var stream = getStreamReader(isRequest);
var requestResponse = isRequest ? (RequestResponseBase)WebSession.Request : WebSession.Response;
bool isChunked = useOriginalHeaderValues? requestResponse.OriginalIsChunked : requestResponse.IsChunked;
long contentLength = useOriginalHeaderValues ? requestResponse.OriginalContentLength : requestResponse.ContentLength;
if (transformation == TransformationMode.None)
{
await writer.CopyBodyAsync(stream, isChunked, contentLength, onCopy, cancellationToken);
return;
}
LimitedStream limitedStream;
Stream decompressStream = null;
string contentEncoding = useOriginalHeaderValues ? requestResponse.OriginalContentEncoding : requestResponse.ContentEncoding;
Stream s = limitedStream = new LimitedStream(stream, bufferPool, isChunked, contentLength);
if (transformation == TransformationMode.Uncompress && contentEncoding != null)
{
s = decompressStream = DecompressionFactory.Create(contentEncoding, s);
}
try
{
using (var bufStream = new CustomBufferedStream(s, bufferPool, bufferSize, true))
{
await writer.CopyBodyAsync(bufStream, false, -1, onCopy, cancellationToken);
}
}
finally
{
decompressStream?.Dispose();
await limitedStream.Finish();
limitedStream.Dispose();
}
}
/// <summary>
/// Read a line from the byte stream
/// </summary>
/// <returns></returns>
private async Task<long> readUntilBoundaryAsync(ICustomStreamReader reader, long totalBytesToRead, string boundary, CancellationToken cancellationToken)
{
int bufferDataLength = 0;
var buffer = bufferPool.GetBuffer(bufferSize);
try
{
int boundaryLength = boundary.Length + 4;
long bytesRead = 0;
while (bytesRead < totalBytesToRead && (reader.DataAvailable || await reader.FillBufferAsync(cancellationToken)))
{
byte newChar = reader.ReadByteFromBuffer();
buffer[bufferDataLength] = newChar;
bufferDataLength++;
bytesRead++;
if (bufferDataLength >= boundaryLength)
{
int startIdx = bufferDataLength - boundaryLength;
if (buffer[startIdx] == '-' && buffer[startIdx + 1] == '-')
{
startIdx += 2;
bool ok = true;
for (int i = 0; i < boundary.Length; i++)
{
if (buffer[startIdx + i] != boundary[i])
{
ok = false;
break;
}
}
if (ok)
{
break;
}
}
}
if (bufferDataLength == buffer.Length)
{
// boundary is not longer than 70 bytes according to the specification, so keeping the last 100 (minimum 74) bytes is enough
const int bytesToKeep = 100;
Buffer.BlockCopy(buffer, buffer.Length - bytesToKeep, buffer, 0, bytesToKeep);
bufferDataLength = bytesToKeep;
}
}
return bytesRead;
}
finally
{
bufferPool.ReturnBuffer(buffer);
}
}
/// <summary>
/// Gets the request body as bytes.
/// </summary>
/// <param name="cancellationToken">Optional cancellation token for this async task.</param>
/// <returns>The body as bytes.</returns>
public async Task<byte[]> GetRequestBody(CancellationToken cancellationToken = default)
{
if (!WebSession.Request.IsBodyRead)
{
await readRequestBodyAsync(cancellationToken);
}
return WebSession.Request.Body;
}
/// <summary>
/// Gets the request body as string.
/// </summary>
/// <param name="cancellationToken">Optional cancellation token for this async task.</param>
/// <returns>The body as string.</returns>
public async Task<string> GetRequestBodyAsString(CancellationToken cancellationToken = default)
{
if (!WebSession.Request.IsBodyRead)
{
await readRequestBodyAsync(cancellationToken);
}
return WebSession.Request.BodyString;
}
/// <summary>
/// Sets the request body.
/// </summary>
/// <param name="body">The request body bytes.</param>
public void SetRequestBody(byte[] body)
{
var request = WebSession.Request;
if (request.Locked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
request.Body = body;
}
/// <summary>
/// Sets the body with the specified string.
/// </summary>
/// <param name="body">The request body string to set.</param>
public void SetRequestBodyString(string body)
{
if (WebSession.Request.Locked)
{
throw new Exception("You cannot call this function after request is made to server.");
}
SetRequestBody(WebSession.Request.Encoding.GetBytes(body));
}
/// <summary>
/// Gets the response body as bytes.
/// </summary>
/// <param name="cancellationToken">Optional cancellation token for this async task.</param>
/// <returns>The resulting bytes.</returns>
public async Task<byte[]> GetResponseBody(CancellationToken cancellationToken = default)
{
if (!WebSession.Response.IsBodyRead)
{
await readResponseBodyAsync(cancellationToken);
}
return WebSession.Response.Body;
}
/// <summary>
/// Gets the response body as string.
/// </summary>
/// <param name="cancellationToken">Optional cancellation token for this async task.</param>
/// <returns>The string body.</returns>
public async Task<string> GetResponseBodyAsString(CancellationToken cancellationToken = default)
{
if (!WebSession.Response.IsBodyRead)
{
await readResponseBodyAsync(cancellationToken);
}
return WebSession.Response.BodyString;
}
/// <summary>
/// Set the response body bytes.
/// </summary>
/// <param name="body">The body bytes to set.</param>
public void SetResponseBody(byte[] body)
{
if (!WebSession.Request.Locked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
var response = WebSession.Response;
response.Body = body;
}
/// <summary>
/// Replace the response body with the specified string.
/// </summary>
/// <param name="body">The body string to set.</param>
public void SetResponseBodyString(string body)
{
if (!WebSession.Request.Locked)
{
throw new Exception("You cannot call this function before request is made to server.");
}
var bodyBytes = WebSession.Response.Encoding.GetBytes(body);
SetResponseBody(bodyBytes);
}
/// <summary>
/// Before request is made to server respond with the specified HTML string to client
/// and ignore the request.
/// </summary>
/// <param name="html">HTML content to sent.</param>
/// <param name="headers">HTTP response headers.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void Ok(string html, Dictionary<string, HttpHeader> headers = null,
bool closeServerConnection = false)
{
var response = new OkResponse();
if (headers != null)
{
response.Headers.AddHeaders(headers);
}
response.HttpVersion = WebSession.Request.HttpVersion;
response.Body = response.Encoding.GetBytes(html ?? string.Empty);
Respond(response, closeServerConnection);
}
/// <summary>
/// Before request is made to server respond with the specified byte[] to client
/// and ignore the request.
/// </summary>
/// <param name="result">The html content bytes.</param>
/// <param name="headers">The HTTP headers.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void Ok(byte[] result, Dictionary<string, HttpHeader> headers = null,
bool closeServerConnection = false)
{
var response = new OkResponse();
response.Headers.AddHeaders(headers);
response.HttpVersion = WebSession.Request.HttpVersion;
response.Body = result;
Respond(response, closeServerConnection);
}
/// <summary>
/// Before request is made to server 
/// respond with the specified HTML string and the specified status to client.
/// And then ignore the request. 
/// </summary>
/// <param name="html">The html content.</param>
/// <param name="status">The HTTP status code.</param>
/// <param name="headers">The HTTP headers.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void GenericResponse(string html, HttpStatusCode status,
Dictionary<string, HttpHeader> headers = null, bool closeServerConnection = false)
{
var response = new GenericResponse(status);
response.HttpVersion = WebSession.Request.HttpVersion;
response.Headers.AddHeaders(headers);
response.Body = response.Encoding.GetBytes(html ?? string.Empty);
Respond(response, closeServerConnection);
}
/// <summary>
/// Before request is made to server respond with the specified byte[],
/// the specified status to client. And then ignore the request.
/// </summary>
/// <param name="result">The bytes to sent.</param>
/// <param name="status">The HTTP status code.</param>
/// <param name="headers">The HTTP headers.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void GenericResponse(byte[] result, HttpStatusCode status,
Dictionary<string, HttpHeader> headers, bool closeServerConnection = false)
{
var response = new GenericResponse(status);
response.HttpVersion = WebSession.Request.HttpVersion;
response.Headers.AddHeaders(headers);
response.Body = result;
Respond(response, closeServerConnection);
}
/// <summary>
/// Redirect to provided URL.
/// </summary>
/// <param name="url">The URL to redirect.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void Redirect(string url, bool closeServerConnection = false)
{
var response = new RedirectResponse();
response.HttpVersion = WebSession.Request.HttpVersion;
response.Headers.AddHeader(KnownHeaders.Location, url);
response.Body = emptyData;
Respond(response, closeServerConnection);
}
/// <summary>
/// Respond with given response object to client.
/// </summary>
/// <param name="response">The response object.</param>
/// <param name="closeServerConnection">Close the server connection used by request if any?</param>
public void Respond(Response response, bool closeServerConnection = false)
{
//request already send/ready to be sent.
if (WebSession.Request.Locked)
{
//response already received from server and ready to be sent to client.
if (WebSession.Response.Locked)
{
throw new Exception("You cannot call this function after response is sent to the client.");
}
//cleanup original response.
if (closeServerConnection)
{
//no need to cleanup original connection.
//it will be closed any way.
TerminateServerConnection();
}
response.SetOriginalHeaders(WebSession.Response);
//response already received from server but not yet ready to sent to client.
WebSession.Response = response;
WebSession.Response.Locked = true;
}
//request not yet sent/not yet ready to be sent.
else
{
WebSession.Request.Locked = true;
WebSession.Request.CancelRequest = true;
//set new response.
WebSession.Response = response;
WebSession.Response.Locked = true;
}
}
/// <summary>
/// Terminate the connection to server at the end of this HTTP request/response session.
/// </summary>
public void TerminateServerConnection()
{
WebSession.CloseServerConnection = true;
}
/// <summary>
/// Implement any cleanup here
/// </summary>
public override void Dispose()
{
MultipartRequestPartSent = null;
base.Dispose();
}
}
}
using System;
using System.Collections.Generic;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Proxy authorization exception.
/// </summary>
public class ProxyAuthorizationException : ProxyException
{
/// <summary>
/// Initializes a new instance of the <see cref="ProxyAuthorizationException" /> class.
/// </summary>
/// <param name="message">Exception message.</param>
/// <param name="session">The <see cref="SessionEventArgs" /> instance containing the event data.</param>
/// <param name="innerException">Inner exception associated to upstream proxy authorization</param>
/// <param name="headers">Http's headers associated</param>
internal ProxyAuthorizationException(string message, SessionEventArgsBase session, Exception innerException,
IEnumerable<HttpHeader> headers) : base(message, innerException)
{
Session = session;
Headers = headers;
}
/// <summary>
/// The current session within which this error happened.
/// </summary>
public SessionEventArgsBase Session { get; }
/// <summary>
/// Headers associated with the authorization exception.
/// </summary>
public IEnumerable<HttpHeader> Headers { get; }
}
}
using System;
using System.Collections.Generic;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Proxy authorization exception.
/// </summary>
public class ProxyAuthorizationException : ProxyException
{
/// <summary>
/// Initializes a new instance of the <see cref="ProxyAuthorizationException" /> class.
/// </summary>
/// <param name="message">Exception message.</param>
/// <param name="session">The <see cref="SessionEventArgs" /> instance containing the event data.</param>
/// <param name="innerException">Inner exception associated to upstream proxy authorization</param>
/// <param name="headers">Http's headers associated</param>
internal ProxyAuthorizationException(string message, SessionEventArgsBase session, Exception innerException,
IEnumerable<HttpHeader> headers) : base(message, innerException)
{
Session = session;
Headers = headers;
}
/// <summary>
/// The current session within which this error happened.
/// </summary>
public SessionEventArgsBase Session { get; }
/// <summary>
/// Headers associated with the authorization exception.
/// </summary>
public IEnumerable<HttpHeader> Headers { get; }
}
}
using System;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Base class exception associated with this proxy server.
/// </summary>
public abstract class ProxyException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ProxyException" /> class.
/// - must be invoked by derived classes' constructors
/// </summary>
/// <param name="message">Exception message</param>
protected ProxyException(string message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProxyException" /> class.
/// - 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)
{
}
}
}
using System;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Base class exception associated with this proxy server.
/// </summary>
public abstract class ProxyException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ProxyException" /> class.
/// - must be invoked by derived classes' constructors
/// </summary>
/// <param name="message">Exception message</param>
protected ProxyException(string message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProxyException" /> class.
/// - 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)
{
}
}
}
using System;
using Titanium.Web.Proxy.EventArguments;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Proxy HTTP exception.
/// </summary>
public class ProxyHttpException : ProxyException
{
/// <summary>
/// Initializes a new instance of the <see cref="ProxyHttpException" /> class.
/// </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>
internal ProxyHttpException(string message, Exception innerException, SessionEventArgs sessionEventArgs) : base(
message, innerException)
{
SessionEventArgs = sessionEventArgs;
}
/// <summary>
/// Gets session info associated to the exception.
/// </summary>
/// <remarks>
/// This object properties should not be edited.
/// </remarks>
public SessionEventArgs SessionEventArgs { get; }
}
}
using System;
using Titanium.Web.Proxy.EventArguments;
namespace Titanium.Web.Proxy.Exceptions
{
/// <summary>
/// Proxy HTTP exception.
/// </summary>
public class ProxyHttpException : ProxyException
{
/// <summary>
/// Initializes a new instance of the <see cref="ProxyHttpException" /> class.
/// </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>
internal ProxyHttpException(string message, Exception innerException, SessionEventArgs sessionEventArgs) : base(
message, innerException)
{
SessionEventArgs = sessionEventArgs;
}
/// <summary>
/// Gets session info associated to the exception.
/// </summary>
/// <remarks>
/// This object properties should not be edited.
/// </remarks>
public SessionEventArgs SessionEventArgs { get; }
}
}
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace Titanium.Web.Proxy.Helpers
{
internal class NetworkHelper
{
/// <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)
{
if (IPAddress.IsLoopback(address))
{
return true;
}
// get local IP addresses
var localIPs = Dns.GetHostAddresses(Dns.GetHostName());
// test if any host IP equals to any local IP or to localhost
return localIPs.Contains(address);
}
internal static bool IsLocalIpAddress(string hostName)
{
hostName = hostName.ToLower();
if (hostName == "127.0.0.1"
|| hostName == "localhost")
{
return true;
}
var localhostDnsName = Dns.GetHostName().ToLower();
//if hostname matches current machine DNS name
if (hostName == localhostDnsName)
{
return true;
}
var isLocalhost = false;
IPHostEntry hostEntry = null;
//check if parsable to an IP Address
if (IPAddress.TryParse(hostName, out var ipAddress))
{
hostEntry = Dns.GetHostEntry(localhostDnsName);
isLocalhost = hostEntry.AddressList.Any(x => x.Equals(ipAddress));
}
if (!isLocalhost)
{
try
{
hostEntry = Dns.GetHostEntry(hostName);
isLocalhost = hostEntry.AddressList.Any(x => hostEntry.AddressList.Any(x.Equals));
}
catch (SocketException)
{
}
}
return isLocalhost;
}
}
}
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace Titanium.Web.Proxy.Helpers
{
internal class NetworkHelper
{
/// <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)
{
if (IPAddress.IsLoopback(address))
{
return true;
}
// get local IP addresses
var localIPs = Dns.GetHostAddresses(Dns.GetHostName());
// test if any host IP equals to any local IP or to localhost
return localIPs.Contains(address);
}
internal static bool IsLocalIpAddress(string hostName)
{
hostName = hostName.ToLower();
if (hostName == "127.0.0.1"
|| hostName == "localhost")
{
return true;
}
var localhostDnsName = Dns.GetHostName().ToLower();
//if hostname matches current machine DNS name
if (hostName == localhostDnsName)
{
return true;
}
var isLocalhost = false;
IPHostEntry hostEntry = null;
//check if parsable to an IP Address
if (IPAddress.TryParse(hostName, out var ipAddress))
{
hostEntry = Dns.GetHostEntry(localhostDnsName);
isLocalhost = hostEntry.AddressList.Any(x => x.Equals(ipAddress));
}
if (!isLocalhost)
{
try
{
hostEntry = Dns.GetHostEntry(hostName);
isLocalhost = hostEntry.AddressList.Any(x => hostEntry.AddressList.Any(x.Equals));
}
catch (SocketException)
{
}
}
return isLocalhost;
}
}
}
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network.Tcp;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Http
{
/// <summary>
/// Used to communicate with the server over HTTP(S)
/// </summary>
public class HttpWebClient
{
internal HttpWebClient(Request request = null, Response response = null)
{
Request = request ?? new Request();
Response = response ?? new Response();
}
/// <summary>
/// Connection to server
/// </summary>
internal TcpServerConnection ServerConnection { get; set; }
/// <summary>
/// Should we close the server connection at the end of this HTTP request/response session.
/// </summary>
internal bool CloseServerConnection { get; set; }
/// <summary>
/// Stores internal data for the session.
/// </summary>
internal InternalDataStore Data { get; } = new InternalDataStore();
/// <summary>
/// Gets or sets the user data.
/// </summary>
public object UserData { get; set; }
/// <summary>
/// Override UpStreamEndPoint for this request; Local NIC via request is made
/// </summary>
public IPEndPoint UpStreamEndPoint { get; set; }
/// <summary>
/// Headers passed with Connect.
/// </summary>
public ConnectRequest ConnectRequest { get; internal set; }
/// <summary>
/// Web Request.
/// </summary>
public Request Request { get; }
/// <summary>
/// Web Response.
/// </summary>
public Response Response { get; internal 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 => Request.IsHttps;
/// <summary>
/// Set the tcp connection to server used by this webclient
/// </summary>
/// <param name="serverConnection">Instance of <see cref="TcpServerConnection" /></param>
internal void SetConnection(TcpServerConnection serverConnection)
{
serverConnection.LastAccess = DateTime.Now;
ServerConnection = serverConnection;
}
/// <summary>
/// Prepare and send the http(s) request
/// </summary>
/// <returns></returns>
internal async Task SendRequest(bool enable100ContinueBehaviour, bool isTransparent,
CancellationToken cancellationToken)
{
var upstreamProxy = ServerConnection.UpStreamProxy;
bool useUpstreamProxy = upstreamProxy != null && ServerConnection.IsHttps == false;
var writer = ServerConnection.StreamWriter;
// prepare the request & headers
await writer.WriteLineAsync(Request.CreateRequestLine(Request.Method,
useUpstreamProxy || isTransparent ? Request.OriginalUrl : Request.RequestUri.PathAndQuery,
Request.HttpVersion), cancellationToken);
var headerBuilder = new StringBuilder();
// Send Authentication to Upstream proxy if needed
if (!isTransparent && upstreamProxy != null
&& ServerConnection.IsHttps == false
&& !string.IsNullOrEmpty(upstreamProxy.UserName)
&& upstreamProxy.Password != null)
{
headerBuilder.Append($"{HttpHeader.ProxyConnectionKeepAlive}{ProxyConstants.NewLine}");
headerBuilder.Append($"{HttpHeader.GetProxyAuthorizationHeader(upstreamProxy.UserName, upstreamProxy.Password)}{ProxyConstants.NewLine}");
}
// write request headers
foreach (var header in Request.Headers)
{
if (isTransparent || header.Name != KnownHeaders.ProxyAuthorization)
{
headerBuilder.Append($"{header}{ProxyConstants.NewLine}");
}
}
headerBuilder.Append(ProxyConstants.NewLine);
await writer.WriteAsync(headerBuilder.ToString(), cancellationToken);
if (enable100ContinueBehaviour)
{
if (Request.ExpectContinue)
{
string httpStatus;
try
{
httpStatus = await ServerConnection.Stream.ReadLineAsync(cancellationToken);
if (httpStatus == null)
{
throw new ServerConnectionException("Server connection was closed.");
}
}
catch (Exception e) when (!(e is ServerConnectionException))
{
throw new ServerConnectionException("Server connection was closed.");
}
Response.ParseResponseLine(httpStatus, out _, out int responseStatusCode,
out string responseStatusDescription);
// find if server is willing for expect continue
if (responseStatusCode == (int)HttpStatusCode.Continue
&& responseStatusDescription.EqualsIgnoreCase("continue"))
{
Request.Is100Continue = true;
await ServerConnection.Stream.ReadLineAsync(cancellationToken);
}
else if (responseStatusCode == (int)HttpStatusCode.ExpectationFailed
&& responseStatusDescription.EqualsIgnoreCase("expectation failed"))
{
Request.ExpectationFailed = true;
await ServerConnection.Stream.ReadLineAsync(cancellationToken);
}
}
}
}
/// <summary>
/// Receive and parse the http response from server
/// </summary>
/// <returns></returns>
internal async Task ReceiveResponse(CancellationToken cancellationToken)
{
// return if this is already read
if (Response.StatusCode != 0)
{
return;
}
string httpStatus;
try
{
httpStatus = await ServerConnection.Stream.ReadLineAsync(cancellationToken);
if (httpStatus == null)
{
throw new ServerConnectionException("Server connection was closed.");
}
}
catch (Exception e) when (!(e is ServerConnectionException))
{
throw new ServerConnectionException("Server connection was closed.");
}
if (httpStatus == string.Empty)
{
httpStatus = await ServerConnection.Stream.ReadLineAsync(cancellationToken);
}
Response.ParseResponseLine(httpStatus, out var version, out int statusCode, out string statusDescription);
Response.HttpVersion = version;
Response.StatusCode = statusCode;
Response.StatusDescription = statusDescription;
// For HTTP 1.1 comptibility server may send expect-continue even if not asked for it in request
if (Response.StatusCode == (int)HttpStatusCode.Continue
&& Response.StatusDescription.EqualsIgnoreCase("continue"))
{
// Read the next line after 100-continue
Response.Is100Continue = true;
Response.StatusCode = 0;
await ServerConnection.Stream.ReadLineAsync(cancellationToken);
// now receive response
await ReceiveResponse(cancellationToken);
return;
}
if (Response.StatusCode == (int)HttpStatusCode.ExpectationFailed
&& Response.StatusDescription.EqualsIgnoreCase("expectation failed"))
{
// read next line after expectation failed response
Response.ExpectationFailed = true;
Response.StatusCode = 0;
await ServerConnection.Stream.ReadLineAsync(cancellationToken);
// now receive response
await ReceiveResponse(cancellationToken);
return;
}
// Read the response headers in to unique and non-unique header collections
await HeaderParser.ReadHeaders(ServerConnection.Stream, Response.Headers, cancellationToken);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
internal void FinishSession()
{
ServerConnection = null;
ConnectRequest?.FinishSession();
Request?.FinishSession();
Response?.FinishSession();
Data.Clear();
UserData = null;
}
}
}
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network.Tcp;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.Http
{
/// <summary>
/// Used to communicate with the server over HTTP(S)
/// </summary>
public class HttpWebClient
{
internal HttpWebClient(Request request = null, Response response = null)
{
Request = request ?? new Request();
Response = response ?? new Response();
}
/// <summary>
/// Connection to server
/// </summary>
internal TcpServerConnection ServerConnection { get; set; }
/// <summary>
/// Should we close the server connection at the end of this HTTP request/response session.
/// </summary>
internal bool CloseServerConnection { get; set; }
/// <summary>
/// Stores internal data for the session.
/// </summary>
internal InternalDataStore Data { get; } = new InternalDataStore();
/// <summary>
/// Gets or sets the user data.
/// </summary>
public object UserData { get; set; }
/// <summary>
/// Override UpStreamEndPoint for this request; Local NIC via request is made
/// </summary>
public IPEndPoint UpStreamEndPoint { get; set; }
/// <summary>
/// Headers passed with Connect.
/// </summary>
public ConnectRequest ConnectRequest { get; internal set; }
/// <summary>
/// Web Request.
/// </summary>
public Request Request { get; }
/// <summary>
/// Web Response.
/// </summary>
public Response Response { get; internal 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 => Request.IsHttps;
/// <summary>
/// Set the tcp connection to server used by this webclient
/// </summary>
/// <param name="serverConnection">Instance of <see cref="TcpServerConnection" /></param>
internal void SetConnection(TcpServerConnection serverConnection)
{
serverConnection.LastAccess = DateTime.Now;
ServerConnection = serverConnection;
}
/// <summary>
/// Prepare and send the http(s) request
/// </summary>
/// <returns></returns>
internal async Task SendRequest(bool enable100ContinueBehaviour, bool isTransparent,
CancellationToken cancellationToken)
{
var upstreamProxy = ServerConnection.UpStreamProxy;
bool useUpstreamProxy = upstreamProxy != null && ServerConnection.IsHttps == false;
var writer = ServerConnection.StreamWriter;
// prepare the request & headers
await writer.WriteLineAsync(Request.CreateRequestLine(Request.Method,
useUpstreamProxy || isTransparent ? Request.OriginalUrl : Request.RequestUri.PathAndQuery,
Request.HttpVersion), cancellationToken);
var headerBuilder = new StringBuilder();
// Send Authentication to Upstream proxy if needed
if (!isTransparent && upstreamProxy != null
&& ServerConnection.IsHttps == false
&& !string.IsNullOrEmpty(upstreamProxy.UserName)
&& upstreamProxy.Password != null)
{
headerBuilder.Append($"{HttpHeader.ProxyConnectionKeepAlive}{ProxyConstants.NewLine}");
headerBuilder.Append($"{HttpHeader.GetProxyAuthorizationHeader(upstreamProxy.UserName, upstreamProxy.Password)}{ProxyConstants.NewLine}");
}
// write request headers
foreach (var header in Request.Headers)
{
if (isTransparent || header.Name != KnownHeaders.ProxyAuthorization)
{
headerBuilder.Append($"{header}{ProxyConstants.NewLine}");
}
}
headerBuilder.Append(ProxyConstants.NewLine);
await writer.WriteAsync(headerBuilder.ToString(), cancellationToken);
if (enable100ContinueBehaviour)
{
if (Request.ExpectContinue)
{
string httpStatus;
try
{
httpStatus = await ServerConnection.Stream.ReadLineAsync(cancellationToken);
if (httpStatus == null)
{
throw new ServerConnectionException("Server connection was closed.");
}
}
catch (Exception e) when (!(e is ServerConnectionException))
{
throw new ServerConnectionException("Server connection was closed.");
}
Response.ParseResponseLine(httpStatus, out _, out int responseStatusCode,
out string responseStatusDescription);
// find if server is willing for expect continue
if (responseStatusCode == (int)HttpStatusCode.Continue
&& responseStatusDescription.EqualsIgnoreCase("continue"))
{
Request.Is100Continue = true;
await ServerConnection.Stream.ReadLineAsync(cancellationToken);
}
else if (responseStatusCode == (int)HttpStatusCode.ExpectationFailed
&& responseStatusDescription.EqualsIgnoreCase("expectation failed"))
{
Request.ExpectationFailed = true;
await ServerConnection.Stream.ReadLineAsync(cancellationToken);
}
}
}
}
/// <summary>
/// Receive and parse the http response from server
/// </summary>
/// <returns></returns>
internal async Task ReceiveResponse(CancellationToken cancellationToken)
{
// return if this is already read
if (Response.StatusCode != 0)
{
return;
}
string httpStatus;
try
{
httpStatus = await ServerConnection.Stream.ReadLineAsync(cancellationToken);
if (httpStatus == null)
{
throw new ServerConnectionException("Server connection was closed.");
}
}
catch (Exception e) when (!(e is ServerConnectionException))
{
throw new ServerConnectionException("Server connection was closed.");
}
if (httpStatus == string.Empty)
{
httpStatus = await ServerConnection.Stream.ReadLineAsync(cancellationToken);
}
Response.ParseResponseLine(httpStatus, out var version, out int statusCode, out string statusDescription);
Response.HttpVersion = version;
Response.StatusCode = statusCode;
Response.StatusDescription = statusDescription;
// For HTTP 1.1 comptibility server may send expect-continue even if not asked for it in request
if (Response.StatusCode == (int)HttpStatusCode.Continue
&& Response.StatusDescription.EqualsIgnoreCase("continue"))
{
// Read the next line after 100-continue
Response.Is100Continue = true;
Response.StatusCode = 0;
await ServerConnection.Stream.ReadLineAsync(cancellationToken);
// now receive response
await ReceiveResponse(cancellationToken);
return;
}
if (Response.StatusCode == (int)HttpStatusCode.ExpectationFailed
&& Response.StatusDescription.EqualsIgnoreCase("expectation failed"))
{
// read next line after expectation failed response
Response.ExpectationFailed = true;
Response.StatusCode = 0;
await ServerConnection.Stream.ReadLineAsync(cancellationToken);
// now receive response
await ReceiveResponse(cancellationToken);
return;
}
// Read the response headers in to unique and non-unique header collections
await HeaderParser.ReadHeaders(ServerConnection.Stream, Response.Headers, cancellationToken);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
internal void FinishSession()
{
ServerConnection = null;
ConnectRequest?.FinishSession();
Request?.FinishSession();
Response?.FinishSession();
Data.Clear();
UserData = null;
}
}
}
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 password;
private string userName;
/// <summary>
/// Use default windows credentials?
/// </summary>
public bool UseDefaultCredentials { get; set; }
/// <summary>
/// Bypass this proxy for connections to localhost?
/// </summary>
public bool BypassLocalhost { get; set; }
/// <summary>
/// Username.
/// </summary>
public string UserName
{
get => UseDefaultCredentials ? defaultCredentials.Value.UserName : userName;
set
{
userName = value;
if (defaultCredentials.Value.UserName != userName)
{
UseDefaultCredentials = false;
}
}
}
/// <summary>
/// Password.
/// </summary>
public string Password
{
get => UseDefaultCredentials ? defaultCredentials.Value.Password : password;
set
{
password = value;
if (defaultCredentials.Value.Password != password)
{
UseDefaultCredentials = false;
}
}
}
/// <summary>
/// Host name.
/// </summary>
public string HostName { get; set; }
/// <summary>
/// Port.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Get cache key for Tcp connection cahe.
/// </summary>
/// <returns></returns>
internal string GetCacheKey()
{
return $"{HostName}-{Port}" + (UseDefaultCredentials ? $"-{UserName}-{Password}" : string.Empty);
}
/// <summary>
/// returns data in Hostname:port format.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"{HostName}:{Port}";
}
}
}
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 password;
private string userName;
/// <summary>
/// Use default windows credentials?
/// </summary>
public bool UseDefaultCredentials { get; set; }
/// <summary>
/// Bypass this proxy for connections to localhost?
/// </summary>
public bool BypassLocalhost { get; set; }
/// <summary>
/// Username.
/// </summary>
public string UserName
{
get => UseDefaultCredentials ? defaultCredentials.Value.UserName : userName;
set
{
userName = value;
if (defaultCredentials.Value.UserName != userName)
{
UseDefaultCredentials = false;
}
}
}
/// <summary>
/// Password.
/// </summary>
public string Password
{
get => UseDefaultCredentials ? defaultCredentials.Value.Password : password;
set
{
password = value;
if (defaultCredentials.Value.Password != password)
{
UseDefaultCredentials = false;
}
}
}
/// <summary>
/// Host name.
/// </summary>
public string HostName { get; set; }
/// <summary>
/// Port.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Get cache key for Tcp connection cahe.
/// </summary>
/// <returns></returns>
internal string GetCacheKey()
{
return $"{HostName}-{Port}" + (UseDefaultCredentials ? $"-{UserName}-{Password}" : string.Empty);
}
/// <summary>
/// returns data in Hostname:port format.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"{HostName}:{Port}";
}
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Network.Tcp
{
/// <summary>
/// A class that manages Tcp Connection to server used by this proxy server.
/// </summary>
internal class TcpConnectionFactory : IDisposable
{
//Tcp server connection pool cache
private readonly ConcurrentDictionary<string, ConcurrentQueue<TcpServerConnection>> cache
= new ConcurrentDictionary<string, ConcurrentQueue<TcpServerConnection>>();
//Tcp connections waiting to be disposed by cleanup task
private readonly ConcurrentBag<TcpServerConnection> disposalBag =
new ConcurrentBag<TcpServerConnection>();
//cache object race operations lock
private readonly SemaphoreSlim @lock = new SemaphoreSlim(1);
private volatile bool runCleanUpTask = true;
internal TcpConnectionFactory(ProxyServer server)
{
this.server = server;
Task.Run(async () => await clearOutdatedConnections());
}
internal ProxyServer server { get; set; }
internal string GetConnectionCacheKey(string remoteHostName, int remotePort,
bool isHttps, List<SslApplicationProtocol> applicationProtocols,
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy)
{
//http version is ignored since its an application level decision b/w HTTP 1.0/1.1
//also when doing connect request MS Edge browser sends http 1.0 but uses 1.1 after server sends 1.1 its response.
//That can create cache miss for same server connection unneccessarily expecially when prefetcing with Connect.
//http version 2 is separated using applicationProtocols below.
var cacheKeyBuilder = new StringBuilder($"{remoteHostName}-{remotePort}-" +
//when creating Tcp client isConnect won't matter
$"{isHttps}-");
if (applicationProtocols != null)
{
foreach (var protocol in applicationProtocols.OrderBy(x => x))
{
cacheKeyBuilder.Append($"{protocol}-");
}
}
cacheKeyBuilder.Append(upStreamEndPoint != null
? $"{upStreamEndPoint.Address}-{upStreamEndPoint.Port}-"
: string.Empty);
cacheKeyBuilder.Append(externalProxy != null ? $"{externalProxy.GetCacheKey()}-" : string.Empty);
return cacheKeyBuilder.ToString();
}
/// <summary>
/// Gets the connection cache key.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <param name="applicationProtocol"></param>
/// <returns></returns>
internal async Task<string> GetConnectionCacheKey(ProxyServer server, SessionEventArgsBase args,
SslApplicationProtocol applicationProtocol)
{
List<SslApplicationProtocol> applicationProtocols = null;
if (applicationProtocol != default)
{
applicationProtocols = new List<SslApplicationProtocol> { applicationProtocol };
}
ExternalProxy customUpStreamProxy = null;
bool isHttps = args.IsHttps;
if (server.GetCustomUpStreamProxyFunc != null)
{
customUpStreamProxy = await server.GetCustomUpStreamProxyFunc(args);
}
args.CustomUpStreamProxyUsed = customUpStreamProxy;
return GetConnectionCacheKey(
args.WebSession.Request.RequestUri.Host,
args.WebSession.Request.RequestUri.Port,
isHttps, applicationProtocols,
server, args.WebSession.UpStreamEndPoint ?? server.UpStreamEndPoint,
customUpStreamProxy ?? (isHttps ? server.UpStreamHttpsProxy : server.UpStreamHttpProxy));
}
/// <summary>
/// Create a server connection.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="applicationProtocol"></param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
internal Task<TcpServerConnection> GetServerConnection(ProxyServer server, SessionEventArgsBase args, bool isConnect,
SslApplicationProtocol applicationProtocol, bool noCache, CancellationToken cancellationToken)
{
List<SslApplicationProtocol> applicationProtocols = null;
if (applicationProtocol != default)
{
applicationProtocols = new List<SslApplicationProtocol> { applicationProtocol };
}
return GetServerConnection(server, args, isConnect, applicationProtocols, noCache, cancellationToken);
}
/// <summary>
/// Create a server connection.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="applicationProtocols"></param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
internal async Task<TcpServerConnection> GetServerConnection(ProxyServer server, SessionEventArgsBase args, bool isConnect,
List<SslApplicationProtocol> applicationProtocols, bool noCache, CancellationToken cancellationToken)
{
ExternalProxy customUpStreamProxy = null;
bool isHttps = args.IsHttps;
if (server.GetCustomUpStreamProxyFunc != null)
{
customUpStreamProxy = await server.GetCustomUpStreamProxyFunc(args);
}
args.CustomUpStreamProxyUsed = customUpStreamProxy;
return await GetServerConnection(
args.WebSession.Request.RequestUri.Host,
args.WebSession.Request.RequestUri.Port,
args.WebSession.Request.HttpVersion,
isHttps, applicationProtocols, isConnect,
server, args.WebSession.UpStreamEndPoint ?? server.UpStreamEndPoint,
customUpStreamProxy ?? (isHttps ? server.UpStreamHttpsProxy : server.UpStreamHttpProxy),
noCache, cancellationToken);
}
/// <summary>
/// Gets a TCP connection to server from connection pool.
/// </summary>
/// <param name="remoteHostName">The remote hostname.</param>
/// <param name="remotePort">The remote port.</param>
/// <param name="httpVersion">The http version to use.</param>
/// <param name="isHttps">Is this a HTTPS request.</param>
/// <param name="applicationProtocols">The list of HTTPS application level protocol to negotiate if needed.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="proxyServer">The current ProxyServer instance.</param>
/// <param name="upStreamEndPoint">The local upstream endpoint to make request via.</param>
/// <param name="externalProxy">The external proxy to make request via.</param>
/// <param name="noCache">Not from cache/create new connection.</param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
internal async Task<TcpServerConnection> GetServerConnection(string remoteHostName, int remotePort,
Version httpVersion, bool isHttps, List<SslApplicationProtocol> applicationProtocols, bool isConnect,
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy,
bool noCache, CancellationToken cancellationToken)
{
var cacheKey = GetConnectionCacheKey(remoteHostName, remotePort,
isHttps, applicationProtocols,
proxyServer, upStreamEndPoint, externalProxy);
if (proxyServer.EnableConnectionPool && !noCache)
{
if (cache.TryGetValue(cacheKey, out var existingConnections))
{
while (existingConnections.Count > 0)
{
if (existingConnections.TryDequeue(out var recentConnection))
{
//+3 seconds for potential delay after getting connection
var cutOff = DateTime.Now.AddSeconds(-1 * proxyServer.ConnectionTimeOutSeconds + 3);
if (recentConnection.LastAccess > cutOff
&& recentConnection.TcpClient.IsGoodConnection())
{
return recentConnection;
}
disposalBag.Add(recentConnection);
}
}
}
}
var connection = await createServerConnection(remoteHostName, remotePort, httpVersion, isHttps,
applicationProtocols, isConnect, proxyServer, upStreamEndPoint, externalProxy, cancellationToken);
connection.CacheKey = cacheKey;
return connection;
}
/// <summary>
/// Creates a TCP connection to server
/// </summary>
/// <param name="remoteHostName">The remote hostname.</param>
/// <param name="remotePort">The remote port.</param>
/// <param name="httpVersion">The http version to use.</param>
/// <param name="isHttps">Is this a HTTPS request.</param>
/// <param name="applicationProtocols">The list of HTTPS application level protocol to negotiate if needed.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="proxyServer">The current ProxyServer instance.</param>
/// <param name="upStreamEndPoint">The local upstream endpoint to make request via.</param>
/// <param name="externalProxy">The external proxy to make request via.</param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
private async Task<TcpServerConnection> createServerConnection(string remoteHostName, int remotePort,
Version httpVersion, bool isHttps, List<SslApplicationProtocol> applicationProtocols, bool isConnect,
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy,
CancellationToken cancellationToken)
{
//deny connection to proxy end points to avoid infinite connection loop.
if (server.ProxyEndPoints.Any(x => x.Port == remotePort)
&& NetworkHelper.IsLocalIpAddress(remoteHostName))
{
throw new Exception($"A client is making HTTP request to one of the listening ports of this proxy {remoteHostName}:{remotePort}");
}
if (externalProxy != null)
{
if (server.ProxyEndPoints.Any(x => x.Port == externalProxy.Port)
&& NetworkHelper.IsLocalIpAddress(externalProxy.HostName))
{
throw new Exception($"A client is making HTTP request via external proxy to one of the listening ports of this proxy {remoteHostName}:{remotePort}");
}
}
bool useUpstreamProxy = false;
// check if external proxy is set for HTTP/HTTPS
if (externalProxy != null &&
!(externalProxy.HostName == remoteHostName && externalProxy.Port == remotePort))
{
useUpstreamProxy = true;
// check if we need to ByPass
if (externalProxy.BypassLocalhost && NetworkHelper.IsLocalIpAddress(remoteHostName))
{
useUpstreamProxy = false;
}
}
TcpClient tcpClient = null;
CustomBufferedStream stream = null;
SslApplicationProtocol negotiatedApplicationProtocol = default;
try
{
tcpClient = new TcpClient(upStreamEndPoint)
{
ReceiveTimeout = proxyServer.ConnectionTimeOutSeconds * 1000,
SendTimeout = proxyServer.ConnectionTimeOutSeconds * 1000,
SendBufferSize = proxyServer.BufferSize,
ReceiveBufferSize = proxyServer.BufferSize,
LingerState = new LingerOption(true, proxyServer.TcpTimeWaitSeconds)
};
//linux has a bug with socket reuse in .net core.
if (proxyServer.ReuseSocket && RunTime.IsWindows || RunTime.IsRunningOnMono)
{
tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
// If this proxy uses another external proxy then create a tunnel request for HTTP/HTTPS connections
if (useUpstreamProxy)
{
await tcpClient.ConnectAsync(externalProxy.HostName, externalProxy.Port);
}
else
{
await tcpClient.ConnectAsync(remoteHostName, remotePort);
}
await proxyServer.InvokeConnectionCreateEvent(tcpClient, false);
stream = new CustomBufferedStream(tcpClient.GetStream(), proxyServer.BufferPool, proxyServer.BufferSize);
if (useUpstreamProxy && (isConnect || isHttps))
{
var writer = new HttpRequestWriter(stream, proxyServer.BufferPool, proxyServer.BufferSize);
var connectRequest = new ConnectRequest
{
OriginalUrl = $"{remoteHostName}:{remotePort}",
HttpVersion = httpVersion
};
connectRequest.Headers.AddHeader(KnownHeaders.Connection, KnownHeaders.ConnectionKeepAlive);
if (!string.IsNullOrEmpty(externalProxy.UserName) && externalProxy.Password != null)
{
connectRequest.Headers.AddHeader(HttpHeader.ProxyConnectionKeepAlive);
connectRequest.Headers.AddHeader(
HttpHeader.GetProxyAuthorizationHeader(externalProxy.UserName, externalProxy.Password));
}
await writer.WriteRequestAsync(connectRequest, cancellationToken: cancellationToken);
string httpStatus = await stream.ReadLineAsync(cancellationToken);
Response.ParseResponseLine(httpStatus, out _, out int statusCode, out string statusDescription);
if (statusCode != 200 && !statusDescription.EqualsIgnoreCase("OK")
&& !statusDescription.EqualsIgnoreCase("Connection Established"))
{
throw new Exception("Upstream proxy failed to create a secure tunnel");
}
await stream.ReadAndIgnoreAllLinesAsync(cancellationToken);
}
if (isHttps)
{
var sslStream = new SslStream(stream, false, proxyServer.ValidateServerCertificate,
proxyServer.SelectClientCertificate);
stream = new CustomBufferedStream(sslStream, proxyServer.BufferPool, proxyServer.BufferSize);
var options = new SslClientAuthenticationOptions
{
ApplicationProtocols = applicationProtocols,
TargetHost = remoteHostName,
ClientCertificates = null,
EnabledSslProtocols = proxyServer.SupportedSslProtocols,
CertificateRevocationCheckMode = proxyServer.CheckCertificateRevocation
};
await sslStream.AuthenticateAsClientAsync(options, cancellationToken);
#if NETCOREAPP2_1
negotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol;
#endif
}
}
catch (Exception)
{
stream?.Dispose();
tcpClient?.Close();
throw;
}
return new TcpServerConnection(proxyServer, tcpClient)
{
UpStreamProxy = externalProxy,
UpStreamEndPoint = upStreamEndPoint,
HostName = remoteHostName,
Port = remotePort,
IsHttps = isHttps,
NegotiatedApplicationProtocol = negotiatedApplicationProtocol,
UseUpstreamProxy = useUpstreamProxy,
StreamWriter = new HttpRequestWriter(stream, proxyServer.BufferPool, proxyServer.BufferSize),
Stream = stream,
Version = httpVersion
};
}
/// <summary>
/// Release connection back to cache.
/// </summary>
/// <param name="connection">The Tcp server connection to return.</param>
/// <param name="close">Should we just close the connection instead of reusing?</param>
internal async Task Release(TcpServerConnection connection, bool close = false)
{
if (connection == null)
{
return;
}
if (close || connection.IsWinAuthenticated || !server.EnableConnectionPool)
{
disposalBag.Add(connection);
return;
}
connection.LastAccess = DateTime.Now;
try
{
await @lock.WaitAsync();
while (true)
{
if (cache.TryGetValue(connection.CacheKey, out var existingConnections))
{
while (existingConnections.Count >= server.MaxCachedConnections)
{
if (existingConnections.TryDequeue(out var staleConnection))
{
disposalBag.Add(staleConnection);
}
}
existingConnections.Enqueue(connection);
break;
}
if (cache.TryAdd(connection.CacheKey,
new ConcurrentQueue<TcpServerConnection>(new[] { connection })))
{
break;
}
}
}
finally
{
@lock.Release();
}
}
internal async Task Release(Task<TcpServerConnection> connectionCreateTask, bool closeServerConnection)
{
if (connectionCreateTask != null)
{
TcpServerConnection connection = null;
try
{
connection = await connectionCreateTask;
}
catch { }
finally
{
await Release(connection, closeServerConnection);
}
}
}
private async Task clearOutdatedConnections()
{
while (runCleanUpTask)
{
try
{
foreach (var item in cache)
{
var queue = item.Value;
while (queue.Count > 0)
{
if (queue.TryDequeue(out var connection))
{
var cutOff = DateTime.Now.AddSeconds(-1 * server.ConnectionTimeOutSeconds);
if (!server.EnableConnectionPool
|| connection.LastAccess < cutOff)
{
disposalBag.Add(connection);
continue;
}
queue.Enqueue(connection);
break;
}
}
}
try
{
await @lock.WaitAsync();
//clear empty queues
var emptyKeys = cache.Where(x => x.Value.Count == 0).Select(x => x.Key).ToList();
foreach (string key in emptyKeys)
{
cache.TryRemove(key, out var _);
}
}
finally
{
@lock.Release();
}
while (!disposalBag.IsEmpty)
{
if (disposalBag.TryTake(out var connection))
{
connection?.Dispose();
}
}
}
catch (Exception e)
{
server.ExceptionFunc(new Exception("An error occurred when disposing server connections.", e));
}
finally
{
//cleanup every 3 seconds by default
await Task.Delay(1000 * 3);
}
}
}
public void Dispose()
{
runCleanUpTask = false;
try
{
@lock.Wait();
foreach (var queue in cache.Select(x => x.Value).ToList())
{
while (!queue.IsEmpty)
{
if (queue.TryDequeue(out var connection))
{
disposalBag.Add(connection);
}
}
}
cache.Clear();
}
finally
{
@lock.Release();
}
while (!disposalBag.IsEmpty)
{
if (disposalBag.TryTake(out var connection))
{
connection?.Dispose();
}
}
}
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Network.Tcp
{
/// <summary>
/// A class that manages Tcp Connection to server used by this proxy server.
/// </summary>
internal class TcpConnectionFactory : IDisposable
{
//Tcp server connection pool cache
private readonly ConcurrentDictionary<string, ConcurrentQueue<TcpServerConnection>> cache
= new ConcurrentDictionary<string, ConcurrentQueue<TcpServerConnection>>();
//Tcp connections waiting to be disposed by cleanup task
private readonly ConcurrentBag<TcpServerConnection> disposalBag =
new ConcurrentBag<TcpServerConnection>();
//cache object race operations lock
private readonly SemaphoreSlim @lock = new SemaphoreSlim(1);
private volatile bool runCleanUpTask = true;
internal TcpConnectionFactory(ProxyServer server)
{
this.server = server;
Task.Run(async () => await clearOutdatedConnections());
}
internal ProxyServer server { get; set; }
internal string GetConnectionCacheKey(string remoteHostName, int remotePort,
bool isHttps, List<SslApplicationProtocol> applicationProtocols,
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy)
{
//http version is ignored since its an application level decision b/w HTTP 1.0/1.1
//also when doing connect request MS Edge browser sends http 1.0 but uses 1.1 after server sends 1.1 its response.
//That can create cache miss for same server connection unneccessarily expecially when prefetcing with Connect.
//http version 2 is separated using applicationProtocols below.
var cacheKeyBuilder = new StringBuilder($"{remoteHostName}-{remotePort}-" +
//when creating Tcp client isConnect won't matter
$"{isHttps}-");
if (applicationProtocols != null)
{
foreach (var protocol in applicationProtocols.OrderBy(x => x))
{
cacheKeyBuilder.Append($"{protocol}-");
}
}
cacheKeyBuilder.Append(upStreamEndPoint != null
? $"{upStreamEndPoint.Address}-{upStreamEndPoint.Port}-"
: string.Empty);
cacheKeyBuilder.Append(externalProxy != null ? $"{externalProxy.GetCacheKey()}-" : string.Empty);
return cacheKeyBuilder.ToString();
}
/// <summary>
/// Gets the connection cache key.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <param name="applicationProtocol"></param>
/// <returns></returns>
internal async Task<string> GetConnectionCacheKey(ProxyServer server, SessionEventArgsBase args,
SslApplicationProtocol applicationProtocol)
{
List<SslApplicationProtocol> applicationProtocols = null;
if (applicationProtocol != default)
{
applicationProtocols = new List<SslApplicationProtocol> { applicationProtocol };
}
ExternalProxy customUpStreamProxy = null;
bool isHttps = args.IsHttps;
if (server.GetCustomUpStreamProxyFunc != null)
{
customUpStreamProxy = await server.GetCustomUpStreamProxyFunc(args);
}
args.CustomUpStreamProxyUsed = customUpStreamProxy;
return GetConnectionCacheKey(
args.WebSession.Request.RequestUri.Host,
args.WebSession.Request.RequestUri.Port,
isHttps, applicationProtocols,
server, args.WebSession.UpStreamEndPoint ?? server.UpStreamEndPoint,
customUpStreamProxy ?? (isHttps ? server.UpStreamHttpsProxy : server.UpStreamHttpProxy));
}
/// <summary>
/// Create a server connection.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="applicationProtocol"></param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
internal Task<TcpServerConnection> GetServerConnection(ProxyServer server, SessionEventArgsBase args, bool isConnect,
SslApplicationProtocol applicationProtocol, bool noCache, CancellationToken cancellationToken)
{
List<SslApplicationProtocol> applicationProtocols = null;
if (applicationProtocol != default)
{
applicationProtocols = new List<SslApplicationProtocol> { applicationProtocol };
}
return GetServerConnection(server, args, isConnect, applicationProtocols, noCache, cancellationToken);
}
/// <summary>
/// Create a server connection.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="applicationProtocols"></param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
internal async Task<TcpServerConnection> GetServerConnection(ProxyServer server, SessionEventArgsBase args, bool isConnect,
List<SslApplicationProtocol> applicationProtocols, bool noCache, CancellationToken cancellationToken)
{
ExternalProxy customUpStreamProxy = null;
bool isHttps = args.IsHttps;
if (server.GetCustomUpStreamProxyFunc != null)
{
customUpStreamProxy = await server.GetCustomUpStreamProxyFunc(args);
}
args.CustomUpStreamProxyUsed = customUpStreamProxy;
return await GetServerConnection(
args.WebSession.Request.RequestUri.Host,
args.WebSession.Request.RequestUri.Port,
args.WebSession.Request.HttpVersion,
isHttps, applicationProtocols, isConnect,
server, args.WebSession.UpStreamEndPoint ?? server.UpStreamEndPoint,
customUpStreamProxy ?? (isHttps ? server.UpStreamHttpsProxy : server.UpStreamHttpProxy),
noCache, cancellationToken);
}
/// <summary>
/// Gets a TCP connection to server from connection pool.
/// </summary>
/// <param name="remoteHostName">The remote hostname.</param>
/// <param name="remotePort">The remote port.</param>
/// <param name="httpVersion">The http version to use.</param>
/// <param name="isHttps">Is this a HTTPS request.</param>
/// <param name="applicationProtocols">The list of HTTPS application level protocol to negotiate if needed.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="proxyServer">The current ProxyServer instance.</param>
/// <param name="upStreamEndPoint">The local upstream endpoint to make request via.</param>
/// <param name="externalProxy">The external proxy to make request via.</param>
/// <param name="noCache">Not from cache/create new connection.</param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
internal async Task<TcpServerConnection> GetServerConnection(string remoteHostName, int remotePort,
Version httpVersion, bool isHttps, List<SslApplicationProtocol> applicationProtocols, bool isConnect,
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy,
bool noCache, CancellationToken cancellationToken)
{
var cacheKey = GetConnectionCacheKey(remoteHostName, remotePort,
isHttps, applicationProtocols,
proxyServer, upStreamEndPoint, externalProxy);
if (proxyServer.EnableConnectionPool && !noCache)
{
if (cache.TryGetValue(cacheKey, out var existingConnections))
{
while (existingConnections.Count > 0)
{
if (existingConnections.TryDequeue(out var recentConnection))
{
//+3 seconds for potential delay after getting connection
var cutOff = DateTime.Now.AddSeconds(-1 * proxyServer.ConnectionTimeOutSeconds + 3);
if (recentConnection.LastAccess > cutOff
&& recentConnection.TcpClient.IsGoodConnection())
{
return recentConnection;
}
disposalBag.Add(recentConnection);
}
}
}
}
var connection = await createServerConnection(remoteHostName, remotePort, httpVersion, isHttps,
applicationProtocols, isConnect, proxyServer, upStreamEndPoint, externalProxy, cancellationToken);
connection.CacheKey = cacheKey;
return connection;
}
/// <summary>
/// Creates a TCP connection to server
/// </summary>
/// <param name="remoteHostName">The remote hostname.</param>
/// <param name="remotePort">The remote port.</param>
/// <param name="httpVersion">The http version to use.</param>
/// <param name="isHttps">Is this a HTTPS request.</param>
/// <param name="applicationProtocols">The list of HTTPS application level protocol to negotiate if needed.</param>
/// <param name="isConnect">Is this a CONNECT request.</param>
/// <param name="proxyServer">The current ProxyServer instance.</param>
/// <param name="upStreamEndPoint">The local upstream endpoint to make request via.</param>
/// <param name="externalProxy">The external proxy to make request via.</param>
/// <param name="cancellationToken">The cancellation token for this async task.</param>
/// <returns></returns>
private async Task<TcpServerConnection> createServerConnection(string remoteHostName, int remotePort,
Version httpVersion, bool isHttps, List<SslApplicationProtocol> applicationProtocols, bool isConnect,
ProxyServer proxyServer, IPEndPoint upStreamEndPoint, ExternalProxy externalProxy,
CancellationToken cancellationToken)
{
//deny connection to proxy end points to avoid infinite connection loop.
if (server.ProxyEndPoints.Any(x => x.Port == remotePort)
&& NetworkHelper.IsLocalIpAddress(remoteHostName))
{
throw new Exception($"A client is making HTTP request to one of the listening ports of this proxy {remoteHostName}:{remotePort}");
}
if (externalProxy != null)
{
if (server.ProxyEndPoints.Any(x => x.Port == externalProxy.Port)
&& NetworkHelper.IsLocalIpAddress(externalProxy.HostName))
{
throw new Exception($"A client is making HTTP request via external proxy to one of the listening ports of this proxy {remoteHostName}:{remotePort}");
}
}
bool useUpstreamProxy = false;
// check if external proxy is set for HTTP/HTTPS
if (externalProxy != null &&
!(externalProxy.HostName == remoteHostName && externalProxy.Port == remotePort))
{
useUpstreamProxy = true;
// check if we need to ByPass
if (externalProxy.BypassLocalhost && NetworkHelper.IsLocalIpAddress(remoteHostName))
{
useUpstreamProxy = false;
}
}
TcpClient tcpClient = null;
CustomBufferedStream stream = null;
SslApplicationProtocol negotiatedApplicationProtocol = default;
try
{
tcpClient = new TcpClient(upStreamEndPoint)
{
ReceiveTimeout = proxyServer.ConnectionTimeOutSeconds * 1000,
SendTimeout = proxyServer.ConnectionTimeOutSeconds * 1000,
SendBufferSize = proxyServer.BufferSize,
ReceiveBufferSize = proxyServer.BufferSize,
LingerState = new LingerOption(true, proxyServer.TcpTimeWaitSeconds)
};
//linux has a bug with socket reuse in .net core.
if (proxyServer.ReuseSocket && RunTime.IsWindows || RunTime.IsRunningOnMono)
{
tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
// If this proxy uses another external proxy then create a tunnel request for HTTP/HTTPS connections
if (useUpstreamProxy)
{
await tcpClient.ConnectAsync(externalProxy.HostName, externalProxy.Port);
}
else
{
await tcpClient.ConnectAsync(remoteHostName, remotePort);
}
await proxyServer.InvokeConnectionCreateEvent(tcpClient, false);
stream = new CustomBufferedStream(tcpClient.GetStream(), proxyServer.BufferPool, proxyServer.BufferSize);
if (useUpstreamProxy && (isConnect || isHttps))
{
var writer = new HttpRequestWriter(stream, proxyServer.BufferPool, proxyServer.BufferSize);
var connectRequest = new ConnectRequest
{
OriginalUrl = $"{remoteHostName}:{remotePort}",
HttpVersion = httpVersion
};
connectRequest.Headers.AddHeader(KnownHeaders.Connection, KnownHeaders.ConnectionKeepAlive);
if (!string.IsNullOrEmpty(externalProxy.UserName) && externalProxy.Password != null)
{
connectRequest.Headers.AddHeader(HttpHeader.ProxyConnectionKeepAlive);
connectRequest.Headers.AddHeader(
HttpHeader.GetProxyAuthorizationHeader(externalProxy.UserName, externalProxy.Password));
}
await writer.WriteRequestAsync(connectRequest, cancellationToken: cancellationToken);
string httpStatus = await stream.ReadLineAsync(cancellationToken);
Response.ParseResponseLine(httpStatus, out _, out int statusCode, out string statusDescription);
if (statusCode != 200 && !statusDescription.EqualsIgnoreCase("OK")
&& !statusDescription.EqualsIgnoreCase("Connection Established"))
{
throw new Exception("Upstream proxy failed to create a secure tunnel");
}
await stream.ReadAndIgnoreAllLinesAsync(cancellationToken);
}
if (isHttps)
{
var sslStream = new SslStream(stream, false, proxyServer.ValidateServerCertificate,
proxyServer.SelectClientCertificate);
stream = new CustomBufferedStream(sslStream, proxyServer.BufferPool, proxyServer.BufferSize);
var options = new SslClientAuthenticationOptions
{
ApplicationProtocols = applicationProtocols,
TargetHost = remoteHostName,
ClientCertificates = null,
EnabledSslProtocols = proxyServer.SupportedSslProtocols,
CertificateRevocationCheckMode = proxyServer.CheckCertificateRevocation
};
await sslStream.AuthenticateAsClientAsync(options, cancellationToken);
#if NETCOREAPP2_1
negotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol;
#endif
}
}
catch (Exception)
{
stream?.Dispose();
tcpClient?.Close();
throw;
}
return new TcpServerConnection(proxyServer, tcpClient)
{
UpStreamProxy = externalProxy,
UpStreamEndPoint = upStreamEndPoint,
HostName = remoteHostName,
Port = remotePort,
IsHttps = isHttps,
NegotiatedApplicationProtocol = negotiatedApplicationProtocol,
UseUpstreamProxy = useUpstreamProxy,
StreamWriter = new HttpRequestWriter(stream, proxyServer.BufferPool, proxyServer.BufferSize),
Stream = stream,
Version = httpVersion
};
}
/// <summary>
/// Release connection back to cache.
/// </summary>
/// <param name="connection">The Tcp server connection to return.</param>
/// <param name="close">Should we just close the connection instead of reusing?</param>
internal async Task Release(TcpServerConnection connection, bool close = false)
{
if (connection == null)
{
return;
}
if (close || connection.IsWinAuthenticated || !server.EnableConnectionPool)
{
disposalBag.Add(connection);
return;
}
connection.LastAccess = DateTime.Now;
try
{
await @lock.WaitAsync();
while (true)
{
if (cache.TryGetValue(connection.CacheKey, out var existingConnections))
{
while (existingConnections.Count >= server.MaxCachedConnections)
{
if (existingConnections.TryDequeue(out var staleConnection))
{
disposalBag.Add(staleConnection);
}
}
existingConnections.Enqueue(connection);
break;
}
if (cache.TryAdd(connection.CacheKey,
new ConcurrentQueue<TcpServerConnection>(new[] { connection })))
{
break;
}
}
}
finally
{
@lock.Release();
}
}
internal async Task Release(Task<TcpServerConnection> connectionCreateTask, bool closeServerConnection)
{
if (connectionCreateTask != null)
{
TcpServerConnection connection = null;
try
{
connection = await connectionCreateTask;
}
catch { }
finally
{
await Release(connection, closeServerConnection);
}
}
}
private async Task clearOutdatedConnections()
{
while (runCleanUpTask)
{
try
{
foreach (var item in cache)
{
var queue = item.Value;
while (queue.Count > 0)
{
if (queue.TryDequeue(out var connection))
{
var cutOff = DateTime.Now.AddSeconds(-1 * server.ConnectionTimeOutSeconds);
if (!server.EnableConnectionPool
|| connection.LastAccess < cutOff)
{
disposalBag.Add(connection);
continue;
}
queue.Enqueue(connection);
break;
}
}
}
try
{
await @lock.WaitAsync();
//clear empty queues
var emptyKeys = cache.Where(x => x.Value.Count == 0).Select(x => x.Key).ToList();
foreach (string key in emptyKeys)
{
cache.TryRemove(key, out var _);
}
}
finally
{
@lock.Release();
}
while (!disposalBag.IsEmpty)
{
if (disposalBag.TryTake(out var connection))
{
connection?.Dispose();
}
}
}
catch (Exception e)
{
server.ExceptionFunc(new Exception("An error occurred when disposing server connections.", e));
}
finally
{
//cleanup every 3 seconds by default
await Task.Delay(1000 * 3);
}
}
}
public void Dispose()
{
runCleanUpTask = false;
try
{
@lock.Wait();
foreach (var queue in cache.Select(x => x.Value).ToList())
{
while (!queue.IsEmpty)
{
if (queue.TryDequeue(out var connection))
{
disposalBag.Add(connection);
}
}
}
cache.Clear();
}
finally
{
@lock.Release();
}
while (!disposalBag.IsEmpty)
{
if (disposalBag.TryTake(out var connection))
{
connection?.Dispose();
}
}
}
}
}
using System;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy
{
public partial class ProxyServer
{
/// <summary>
/// Callback to authorize clients of this proxy instance.
/// </summary>
/// <param name="session">The session event arguments.</param>
/// <returns>True if authorized.</returns>
private async Task<bool> checkAuthorization(SessionEventArgsBase session)
{
// If we are not authorizing clients return true
if (ProxyBasicAuthenticateFunc == null && ProxySchemeAuthenticateFunc == null)
{
return true;
}
var httpHeaders = session.WebSession.Request.Headers;
try
{
var header = httpHeaders.GetFirstHeader(KnownHeaders.ProxyAuthorization);
if (header == null)
{
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Required");
return false;
}
var headerValueParts = header.Value.Split(ProxyConstants.SpaceSplit);
if (headerValueParts.Length != 2)
{
// Return not authorized
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
return false;
}
if (ProxyBasicAuthenticateFunc != null)
{
return await authenticateUserBasic(session, headerValueParts);
}
if (ProxySchemeAuthenticateFunc != null)
{
var result = await ProxySchemeAuthenticateFunc(session, headerValueParts[0], headerValueParts[1]);
if (result.Result == ProxyAuthenticationResult.ContinuationNeeded)
{
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid", result.Continuation);
return false;
}
return result.Result == ProxyAuthenticationResult.Success;
}
return false;
}
catch (Exception e)
{
ExceptionFunc(new ProxyAuthorizationException("Error whilst authorizing request", session, e,
httpHeaders));
// Return not authorized
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
return false;
}
}
private async Task<bool> authenticateUserBasic(SessionEventArgsBase session, string[] headerValueParts)
{
if (!headerValueParts[0].EqualsIgnoreCase(KnownHeaders.ProxyAuthorizationBasic))
{
// Return not authorized
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
return false;
}
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(headerValueParts[1]));
int colonIndex = decoded.IndexOf(':');
if (colonIndex == -1)
{
// Return not authorized
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
return false;
}
string username = decoded.Substring(0, colonIndex);
string password = decoded.Substring(colonIndex + 1);
bool authenticated = await ProxyBasicAuthenticateFunc(session, username, password);
if (!authenticated)
{
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
}
return authenticated;
}
/// <summary>
/// Create an authentication required response.
/// </summary>
/// <param name="description">Response description.</param>
/// <returns></returns>
private Response createAuthentication407Response(string description, string continuation = null)
{
var response = new Response
{
HttpVersion = HttpHeader.Version11,
StatusCode = (int)HttpStatusCode.ProxyAuthenticationRequired,
StatusDescription = description
};
if (!string.IsNullOrWhiteSpace(continuation))
{
return createContinuationResponse(response, continuation);
}
if (ProxyBasicAuthenticateFunc != null)
{
response.Headers.AddHeader(KnownHeaders.ProxyAuthenticate, $"Basic realm=\"{ProxyAuthenticationRealm}\"");
}
if (ProxySchemeAuthenticateFunc != null)
{
foreach (var scheme in ProxyAuthenticationSchemes)
{
response.Headers.AddHeader(KnownHeaders.ProxyAuthenticate, scheme);
}
}
response.Headers.AddHeader(KnownHeaders.ProxyConnection, KnownHeaders.ProxyConnectionClose);
response.Headers.FixProxyHeaders();
return response;
}
private Response createContinuationResponse(Response response, string continuation)
{
response.Headers.AddHeader(KnownHeaders.ProxyAuthenticate, continuation);
response.Headers.AddHeader(KnownHeaders.ProxyConnection, KnownHeaders.ConnectionKeepAlive);
response.Headers.FixProxyHeaders();
return response;
}
}
}
using System;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy
{
public partial class ProxyServer
{
/// <summary>
/// Callback to authorize clients of this proxy instance.
/// </summary>
/// <param name="session">The session event arguments.</param>
/// <returns>True if authorized.</returns>
private async Task<bool> checkAuthorization(SessionEventArgsBase session)
{
// If we are not authorizing clients return true
if (ProxyBasicAuthenticateFunc == null && ProxySchemeAuthenticateFunc == null)
{
return true;
}
var httpHeaders = session.WebSession.Request.Headers;
try
{
var header = httpHeaders.GetFirstHeader(KnownHeaders.ProxyAuthorization);
if (header == null)
{
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Required");
return false;
}
var headerValueParts = header.Value.Split(ProxyConstants.SpaceSplit);
if (headerValueParts.Length != 2)
{
// Return not authorized
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
return false;
}
if (ProxyBasicAuthenticateFunc != null)
{
return await authenticateUserBasic(session, headerValueParts);
}
if (ProxySchemeAuthenticateFunc != null)
{
var result = await ProxySchemeAuthenticateFunc(session, headerValueParts[0], headerValueParts[1]);
if (result.Result == ProxyAuthenticationResult.ContinuationNeeded)
{
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid", result.Continuation);
return false;
}
return result.Result == ProxyAuthenticationResult.Success;
}
return false;
}
catch (Exception e)
{
ExceptionFunc(new ProxyAuthorizationException("Error whilst authorizing request", session, e,
httpHeaders));
// Return not authorized
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
return false;
}
}
private async Task<bool> authenticateUserBasic(SessionEventArgsBase session, string[] headerValueParts)
{
if (!headerValueParts[0].EqualsIgnoreCase(KnownHeaders.ProxyAuthorizationBasic))
{
// Return not authorized
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
return false;
}
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(headerValueParts[1]));
int colonIndex = decoded.IndexOf(':');
if (colonIndex == -1)
{
// Return not authorized
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
return false;
}
string username = decoded.Substring(0, colonIndex);
string password = decoded.Substring(colonIndex + 1);
bool authenticated = await ProxyBasicAuthenticateFunc(session, username, password);
if (!authenticated)
{
session.WebSession.Response = createAuthentication407Response("Proxy Authentication Invalid");
}
return authenticated;
}
/// <summary>
/// Create an authentication required response.
/// </summary>
/// <param name="description">Response description.</param>
/// <returns></returns>
private Response createAuthentication407Response(string description, string continuation = null)
{
var response = new Response
{
HttpVersion = HttpHeader.Version11,
StatusCode = (int)HttpStatusCode.ProxyAuthenticationRequired,
StatusDescription = description
};
if (!string.IsNullOrWhiteSpace(continuation))
{
return createContinuationResponse(response, continuation);
}
if (ProxyBasicAuthenticateFunc != null)
{
response.Headers.AddHeader(KnownHeaders.ProxyAuthenticate, $"Basic realm=\"{ProxyAuthenticationRealm}\"");
}
if (ProxySchemeAuthenticateFunc != null)
{
foreach (var scheme in ProxyAuthenticationSchemes)
{
response.Headers.AddHeader(KnownHeaders.ProxyAuthenticate, scheme);
}
}
response.Headers.AddHeader(KnownHeaders.ProxyConnection, KnownHeaders.ProxyConnectionClose);
response.Headers.FixProxyHeaders();
return response;
}
private Response createContinuationResponse(Response response, string continuation)
{
response.Headers.AddHeader(KnownHeaders.ProxyAuthenticate, continuation);
response.Headers.AddHeader(KnownHeaders.ProxyConnection, KnownHeaders.ConnectionKeepAlive);
response.Headers.FixProxyHeaders();
return response;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended;
using StreamExtended.Network;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Helpers.WinHttp;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network;
using Titanium.Web.Proxy.Network.Tcp;
namespace Titanium.Web.Proxy
{
/// <inheritdoc />
/// <summary>
/// This class is the backbone of proxy. One can create as many instances as needed.
/// However care should be taken to avoid using the same listening ports across multiple instances.
/// </summary>
public partial class ProxyServer : IDisposable
{
/// <summary>
/// HTTP &amp; HTTPS scheme shorthands.
/// </summary>
internal static readonly string UriSchemeHttp = Uri.UriSchemeHttp;
internal static readonly string UriSchemeHttps = Uri.UriSchemeHttps;
/// <summary>
/// A default exception log func.
/// </summary>
private readonly ExceptionHandler defaultExceptionFunc = e => { };
/// <summary>
/// Backing field for exposed public property.
/// </summary>
private int clientConnectionCount;
/// <summary>
/// Backing field for exposed public property.
/// </summary>
private ExceptionHandler exceptionFunc;
/// <summary>
/// Backing field for exposed public property.
/// </summary>
private int serverConnectionCount;
/// <summary>
/// Upstream proxy manager.
/// </summary>
private WinHttpWebProxyFinder systemProxyResolver;
/// <inheritdoc />
/// <summary>
/// Initializes a new instance of ProxyServer class with provided parameters.
/// </summary>
/// <param name="userTrustRootCertificate">
/// Should fake HTTPS certificate be trusted by this machine's user certificate
/// store?
/// </param>
/// <param name="machineTrustRootCertificate">Should fake HTTPS certificate be trusted by this machine's certificate store?</param>
/// <param name="trustRootCertificateAsAdmin">
/// Should we attempt to trust certificates with elevated permissions by
/// prompting for UAC if required?
/// </param>
public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false,
bool trustRootCertificateAsAdmin = false) : this(null, null, userTrustRootCertificate,
machineTrustRootCertificate, trustRootCertificateAsAdmin)
{
}
/// <summary>
/// Initializes a new instance of ProxyServer class with provided parameters.
/// </summary>
/// <param name="rootCertificateName">Name of the root certificate.</param>
/// <param name="rootCertificateIssuerName">Name of the root certificate issuer.</param>
/// <param name="userTrustRootCertificate">
/// Should fake HTTPS certificate be trusted by this machine's user certificate
/// store?
/// </param>
/// <param name="machineTrustRootCertificate">Should fake HTTPS certificate be trusted by this machine's certificate store?</param>
/// <param name="trustRootCertificateAsAdmin">
/// Should we attempt to trust certificates with elevated permissions by
/// prompting for UAC if required?
/// </param>
public ProxyServer(string rootCertificateName, string rootCertificateIssuerName,
bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false,
bool trustRootCertificateAsAdmin = false)
{
// default values
ConnectionTimeOutSeconds = 60;
if (BufferPool == null)
{
BufferPool = new DefaultBufferPool();
}
ProxyEndPoints = new List<ProxyEndPoint>();
tcpConnectionFactory = new TcpConnectionFactory(this);
if (!RunTime.IsRunningOnMono && RunTime.IsWindows)
{
systemProxySettingsManager = new SystemProxyManager();
}
CertificateManager = new CertificateManager(rootCertificateName, rootCertificateIssuerName,
userTrustRootCertificate, machineTrustRootCertificate, trustRootCertificateAsAdmin, ExceptionFunc);
}
/// <summary>
/// An factory that creates tcp connection to server.
/// </summary>
private TcpConnectionFactory tcpConnectionFactory { get; }
/// <summary>
/// Manage system proxy settings.
/// </summary>
private SystemProxyManager systemProxySettingsManager { get; }
//Number of exception retries when connection pool is enabled.
private int retries => EnableConnectionPool ? MaxCachedConnections : 0;
/// <summary>
/// Is the proxy currently running?
/// </summary>
public bool ProxyRunning { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether requests will be chained to upstream gateway.
/// Defaults to false.
/// </summary>
public bool ForwardToUpstreamGateway { get; set; }
/// <summary>
/// Enable disable Windows Authentication (NTLM/Kerberos).
/// Note: NTLM/Kerberos will always send local credentials of current user
/// running the proxy process. This is because a man
/// in middle attack with Windows domain authentication is not currently supported.
/// Defaults to false.
/// </summary>
public bool EnableWinAuth { get; set; }
/// <summary>
/// Should we check for certificare revocation during SSL authentication to servers
/// Note: If enabled can reduce performance. Defaults to false.
/// </summary>
public X509RevocationMode CheckCertificateRevocation { 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.
/// Defaults to false.
/// </summary>
public bool Enable100ContinueBehaviour { get; set; }
/// <summary>
/// Should we enable experimental server connection pool?
/// Defaults to disable.
/// </summary>
public bool EnableConnectionPool { get; set; }
/// <summary>
/// Buffer size in bytes used throughout this proxy.
/// Default value is 8192 bytes.
/// </summary>
public int BufferSize { get; set; } = 8192;
/// <summary>
/// Seconds client/server connection are to be kept alive when waiting for read/write to complete.
/// This will also determine the pool eviction time when connection pool is enabled.
/// Default value is 60 seconds.
/// </summary>
public int ConnectionTimeOutSeconds { get; set; }
/// <summary>
/// Maximum number of concurrent connections per remote host in cache.
/// Only valid when connection pooling is enabled.
/// Default value is 2.
/// </summary>
public int MaxCachedConnections { get; set; } = 2;
/// <summary>
/// Number of seconds to linger when Tcp connection is in TIME_WAIT state.
/// Default value is 30.
/// </summary>
public int TcpTimeWaitSeconds { get; set; } = 30;
/// <summary>
/// Should we reuse client/server tcp sockets.
/// Default is true (disabled for linux/macOS due to bug in .Net core).
/// </summary>
public bool ReuseSocket { get; set; } = true;
/// <summary>
/// Total number of active client connections.
/// </summary>
public int ClientConnectionCount => clientConnectionCount;
/// <summary>
/// Total number of active server connections.
/// </summary>
public int ServerConnectionCount => serverConnectionCount;
/// <summary>
/// Realm used during Proxy Basic Authentication.
/// </summary>
public string ProxyAuthenticationRealm { get; set; } = "TitaniumProxy";
/// <summary>
/// List of supported Ssl versions.
/// </summary>
public SslProtocols SupportedSslProtocols { get; set; } =
#if NET45
SslProtocols.Ssl3 |
#endif
SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
/// <summary>
/// The buffer pool used throughout this proxy instance.
/// Set custom implementations by implementing this interface.
/// By default this uses DefaultBufferPool implementation available in StreamExtended library package.
/// </summary>
public IBufferPool BufferPool { get; set; }
/// <summary>
/// Manages certificates used by this proxy.
/// </summary>
public CertificateManager CertificateManager { get; }
/// <summary>
/// External proxy used for Http requests.
/// </summary>
public ExternalProxy UpStreamHttpProxy { get; set; }
/// <summary>
/// External proxy used for Https requests.
/// </summary>
public ExternalProxy UpStreamHttpsProxy { get; set; }
/// <summary>
/// Local adapter/NIC endpoint where proxy makes request via.
/// Defaults via any IP addresses of this machine.
/// </summary>
public IPEndPoint UpStreamEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0);
/// <summary>
/// A list of IpAddress and port this proxy is listening to.
/// </summary>
public List<ProxyEndPoint> ProxyEndPoints { get; set; }
/// <summary>
/// A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests.
/// User should return the ExternalProxy object with valid credentials.
/// </summary>
public Func<SessionEventArgsBase, Task<ExternalProxy>> GetCustomUpStreamProxyFunc { get; set; }
/// <summary>
/// Callback for error events in this proxy instance.
/// </summary>
public ExceptionHandler ExceptionFunc
{
get => exceptionFunc ?? defaultExceptionFunc;
set
{
exceptionFunc = value;
CertificateManager.ExceptionFunc = value;
}
}
/// <summary>
/// A callback to authenticate proxy clients via basic authentication.
/// Parameters are username and password as provided by client.
/// Should return true for successful authentication.
/// </summary>
public Func<SessionEventArgsBase, string, string, Task<bool>> ProxyBasicAuthenticateFunc { get; set; }
/// <summary>
/// A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc.
/// Parameters are current working session, schemeType, and token as provided by a calling client.
/// Should return success for successful authentication, continuation if the package requests, or failure.
/// </summary>
public Func<SessionEventArgsBase, string, string, Task<ProxyAuthenticationContext>> ProxySchemeAuthenticateFunc { get; set; }
/// <summary>
/// A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required.
/// Works in relation with ProxySchemeAuthenticateFunc.
/// </summary>
public IEnumerable<string> ProxyAuthenticationSchemes { get; set; } = new string[0];
/// <summary>
/// Event occurs when client connection count changed.
/// </summary>
public event EventHandler ClientConnectionCountChanged;
/// <summary>
/// Event occurs when server connection count changed.
/// </summary>
public event EventHandler ServerConnectionCountChanged;
/// <summary>
/// Event to override the default verification logic of remote SSL certificate received during authentication.
/// </summary>
public event AsyncEventHandler<CertificateValidationEventArgs> ServerCertificateValidationCallback;
/// <summary>
/// Event to override client certificate selection during mutual SSL authentication.
/// </summary>
public event AsyncEventHandler<CertificateSelectionEventArgs> ClientCertificateSelectionCallback;
/// <summary>
/// Intercept request event to server.
/// </summary>
public event AsyncEventHandler<SessionEventArgs> BeforeRequest;
/// <summary>
/// Intercept response event from server.
/// </summary>
public event AsyncEventHandler<SessionEventArgs> BeforeResponse;
/// <summary>
/// Intercept after response event from server.
/// </summary>
public event AsyncEventHandler<SessionEventArgs> AfterResponse;
/// <summary>
/// Customize TcpClient used for client connection upon create.
/// </summary>
public event AsyncEventHandler<TcpClient> OnClientConnectionCreate;
/// <summary>
/// Customize TcpClient used for server connection upon create.
/// </summary>
public event AsyncEventHandler<TcpClient> OnServerConnectionCreate;
/// <summary>
/// Add a proxy end point.
/// </summary>
/// <param name="endPoint">The proxy endpoint.</param>
public void AddEndPoint(ProxyEndPoint endPoint)
{
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");
}
ProxyEndPoints.Add(endPoint);
if (ProxyRunning)
{
listen(endPoint);
}
}
/// <summary>
/// Remove a proxy end point.
/// Will throw error if the end point does'nt exist.
/// </summary>
/// <param name="endPoint">The existing endpoint to remove.</param>
public void RemoveEndPoint(ProxyEndPoint endPoint)
{
if (ProxyEndPoints.Contains(endPoint) == false)
{
throw new Exception("Cannot remove endPoints not added to proxy");
}
ProxyEndPoints.Remove(endPoint);
if (ProxyRunning)
{
quitListen(endPoint);
}
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine.
/// </summary>
/// <param name="endPoint">The explicit endpoint.</param>
public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint)
{
SetAsSystemProxy(endPoint, ProxyProtocolType.Http);
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine.
/// </summary>
/// <param name="endPoint">The explicit endpoint.</param>
public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint)
{
SetAsSystemProxy(endPoint, ProxyProtocolType.Https);
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine.
/// </summary>
/// <param name="endPoint">The explicit endpoint.</param>
/// <param name="protocolType">The proxy protocol type.</param>
public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType)
{
if (RunTime.IsRunningOnMono)
{
throw new Exception("Mono Runtime do not support system proxy settings.");
}
validateEndPointAsSystemProxy(endPoint);
bool isHttp = (protocolType & ProxyProtocolType.Http) > 0;
bool isHttps = (protocolType & ProxyProtocolType.Https) > 0;
if (isHttps)
{
CertificateManager.EnsureRootCertificate();
// If certificate was trusted by the machine
if (!CertificateManager.CertValidated)
{
protocolType = protocolType & ~ProxyProtocolType.Https;
isHttps = false;
}
}
// clear any settings previously added
if (isHttp)
{
ProxyEndPoints.OfType<ExplicitProxyEndPoint>().ToList().ForEach(x => x.IsSystemHttpProxy = false);
}
if (isHttps)
{
ProxyEndPoints.OfType<ExplicitProxyEndPoint>().ToList().ForEach(x => x.IsSystemHttpsProxy = false);
}
systemProxySettingsManager.SetProxy(
Equals(endPoint.IpAddress, IPAddress.Any) |
Equals(endPoint.IpAddress, IPAddress.Loopback)
? "localhost"
: endPoint.IpAddress.ToString(),
endPoint.Port,
protocolType);
if (isHttp)
{
endPoint.IsSystemHttpProxy = true;
}
if (isHttps)
{
endPoint.IsSystemHttpsProxy = true;
}
string proxyType = null;
switch (protocolType)
{
case ProxyProtocolType.Http:
proxyType = "HTTP";
break;
case ProxyProtocolType.Https:
proxyType = "HTTPS";
break;
case ProxyProtocolType.AllHttp:
proxyType = "HTTP and HTTPS";
break;
}
if (protocolType != ProxyProtocolType.None)
{
Console.WriteLine("Set endpoint at Ip {0} and port: {1} as System {2} Proxy", endPoint.IpAddress,
endPoint.Port, proxyType);
}
}
/// <summary>
/// Clear HTTP proxy settings of current machine.
/// </summary>
public void DisableSystemHttpProxy()
{
DisableSystemProxy(ProxyProtocolType.Http);
}
/// <summary>
/// Clear HTTPS proxy settings of current machine.
/// </summary>
public void DisableSystemHttpsProxy()
{
DisableSystemProxy(ProxyProtocolType.Https);
}
/// <summary>
/// Clear the specified proxy setting for current machine.
/// </summary>
public void DisableSystemProxy(ProxyProtocolType protocolType)
{
if (RunTime.IsRunningOnMono)
{
throw new Exception("Mono Runtime do not support system proxy settings.");
}
systemProxySettingsManager.RemoveProxy(protocolType);
}
/// <summary>
/// Clear all proxy settings for current machine.
/// </summary>
public void DisableAllSystemProxies()
{
if (RunTime.IsRunningOnMono)
{
throw new Exception("Mono Runtime do not support system proxy settings.");
}
systemProxySettingsManager.DisableAllProxy();
}
/// <summary>
/// Start this proxy server instance.
/// </summary>
public void Start()
{
if (ProxyRunning)
{
throw new Exception("Proxy is already running.");
}
if (ProxyEndPoints.OfType<ExplicitProxyEndPoint>().Any(x => x.GenericCertificate == null))
{
CertificateManager.EnsureRootCertificate();
}
// clear any system proxy settings which is pointing to our own endpoint (causing a cycle)
// due to ungracious proxy shutdown before or something else
if (systemProxySettingsManager != null && RunTime.IsWindows)
{
var proxyInfo = systemProxySettingsManager.GetProxyInfoFromRegistry();
if (proxyInfo.Proxies != null)
{
var protocolToRemove = ProxyProtocolType.None;
foreach (var proxy in proxyInfo.Proxies.Values)
{
if (NetworkHelper.IsLocalIpAddress(proxy.HostName)
&& ProxyEndPoints.Any(x => x.Port == proxy.Port))
{
protocolToRemove |= proxy.ProtocolType;
}
}
if (protocolToRemove != ProxyProtocolType.None)
{
systemProxySettingsManager.RemoveProxy(protocolToRemove, false);
}
}
}
if (ForwardToUpstreamGateway && GetCustomUpStreamProxyFunc == null && systemProxySettingsManager != null)
{
// Use WinHttp to handle PAC/WAPD scripts.
systemProxyResolver = new WinHttpWebProxyFinder();
systemProxyResolver.LoadFromIE();
GetCustomUpStreamProxyFunc = getSystemUpStreamProxy;
}
ProxyRunning = true;
CertificateManager.ClearIdleCertificates();
foreach (var endPoint in ProxyEndPoints)
{
listen(endPoint);
}
}
/// <summary>
/// Stop this proxy server instance.
/// </summary>
public void Stop()
{
if (!ProxyRunning)
{
throw new Exception("Proxy is not running.");
}
if (!RunTime.IsRunningOnMono && RunTime.IsWindows)
{
bool setAsSystemProxy = ProxyEndPoints.OfType<ExplicitProxyEndPoint>()
.Any(x => x.IsSystemHttpProxy || x.IsSystemHttpsProxy);
if (setAsSystemProxy)
{
systemProxySettingsManager.RestoreOriginalSettings();
}
}
foreach (var endPoint in ProxyEndPoints)
{
quitListen(endPoint);
}
ProxyEndPoints.Clear();
CertificateManager?.StopClearIdleCertificates();
tcpConnectionFactory.Dispose();
ProxyRunning = false;
}
/// <summary>
/// Listen on given end point of local machine.
/// </summary>
/// <param name="endPoint">The end point to listen.</param>
private void listen(ProxyEndPoint endPoint)
{
endPoint.Listener = new TcpListener(endPoint.IpAddress, endPoint.Port);
//linux/macOS has a bug with socket reuse in .net core.
if (ReuseSocket && (RunTime.IsWindows || RunTime.IsRunningOnMono))
{
endPoint.Listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
try
{
endPoint.Listener.Start();
endPoint.Port = ((IPEndPoint)endPoint.Listener.LocalEndpoint).Port;
// accept clients asynchronously
endPoint.Listener.BeginAcceptTcpClient(onAcceptConnection, endPoint);
}
catch (SocketException ex)
{
var pex = new Exception(
$"Endpoint {endPoint} failed to start. Check inner exception and exception data for details.", ex);
pex.Data.Add("ipAddress", endPoint.IpAddress);
pex.Data.Add("port", endPoint.Port);
throw pex;
}
}
/// <summary>
/// Verify if its safe to set this end point as system proxy.
/// </summary>
/// <param name="endPoint">The end point to validate.</param>
private void validateEndPointAsSystemProxy(ExplicitProxyEndPoint endPoint)
{
if (endPoint == null)
{
throw new ArgumentNullException(nameof(endPoint));
}
if (ProxyEndPoints.Contains(endPoint) == false)
{
throw new Exception("Cannot set endPoints not added to proxy as system proxy");
}
if (!ProxyRunning)
{
throw new Exception("Cannot set system proxy settings before proxy has been started.");
}
}
/// <summary>
/// Gets the system up stream proxy.
/// </summary>
/// <param name="sessionEventArgs">The session.</param>
/// <returns>The external proxy as task result.</returns>
private Task<ExternalProxy> getSystemUpStreamProxy(SessionEventArgsBase sessionEventArgs)
{
var proxy = systemProxyResolver.GetProxy(sessionEventArgs.WebSession.Request.RequestUri);
return Task.FromResult(proxy);
}
/// <summary>
/// Act when a connection is received from client.
/// </summary>
private void onAcceptConnection(IAsyncResult asyn)
{
var endPoint = (ProxyEndPoint)asyn.AsyncState;
TcpClient tcpClient = null;
try
{
// based on end point type call appropriate request handlers
tcpClient = endPoint.Listener.EndAcceptTcpClient(asyn);
}
catch (ObjectDisposedException)
{
// The listener was Stop()'d, disposing the underlying socket and
// triggering the completion of the callback. We're already exiting,
// so just return.
return;
}
catch
{
// Other errors are discarded to keep proxy running
}
if (tcpClient != null)
{
Task.Run(async () => { await handleClient(tcpClient, endPoint); });
}
// Get the listener that handles the client request.
endPoint.Listener.BeginAcceptTcpClient(onAcceptConnection, endPoint);
}
/// <summary>
/// Handle the client.
/// </summary>
/// <param name="tcpClient">The client.</param>
/// <param name="endPoint">The proxy endpoint.</param>
/// <returns>The task.</returns>
private async Task handleClient(TcpClient tcpClient, ProxyEndPoint endPoint)
{
tcpClient.ReceiveTimeout = ConnectionTimeOutSeconds * 1000;
tcpClient.SendTimeout = ConnectionTimeOutSeconds * 1000;
tcpClient.SendBufferSize = BufferSize;
tcpClient.ReceiveBufferSize = BufferSize;
tcpClient.LingerState = new LingerOption(true, TcpTimeWaitSeconds);
await InvokeConnectionCreateEvent(tcpClient, true);
using (var clientConnection = new TcpClientConnection(this, tcpClient))
{
if (endPoint is TransparentProxyEndPoint tep)
{
await handleClient(tep, clientConnection);
}
else
{
await handleClient((ExplicitProxyEndPoint)endPoint, clientConnection);
}
}
}
/// <summary>
/// Handle exception.
/// </summary>
/// <param name="clientStream">The client stream.</param>
/// <param name="exception">The exception.</param>
private void onException(CustomBufferedStream clientStream, Exception exception)
{
#if DEBUG
if (clientStream is DebugCustomBufferedStream debugStream)
{
debugStream.LogException(exception);
}
#endif
ExceptionFunc(exception);
}
/// <summary>
/// Quit listening on the given end point.
/// </summary>
private void quitListen(ProxyEndPoint endPoint)
{
endPoint.Listener.Stop();
endPoint.Listener.Server.Dispose();
}
/// <summary>
/// Update client connection count.
/// </summary>
/// <param name="increment">Should we increment/decrement?</param>
internal void UpdateClientConnectionCount(bool increment)
{
if (increment)
{
Interlocked.Increment(ref clientConnectionCount);
}
else
{
Interlocked.Decrement(ref clientConnectionCount);
}
ClientConnectionCountChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Update server connection count.
/// </summary>
/// <param name="increment">Should we increment/decrement?</param>
internal void UpdateServerConnectionCount(bool increment)
{
if (increment)
{
Interlocked.Increment(ref serverConnectionCount);
}
else
{
Interlocked.Decrement(ref serverConnectionCount);
}
ServerConnectionCountChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invoke client/server tcp connection events if subscribed by API user.
/// </summary>
/// <param name="client">The TcpClient object.</param>
/// <param name="isClientConnection">Is this a client connection created event? If not then we would assume that its a server connection create event.</param>
/// <returns></returns>
internal async Task InvokeConnectionCreateEvent(TcpClient client, bool isClientConnection)
{
//client connection created
if (isClientConnection && OnClientConnectionCreate != null)
{
await OnClientConnectionCreate.InvokeAsync(this, client, ExceptionFunc);
}
//server connection created
if (!isClientConnection && OnServerConnectionCreate != null)
{
await OnServerConnectionCreate.InvokeAsync(this, client, ExceptionFunc);
}
}
/// <summary>
/// Connection retry policy when using connection pool.
/// </summary>
private RetryPolicy<T> retryPolicy<T>() where T : Exception
{
return new RetryPolicy<T>(retries, tcpConnectionFactory);
}
/// <summary>
/// Dispose the Proxy instance.
/// </summary>
public void Dispose()
{
if (ProxyRunning)
{
Stop();
}
CertificateManager?.Dispose();
BufferPool?.Dispose();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended;
using StreamExtended.Network;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Helpers.WinHttp;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network;
using Titanium.Web.Proxy.Network.Tcp;
namespace Titanium.Web.Proxy
{
/// <inheritdoc />
/// <summary>
/// This class is the backbone of proxy. One can create as many instances as needed.
/// However care should be taken to avoid using the same listening ports across multiple instances.
/// </summary>
public partial class ProxyServer : IDisposable
{
/// <summary>
/// HTTP &amp; HTTPS scheme shorthands.
/// </summary>
internal static readonly string UriSchemeHttp = Uri.UriSchemeHttp;
internal static readonly string UriSchemeHttps = Uri.UriSchemeHttps;
/// <summary>
/// A default exception log func.
/// </summary>
private readonly ExceptionHandler defaultExceptionFunc = e => { };
/// <summary>
/// Backing field for exposed public property.
/// </summary>
private int clientConnectionCount;
/// <summary>
/// Backing field for exposed public property.
/// </summary>
private ExceptionHandler exceptionFunc;
/// <summary>
/// Backing field for exposed public property.
/// </summary>
private int serverConnectionCount;
/// <summary>
/// Upstream proxy manager.
/// </summary>
private WinHttpWebProxyFinder systemProxyResolver;
/// <inheritdoc />
/// <summary>
/// Initializes a new instance of ProxyServer class with provided parameters.
/// </summary>
/// <param name="userTrustRootCertificate">
/// Should fake HTTPS certificate be trusted by this machine's user certificate
/// store?
/// </param>
/// <param name="machineTrustRootCertificate">Should fake HTTPS certificate be trusted by this machine's certificate store?</param>
/// <param name="trustRootCertificateAsAdmin">
/// Should we attempt to trust certificates with elevated permissions by
/// prompting for UAC if required?
/// </param>
public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false,
bool trustRootCertificateAsAdmin = false) : this(null, null, userTrustRootCertificate,
machineTrustRootCertificate, trustRootCertificateAsAdmin)
{
}
/// <summary>
/// Initializes a new instance of ProxyServer class with provided parameters.
/// </summary>
/// <param name="rootCertificateName">Name of the root certificate.</param>
/// <param name="rootCertificateIssuerName">Name of the root certificate issuer.</param>
/// <param name="userTrustRootCertificate">
/// Should fake HTTPS certificate be trusted by this machine's user certificate
/// store?
/// </param>
/// <param name="machineTrustRootCertificate">Should fake HTTPS certificate be trusted by this machine's certificate store?</param>
/// <param name="trustRootCertificateAsAdmin">
/// Should we attempt to trust certificates with elevated permissions by
/// prompting for UAC if required?
/// </param>
public ProxyServer(string rootCertificateName, string rootCertificateIssuerName,
bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false,
bool trustRootCertificateAsAdmin = false)
{
// default values
ConnectionTimeOutSeconds = 60;
if (BufferPool == null)
{
BufferPool = new DefaultBufferPool();
}
ProxyEndPoints = new List<ProxyEndPoint>();
tcpConnectionFactory = new TcpConnectionFactory(this);
if (!RunTime.IsRunningOnMono && RunTime.IsWindows)
{
systemProxySettingsManager = new SystemProxyManager();
}
CertificateManager = new CertificateManager(rootCertificateName, rootCertificateIssuerName,
userTrustRootCertificate, machineTrustRootCertificate, trustRootCertificateAsAdmin, ExceptionFunc);
}
/// <summary>
/// An factory that creates tcp connection to server.
/// </summary>
private TcpConnectionFactory tcpConnectionFactory { get; }
/// <summary>
/// Manage system proxy settings.
/// </summary>
private SystemProxyManager systemProxySettingsManager { get; }
//Number of exception retries when connection pool is enabled.
private int retries => EnableConnectionPool ? MaxCachedConnections : 0;
/// <summary>
/// Is the proxy currently running?
/// </summary>
public bool ProxyRunning { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether requests will be chained to upstream gateway.
/// Defaults to false.
/// </summary>
public bool ForwardToUpstreamGateway { get; set; }
/// <summary>
/// Enable disable Windows Authentication (NTLM/Kerberos).
/// Note: NTLM/Kerberos will always send local credentials of current user
/// running the proxy process. This is because a man
/// in middle attack with Windows domain authentication is not currently supported.
/// Defaults to false.
/// </summary>
public bool EnableWinAuth { get; set; }
/// <summary>
/// Should we check for certificare revocation during SSL authentication to servers
/// Note: If enabled can reduce performance. Defaults to false.
/// </summary>
public X509RevocationMode CheckCertificateRevocation { 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.
/// Defaults to false.
/// </summary>
public bool Enable100ContinueBehaviour { get; set; }
/// <summary>
/// Should we enable experimental server connection pool?
/// Defaults to disable.
/// </summary>
public bool EnableConnectionPool { get; set; }
/// <summary>
/// Buffer size in bytes used throughout this proxy.
/// Default value is 8192 bytes.
/// </summary>
public int BufferSize { get; set; } = 8192;
/// <summary>
/// Seconds client/server connection are to be kept alive when waiting for read/write to complete.
/// This will also determine the pool eviction time when connection pool is enabled.
/// Default value is 60 seconds.
/// </summary>
public int ConnectionTimeOutSeconds { get; set; }
/// <summary>
/// Maximum number of concurrent connections per remote host in cache.
/// Only valid when connection pooling is enabled.
/// Default value is 2.
/// </summary>
public int MaxCachedConnections { get; set; } = 2;
/// <summary>
/// Number of seconds to linger when Tcp connection is in TIME_WAIT state.
/// Default value is 30.
/// </summary>
public int TcpTimeWaitSeconds { get; set; } = 30;
/// <summary>
/// Should we reuse client/server tcp sockets.
/// Default is true (disabled for linux/macOS due to bug in .Net core).
/// </summary>
public bool ReuseSocket { get; set; } = true;
/// <summary>
/// Total number of active client connections.
/// </summary>
public int ClientConnectionCount => clientConnectionCount;
/// <summary>
/// Total number of active server connections.
/// </summary>
public int ServerConnectionCount => serverConnectionCount;
/// <summary>
/// Realm used during Proxy Basic Authentication.
/// </summary>
public string ProxyAuthenticationRealm { get; set; } = "TitaniumProxy";
/// <summary>
/// List of supported Ssl versions.
/// </summary>
public SslProtocols SupportedSslProtocols { get; set; } =
#if NET45
SslProtocols.Ssl3 |
#endif
SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
/// <summary>
/// The buffer pool used throughout this proxy instance.
/// Set custom implementations by implementing this interface.
/// By default this uses DefaultBufferPool implementation available in StreamExtended library package.
/// </summary>
public IBufferPool BufferPool { get; set; }
/// <summary>
/// Manages certificates used by this proxy.
/// </summary>
public CertificateManager CertificateManager { get; }
/// <summary>
/// External proxy used for Http requests.
/// </summary>
public ExternalProxy UpStreamHttpProxy { get; set; }
/// <summary>
/// External proxy used for Https requests.
/// </summary>
public ExternalProxy UpStreamHttpsProxy { get; set; }
/// <summary>
/// Local adapter/NIC endpoint where proxy makes request via.
/// Defaults via any IP addresses of this machine.
/// </summary>
public IPEndPoint UpStreamEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0);
/// <summary>
/// A list of IpAddress and port this proxy is listening to.
/// </summary>
public List<ProxyEndPoint> ProxyEndPoints { get; set; }
/// <summary>
/// A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests.
/// User should return the ExternalProxy object with valid credentials.
/// </summary>
public Func<SessionEventArgsBase, Task<ExternalProxy>> GetCustomUpStreamProxyFunc { get; set; }
/// <summary>
/// Callback for error events in this proxy instance.
/// </summary>
public ExceptionHandler ExceptionFunc
{
get => exceptionFunc ?? defaultExceptionFunc;
set
{
exceptionFunc = value;
CertificateManager.ExceptionFunc = value;
}
}
/// <summary>
/// A callback to authenticate proxy clients via basic authentication.
/// Parameters are username and password as provided by client.
/// Should return true for successful authentication.
/// </summary>
public Func<SessionEventArgsBase, string, string, Task<bool>> ProxyBasicAuthenticateFunc { get; set; }
/// <summary>
/// A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc.
/// Parameters are current working session, schemeType, and token as provided by a calling client.
/// Should return success for successful authentication, continuation if the package requests, or failure.
/// </summary>
public Func<SessionEventArgsBase, string, string, Task<ProxyAuthenticationContext>> ProxySchemeAuthenticateFunc { get; set; }
/// <summary>
/// A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required.
/// Works in relation with ProxySchemeAuthenticateFunc.
/// </summary>
public IEnumerable<string> ProxyAuthenticationSchemes { get; set; } = new string[0];
/// <summary>
/// Event occurs when client connection count changed.
/// </summary>
public event EventHandler ClientConnectionCountChanged;
/// <summary>
/// Event occurs when server connection count changed.
/// </summary>
public event EventHandler ServerConnectionCountChanged;
/// <summary>
/// Event to override the default verification logic of remote SSL certificate received during authentication.
/// </summary>
public event AsyncEventHandler<CertificateValidationEventArgs> ServerCertificateValidationCallback;
/// <summary>
/// Event to override client certificate selection during mutual SSL authentication.
/// </summary>
public event AsyncEventHandler<CertificateSelectionEventArgs> ClientCertificateSelectionCallback;
/// <summary>
/// Intercept request event to server.
/// </summary>
public event AsyncEventHandler<SessionEventArgs> BeforeRequest;
/// <summary>
/// Intercept response event from server.
/// </summary>
public event AsyncEventHandler<SessionEventArgs> BeforeResponse;
/// <summary>
/// Intercept after response event from server.
/// </summary>
public event AsyncEventHandler<SessionEventArgs> AfterResponse;
/// <summary>
/// Customize TcpClient used for client connection upon create.
/// </summary>
public event AsyncEventHandler<TcpClient> OnClientConnectionCreate;
/// <summary>
/// Customize TcpClient used for server connection upon create.
/// </summary>
public event AsyncEventHandler<TcpClient> OnServerConnectionCreate;
/// <summary>
/// Add a proxy end point.
/// </summary>
/// <param name="endPoint">The proxy endpoint.</param>
public void AddEndPoint(ProxyEndPoint endPoint)
{
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");
}
ProxyEndPoints.Add(endPoint);
if (ProxyRunning)
{
listen(endPoint);
}
}
/// <summary>
/// Remove a proxy end point.
/// Will throw error if the end point does'nt exist.
/// </summary>
/// <param name="endPoint">The existing endpoint to remove.</param>
public void RemoveEndPoint(ProxyEndPoint endPoint)
{
if (ProxyEndPoints.Contains(endPoint) == false)
{
throw new Exception("Cannot remove endPoints not added to proxy");
}
ProxyEndPoints.Remove(endPoint);
if (ProxyRunning)
{
quitListen(endPoint);
}
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine.
/// </summary>
/// <param name="endPoint">The explicit endpoint.</param>
public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint)
{
SetAsSystemProxy(endPoint, ProxyProtocolType.Http);
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine.
/// </summary>
/// <param name="endPoint">The explicit endpoint.</param>
public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint)
{
SetAsSystemProxy(endPoint, ProxyProtocolType.Https);
}
/// <summary>
/// Set the given explicit end point as the default proxy server for current machine.
/// </summary>
/// <param name="endPoint">The explicit endpoint.</param>
/// <param name="protocolType">The proxy protocol type.</param>
public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType)
{
if (RunTime.IsRunningOnMono)
{
throw new Exception("Mono Runtime do not support system proxy settings.");
}
validateEndPointAsSystemProxy(endPoint);
bool isHttp = (protocolType & ProxyProtocolType.Http) > 0;
bool isHttps = (protocolType & ProxyProtocolType.Https) > 0;
if (isHttps)
{
CertificateManager.EnsureRootCertificate();
// If certificate was trusted by the machine
if (!CertificateManager.CertValidated)
{
protocolType = protocolType & ~ProxyProtocolType.Https;
isHttps = false;
}
}
// clear any settings previously added
if (isHttp)
{
ProxyEndPoints.OfType<ExplicitProxyEndPoint>().ToList().ForEach(x => x.IsSystemHttpProxy = false);
}
if (isHttps)
{
ProxyEndPoints.OfType<ExplicitProxyEndPoint>().ToList().ForEach(x => x.IsSystemHttpsProxy = false);
}
systemProxySettingsManager.SetProxy(
Equals(endPoint.IpAddress, IPAddress.Any) |
Equals(endPoint.IpAddress, IPAddress.Loopback)
? "localhost"
: endPoint.IpAddress.ToString(),
endPoint.Port,
protocolType);
if (isHttp)
{
endPoint.IsSystemHttpProxy = true;
}
if (isHttps)
{
endPoint.IsSystemHttpsProxy = true;
}
string proxyType = null;
switch (protocolType)
{
case ProxyProtocolType.Http:
proxyType = "HTTP";
break;
case ProxyProtocolType.Https:
proxyType = "HTTPS";
break;
case ProxyProtocolType.AllHttp:
proxyType = "HTTP and HTTPS";
break;
}
if (protocolType != ProxyProtocolType.None)
{
Console.WriteLine("Set endpoint at Ip {0} and port: {1} as System {2} Proxy", endPoint.IpAddress,
endPoint.Port, proxyType);
}
}
/// <summary>
/// Clear HTTP proxy settings of current machine.
/// </summary>
public void DisableSystemHttpProxy()
{
DisableSystemProxy(ProxyProtocolType.Http);
}
/// <summary>
/// Clear HTTPS proxy settings of current machine.
/// </summary>
public void DisableSystemHttpsProxy()
{
DisableSystemProxy(ProxyProtocolType.Https);
}
/// <summary>
/// Clear the specified proxy setting for current machine.
/// </summary>
public void DisableSystemProxy(ProxyProtocolType protocolType)
{
if (RunTime.IsRunningOnMono)
{
throw new Exception("Mono Runtime do not support system proxy settings.");
}
systemProxySettingsManager.RemoveProxy(protocolType);
}
/// <summary>
/// Clear all proxy settings for current machine.
/// </summary>
public void DisableAllSystemProxies()
{
if (RunTime.IsRunningOnMono)
{
throw new Exception("Mono Runtime do not support system proxy settings.");
}
systemProxySettingsManager.DisableAllProxy();
}
/// <summary>
/// Start this proxy server instance.
/// </summary>
public void Start()
{
if (ProxyRunning)
{
throw new Exception("Proxy is already running.");
}
if (ProxyEndPoints.OfType<ExplicitProxyEndPoint>().Any(x => x.GenericCertificate == null))
{
CertificateManager.EnsureRootCertificate();
}
// clear any system proxy settings which is pointing to our own endpoint (causing a cycle)
// due to ungracious proxy shutdown before or something else
if (systemProxySettingsManager != null && RunTime.IsWindows)
{
var proxyInfo = systemProxySettingsManager.GetProxyInfoFromRegistry();
if (proxyInfo.Proxies != null)
{
var protocolToRemove = ProxyProtocolType.None;
foreach (var proxy in proxyInfo.Proxies.Values)
{
if (NetworkHelper.IsLocalIpAddress(proxy.HostName)
&& ProxyEndPoints.Any(x => x.Port == proxy.Port))
{
protocolToRemove |= proxy.ProtocolType;
}
}
if (protocolToRemove != ProxyProtocolType.None)
{
systemProxySettingsManager.RemoveProxy(protocolToRemove, false);
}
}
}
if (ForwardToUpstreamGateway && GetCustomUpStreamProxyFunc == null && systemProxySettingsManager != null)
{
// Use WinHttp to handle PAC/WAPD scripts.
systemProxyResolver = new WinHttpWebProxyFinder();
systemProxyResolver.LoadFromIE();
GetCustomUpStreamProxyFunc = getSystemUpStreamProxy;
}
ProxyRunning = true;
CertificateManager.ClearIdleCertificates();
foreach (var endPoint in ProxyEndPoints)
{
listen(endPoint);
}
}
/// <summary>
/// Stop this proxy server instance.
/// </summary>
public void Stop()
{
if (!ProxyRunning)
{
throw new Exception("Proxy is not running.");
}
if (!RunTime.IsRunningOnMono && RunTime.IsWindows)
{
bool setAsSystemProxy = ProxyEndPoints.OfType<ExplicitProxyEndPoint>()
.Any(x => x.IsSystemHttpProxy || x.IsSystemHttpsProxy);
if (setAsSystemProxy)
{
systemProxySettingsManager.RestoreOriginalSettings();
}
}
foreach (var endPoint in ProxyEndPoints)
{
quitListen(endPoint);
}
ProxyEndPoints.Clear();
CertificateManager?.StopClearIdleCertificates();
tcpConnectionFactory.Dispose();
ProxyRunning = false;
}
/// <summary>
/// Listen on given end point of local machine.
/// </summary>
/// <param name="endPoint">The end point to listen.</param>
private void listen(ProxyEndPoint endPoint)
{
endPoint.Listener = new TcpListener(endPoint.IpAddress, endPoint.Port);
//linux/macOS has a bug with socket reuse in .net core.
if (ReuseSocket && (RunTime.IsWindows || RunTime.IsRunningOnMono))
{
endPoint.Listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
try
{
endPoint.Listener.Start();
endPoint.Port = ((IPEndPoint)endPoint.Listener.LocalEndpoint).Port;
// accept clients asynchronously
endPoint.Listener.BeginAcceptTcpClient(onAcceptConnection, endPoint);
}
catch (SocketException ex)
{
var pex = new Exception(
$"Endpoint {endPoint} failed to start. Check inner exception and exception data for details.", ex);
pex.Data.Add("ipAddress", endPoint.IpAddress);
pex.Data.Add("port", endPoint.Port);
throw pex;
}
}
/// <summary>
/// Verify if its safe to set this end point as system proxy.
/// </summary>
/// <param name="endPoint">The end point to validate.</param>
private void validateEndPointAsSystemProxy(ExplicitProxyEndPoint endPoint)
{
if (endPoint == null)
{
throw new ArgumentNullException(nameof(endPoint));
}
if (ProxyEndPoints.Contains(endPoint) == false)
{
throw new Exception("Cannot set endPoints not added to proxy as system proxy");
}
if (!ProxyRunning)
{
throw new Exception("Cannot set system proxy settings before proxy has been started.");
}
}
/// <summary>
/// Gets the system up stream proxy.
/// </summary>
/// <param name="sessionEventArgs">The session.</param>
/// <returns>The external proxy as task result.</returns>
private Task<ExternalProxy> getSystemUpStreamProxy(SessionEventArgsBase sessionEventArgs)
{
var proxy = systemProxyResolver.GetProxy(sessionEventArgs.WebSession.Request.RequestUri);
return Task.FromResult(proxy);
}
/// <summary>
/// Act when a connection is received from client.
/// </summary>
private void onAcceptConnection(IAsyncResult asyn)
{
var endPoint = (ProxyEndPoint)asyn.AsyncState;
TcpClient tcpClient = null;
try
{
// based on end point type call appropriate request handlers
tcpClient = endPoint.Listener.EndAcceptTcpClient(asyn);
}
catch (ObjectDisposedException)
{
// The listener was Stop()'d, disposing the underlying socket and
// triggering the completion of the callback. We're already exiting,
// so just return.
return;
}
catch
{
// Other errors are discarded to keep proxy running
}
if (tcpClient != null)
{
Task.Run(async () => { await handleClient(tcpClient, endPoint); });
}
// Get the listener that handles the client request.
endPoint.Listener.BeginAcceptTcpClient(onAcceptConnection, endPoint);
}
/// <summary>
/// Handle the client.
/// </summary>
/// <param name="tcpClient">The client.</param>
/// <param name="endPoint">The proxy endpoint.</param>
/// <returns>The task.</returns>
private async Task handleClient(TcpClient tcpClient, ProxyEndPoint endPoint)
{
tcpClient.ReceiveTimeout = ConnectionTimeOutSeconds * 1000;
tcpClient.SendTimeout = ConnectionTimeOutSeconds * 1000;
tcpClient.SendBufferSize = BufferSize;
tcpClient.ReceiveBufferSize = BufferSize;
tcpClient.LingerState = new LingerOption(true, TcpTimeWaitSeconds);
await InvokeConnectionCreateEvent(tcpClient, true);
using (var clientConnection = new TcpClientConnection(this, tcpClient))
{
if (endPoint is TransparentProxyEndPoint tep)
{
await handleClient(tep, clientConnection);
}
else
{
await handleClient((ExplicitProxyEndPoint)endPoint, clientConnection);
}
}
}
/// <summary>
/// Handle exception.
/// </summary>
/// <param name="clientStream">The client stream.</param>
/// <param name="exception">The exception.</param>
private void onException(CustomBufferedStream clientStream, Exception exception)
{
#if DEBUG
if (clientStream is DebugCustomBufferedStream debugStream)
{
debugStream.LogException(exception);
}
#endif
ExceptionFunc(exception);
}
/// <summary>
/// Quit listening on the given end point.
/// </summary>
private void quitListen(ProxyEndPoint endPoint)
{
endPoint.Listener.Stop();
endPoint.Listener.Server.Dispose();
}
/// <summary>
/// Update client connection count.
/// </summary>
/// <param name="increment">Should we increment/decrement?</param>
internal void UpdateClientConnectionCount(bool increment)
{
if (increment)
{
Interlocked.Increment(ref clientConnectionCount);
}
else
{
Interlocked.Decrement(ref clientConnectionCount);
}
ClientConnectionCountChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Update server connection count.
/// </summary>
/// <param name="increment">Should we increment/decrement?</param>
internal void UpdateServerConnectionCount(bool increment)
{
if (increment)
{
Interlocked.Increment(ref serverConnectionCount);
}
else
{
Interlocked.Decrement(ref serverConnectionCount);
}
ServerConnectionCountChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invoke client/server tcp connection events if subscribed by API user.
/// </summary>
/// <param name="client">The TcpClient object.</param>
/// <param name="isClientConnection">Is this a client connection created event? If not then we would assume that its a server connection create event.</param>
/// <returns></returns>
internal async Task InvokeConnectionCreateEvent(TcpClient client, bool isClientConnection)
{
//client connection created
if (isClientConnection && OnClientConnectionCreate != null)
{
await OnClientConnectionCreate.InvokeAsync(this, client, ExceptionFunc);
}
//server connection created
if (!isClientConnection && OnServerConnectionCreate != null)
{
await OnServerConnectionCreate.InvokeAsync(this, client, ExceptionFunc);
}
}
/// <summary>
/// Connection retry policy when using connection pool.
/// </summary>
private RetryPolicy<T> retryPolicy<T>() where T : Exception
{
return new RetryPolicy<T>(retries, tcpConnectionFactory);
}
/// <summary>
/// Dispose the Proxy instance.
/// </summary>
public void Dispose()
{
if (ProxyRunning)
{
Stop();
}
CertificateManager?.Dispose();
BufferPool?.Dispose();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
#if NETCOREAPP2_1
using System.Net.Security;
#endif
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network.Tcp;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy
{
/// <summary>
/// Handle the request
/// </summary>
public partial class ProxyServer
{
private bool isWindowsAuthenticationEnabledAndSupported =>
EnableWinAuth && RunTime.IsWindows && !RunTime.IsRunningOnMono;
/// <summary>
/// This is the core request handler method for a particular connection from client.
/// Will create new session (request/response) sequence until
/// client/server abruptly terminates connection or by normal HTTP termination.
/// </summary>
/// <param name="endPoint">The proxy endpoint.</param>
/// <param name="clientConnection">The client connection.</param>
/// <param name="clientStream">The client stream.</param>
/// <param name="clientStreamWriter">The client stream writer.</param>
/// <param name="cancellationTokenSource">The cancellation token source for this async task.</param>
/// <param name="httpsConnectHostname">
/// The https hostname as appeared in CONNECT request if this is a HTTPS request from
/// explicit endpoint.
/// </param>
/// <param name="connectRequest">The Connect request if this is a HTTPS request from explicit endpoint.</param>
/// <param name="prefetchConnectionTask">Prefetched server connection for current client using Connect/SNI headers.</param>
private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientConnection clientConnection,
CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter,
CancellationTokenSource cancellationTokenSource, string httpsConnectHostname, ConnectRequest connectRequest,
Task<TcpServerConnection> prefetchConnectionTask = null)
{
var prefetchTask = prefetchConnectionTask;
TcpServerConnection connection = null;
bool closeServerConnection = false;
try
{
var cancellationToken = cancellationTokenSource.Token;
// Loop through each subsequest request on this particular client connection
// (assuming HTTP connection is kept alive by client)
while (true)
{
// read the request line
string httpCmd = await clientStream.ReadLineAsync(cancellationToken);
if (string.IsNullOrEmpty(httpCmd))
{
return;
}
var args = new SessionEventArgs(this, endPoint, cancellationTokenSource)
{
ProxyClient = { ClientConnection = clientConnection },
WebSession = { ConnectRequest = connectRequest }
};
try
{
try
{
Request.ParseRequestLine(httpCmd, out string httpMethod, out string httpUrl,
out var version);
// Read the request headers in to unique and non-unique header collections
await HeaderParser.ReadHeaders(clientStream, args.WebSession.Request.Headers,
cancellationToken);
Uri httpRemoteUri;
if (ProxyConstants.UriSchemeRegex.IsMatch(httpUrl))
{
try
{
httpRemoteUri = new Uri(httpUrl);
}
catch (Exception ex)
{
throw new Exception($"Invalid URI: '{httpUrl}'", ex);
}
}
else
{
string host = args.WebSession.Request.Host ?? httpsConnectHostname;
string hostAndPath = host;
if (httpUrl.StartsWith("/"))
{
hostAndPath += httpUrl;
}
string url = string.Concat(httpsConnectHostname == null ? "http://" : "https://",
hostAndPath);
try
{
httpRemoteUri = new Uri(url);
}
catch (Exception ex)
{
throw new Exception($"Invalid URI: '{url}'", ex);
}
}
var request = args.WebSession.Request;
request.RequestUri = httpRemoteUri;
request.OriginalUrl = httpUrl;
request.Method = httpMethod;
request.HttpVersion = version;
args.ProxyClient.ClientStream = clientStream;
args.ProxyClient.ClientStreamWriter = clientStreamWriter;
if (!args.IsTransparent)
{
// proxy authorization check
if (httpsConnectHostname == null && await checkAuthorization(args) == false)
{
await invokeBeforeResponse(args);
// send the response
await clientStreamWriter.WriteResponseAsync(args.WebSession.Response,
cancellationToken: cancellationToken);
return;
}
prepareRequestHeaders(request.Headers);
request.Host = request.RequestUri.Authority;
}
// if win auth is enabled
// we need a cache of request body
// so that we can send it after authentication in WinAuthHandler.cs
if (isWindowsAuthenticationEnabledAndSupported && request.HasBody)
{
await args.GetRequestBody(cancellationToken);
}
//we need this to syphon out data from connection if API user changes them.
request.SetOriginalHeaders();
args.TimeLine["Request Received"] = DateTime.Now;
// If user requested interception do it
await invokeBeforeRequest(args);
var response = args.WebSession.Response;
if (request.CancelRequest)
{
// syphon out the request body from client before setting the new body
await args.SyphonOutBodyAsync(true, cancellationToken);
await handleHttpSessionResponse(args);
if (!response.KeepAlive)
{
return;
}
continue;
}
//If prefetch task is available.
if (connection == null && prefetchTask != null)
{
connection = await prefetchTask;
prefetchTask = null;
}
// create a new connection if cache key changes.
// only gets hit when connection pool is disabled.
// or when prefetch task has a unexpectedly different connection.
if (connection != null
&& (await tcpConnectionFactory.GetConnectionCacheKey(this, args,
clientConnection.NegotiatedApplicationProtocol)
!= connection.CacheKey))
{
await tcpConnectionFactory.Release(connection);
connection = null;
}
//a connection generator task with captured parameters via closure.
Func<Task<TcpServerConnection>> generator = () =>
tcpConnectionFactory.GetServerConnection(this, args, isConnect: false,
applicationProtocol:clientConnection.NegotiatedApplicationProtocol,
noCache: false, cancellationToken: cancellationToken);
//for connection pool, retry fails until cache is exhausted.
var result = await retryPolicy<ServerConnectionException>().ExecuteAsync(async (serverConnection) =>
{
args.TimeLine["Server Connection Created"] = DateTime.Now;
// if upgrading to websocket then relay the request without reading the contents
if (request.UpgradeToWebSocket)
{
await handleWebSocketUpgrade(httpCmd, args, request,
response, clientStream, clientStreamWriter,
serverConnection, cancellationTokenSource, cancellationToken);
closeServerConnection = true;
return false;
}
// construct the web request that we are going to issue on behalf of the client.
await handleHttpSessionRequest(serverConnection, args);
return true;
}, generator, connection);
//update connection to latest used
connection = result.LatestConnection;
//throw if exception happened
if(!result.IsSuccess)
{
throw result.Exception;
}
if(!result.Continue)
{
return;
}
//user requested
if (args.WebSession.CloseServerConnection)
{
closeServerConnection = true;
return;
}
// if connection is closing exit
if (!response.KeepAlive)
{
closeServerConnection = true;
return;
}
if (cancellationTokenSource.IsCancellationRequested)
{
throw new Exception("Session was terminated by user.");
}
//Get/release server connection for each HTTP session instead of per client connection.
//This will be more efficient especially when client is idly holding server connection
//between sessions without using it.
//Do not release authenticated connections for performance reasons.
//Otherwise it will keep authenticating per session.
if (EnableConnectionPool && connection!=null
&& !connection.IsWinAuthenticated)
{
await tcpConnectionFactory.Release(connection);
connection = null;
}
}
catch (Exception e) when (!(e is ProxyHttpException))
{
throw new ProxyHttpException("Error occured whilst handling session request", e, args);
}
}
catch (Exception e)
{
args.Exception = e;
closeServerConnection = true;
throw;
}
finally
{
await invokeAfterResponse(args);
args.Dispose();
}
}
}
finally
{
await tcpConnectionFactory.Release(connection,
closeServerConnection);
await tcpConnectionFactory.Release(prefetchTask, closeServerConnection);
}
}
/// <summary>
/// Handle a specific session (request/response sequence)
/// </summary>
/// <param name="serverConnection">The tcp connection.</param>
/// <param name="args">The session event arguments.</param>
/// <returns></returns>
private async Task handleHttpSessionRequest(TcpServerConnection serverConnection, SessionEventArgs args)
{
var cancellationToken = args.CancellationTokenSource.Token;
var request = args.WebSession.Request;
request.Locked = true;
var body = request.CompressBodyAndUpdateContentLength();
// if expect continue is enabled then send the headers first
// and see if server would return 100 conitinue
if (request.ExpectContinue)
{
args.WebSession.SetConnection(serverConnection);
await args.WebSession.SendRequest(Enable100ContinueBehaviour, args.IsTransparent,
cancellationToken);
}
// If 100 continue was the response inform that to the client
if (Enable100ContinueBehaviour)
{
var clientStreamWriter = args.ProxyClient.ClientStreamWriter;
if (request.Is100Continue)
{
await clientStreamWriter.WriteResponseStatusAsync(args.WebSession.Response.HttpVersion,
(int)HttpStatusCode.Continue, "Continue", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
else if (request.ExpectationFailed)
{
await clientStreamWriter.WriteResponseStatusAsync(args.WebSession.Response.HttpVersion,
(int)HttpStatusCode.ExpectationFailed, "Expectation Failed", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
}
// If expect continue is not enabled then set the connectio and send request headers
if (!request.ExpectContinue)
{
args.WebSession.SetConnection(serverConnection);
await args.WebSession.SendRequest(Enable100ContinueBehaviour, args.IsTransparent,
cancellationToken);
}
// check if content-length is > 0
if (request.ContentLength > 0)
{
if (request.IsBodyRead)
{
var writer = args.WebSession.ServerConnection.StreamWriter;
await writer.WriteBodyAsync(body, request.IsChunked, cancellationToken);
}
else
{
if (!request.ExpectationFailed)
{
if (request.HasBody)
{
HttpWriter writer = args.WebSession.ServerConnection.StreamWriter;
await args.CopyRequestBodyAsync(writer, TransformationMode.None, cancellationToken);
}
}
}
}
// If not expectation failed response was returned by server then parse response
if (!request.ExpectationFailed)
{
await handleHttpSessionResponse(args);
}
args.TimeLine["Response Sent"] = DateTime.Now;
}
/// <summary>
/// Prepare the request headers so that we can avoid encodings not parsable by this proxy
/// </summary>
private void prepareRequestHeaders(HeaderCollection requestHeaders)
{
string acceptEncoding = requestHeaders.GetHeaderValueOrNull(KnownHeaders.AcceptEncoding);
if (acceptEncoding != null)
{
var supportedAcceptEncoding = new List<string>();
//only allow proxy supported compressions
supportedAcceptEncoding.AddRange(acceptEncoding.Split(',')
.Select(x => x.Trim())
.Where(x => ProxyConstants.ProxySupportedCompressions.Contains(x)));
//uncompressed is always supported by proxy
supportedAcceptEncoding.Add("identity");
requestHeaders.SetOrAddHeaderValue(KnownHeaders.AcceptEncoding,
string.Join(",", supportedAcceptEncoding));
}
requestHeaders.FixProxyHeaders();
}
/// <summary>
/// Invoke before request handler if it is set.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <returns></returns>
private async Task invokeBeforeRequest(SessionEventArgs args)
{
if (BeforeRequest != null)
{
await BeforeRequest.InvokeAsync(this, args, ExceptionFunc);
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
#if NETCOREAPP2_1
using System.Net.Security;
#endif
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using StreamExtended.Network;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network.Tcp;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy
{
/// <summary>
/// Handle the request
/// </summary>
public partial class ProxyServer
{
private bool isWindowsAuthenticationEnabledAndSupported =>
EnableWinAuth && RunTime.IsWindows && !RunTime.IsRunningOnMono;
/// <summary>
/// This is the core request handler method for a particular connection from client.
/// Will create new session (request/response) sequence until
/// client/server abruptly terminates connection or by normal HTTP termination.
/// </summary>
/// <param name="endPoint">The proxy endpoint.</param>
/// <param name="clientConnection">The client connection.</param>
/// <param name="clientStream">The client stream.</param>
/// <param name="clientStreamWriter">The client stream writer.</param>
/// <param name="cancellationTokenSource">The cancellation token source for this async task.</param>
/// <param name="httpsConnectHostname">
/// The https hostname as appeared in CONNECT request if this is a HTTPS request from
/// explicit endpoint.
/// </param>
/// <param name="connectRequest">The Connect request if this is a HTTPS request from explicit endpoint.</param>
/// <param name="prefetchConnectionTask">Prefetched server connection for current client using Connect/SNI headers.</param>
private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientConnection clientConnection,
CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter,
CancellationTokenSource cancellationTokenSource, string httpsConnectHostname, ConnectRequest connectRequest,
Task<TcpServerConnection> prefetchConnectionTask = null)
{
var prefetchTask = prefetchConnectionTask;
TcpServerConnection connection = null;
bool closeServerConnection = false;
try
{
var cancellationToken = cancellationTokenSource.Token;
// Loop through each subsequest request on this particular client connection
// (assuming HTTP connection is kept alive by client)
while (true)
{
// read the request line
string httpCmd = await clientStream.ReadLineAsync(cancellationToken);
if (string.IsNullOrEmpty(httpCmd))
{
return;
}
var args = new SessionEventArgs(this, endPoint, cancellationTokenSource)
{
ProxyClient = { ClientConnection = clientConnection },
WebSession = { ConnectRequest = connectRequest }
};
try
{
try
{
Request.ParseRequestLine(httpCmd, out string httpMethod, out string httpUrl,
out var version);
// Read the request headers in to unique and non-unique header collections
await HeaderParser.ReadHeaders(clientStream, args.WebSession.Request.Headers,
cancellationToken);
Uri httpRemoteUri;
if (ProxyConstants.UriSchemeRegex.IsMatch(httpUrl))
{
try
{
httpRemoteUri = new Uri(httpUrl);
}
catch (Exception ex)
{
throw new Exception($"Invalid URI: '{httpUrl}'", ex);
}
}
else
{
string host = args.WebSession.Request.Host ?? httpsConnectHostname;
string hostAndPath = host;
if (httpUrl.StartsWith("/"))
{
hostAndPath += httpUrl;
}
string url = string.Concat(httpsConnectHostname == null ? "http://" : "https://",
hostAndPath);
try
{
httpRemoteUri = new Uri(url);
}
catch (Exception ex)
{
throw new Exception($"Invalid URI: '{url}'", ex);
}
}
var request = args.WebSession.Request;
request.RequestUri = httpRemoteUri;
request.OriginalUrl = httpUrl;
request.Method = httpMethod;
request.HttpVersion = version;
args.ProxyClient.ClientStream = clientStream;
args.ProxyClient.ClientStreamWriter = clientStreamWriter;
if (!args.IsTransparent)
{
// proxy authorization check
if (httpsConnectHostname == null && await checkAuthorization(args) == false)
{
await invokeBeforeResponse(args);
// send the response
await clientStreamWriter.WriteResponseAsync(args.WebSession.Response,
cancellationToken: cancellationToken);
return;
}
prepareRequestHeaders(request.Headers);
request.Host = request.RequestUri.Authority;
}
// if win auth is enabled
// we need a cache of request body
// so that we can send it after authentication in WinAuthHandler.cs
if (isWindowsAuthenticationEnabledAndSupported && request.HasBody)
{
await args.GetRequestBody(cancellationToken);
}
//we need this to syphon out data from connection if API user changes them.
request.SetOriginalHeaders();
args.TimeLine["Request Received"] = DateTime.Now;
// If user requested interception do it
await invokeBeforeRequest(args);
var response = args.WebSession.Response;
if (request.CancelRequest)
{
// syphon out the request body from client before setting the new body
await args.SyphonOutBodyAsync(true, cancellationToken);
await handleHttpSessionResponse(args);
if (!response.KeepAlive)
{
return;
}
continue;
}
//If prefetch task is available.
if (connection == null && prefetchTask != null)
{
connection = await prefetchTask;
prefetchTask = null;
}
// create a new connection if cache key changes.
// only gets hit when connection pool is disabled.
// or when prefetch task has a unexpectedly different connection.
if (connection != null
&& (await tcpConnectionFactory.GetConnectionCacheKey(this, args,
clientConnection.NegotiatedApplicationProtocol)
!= connection.CacheKey))
{
await tcpConnectionFactory.Release(connection);
connection = null;
}
//a connection generator task with captured parameters via closure.
Func<Task<TcpServerConnection>> generator = () =>
tcpConnectionFactory.GetServerConnection(this, args, isConnect: false,
applicationProtocol:clientConnection.NegotiatedApplicationProtocol,
noCache: false, cancellationToken: cancellationToken);
//for connection pool, retry fails until cache is exhausted.
var result = await retryPolicy<ServerConnectionException>().ExecuteAsync(async (serverConnection) =>
{
args.TimeLine["Server Connection Created"] = DateTime.Now;
// if upgrading to websocket then relay the request without reading the contents
if (request.UpgradeToWebSocket)
{
await handleWebSocketUpgrade(httpCmd, args, request,
response, clientStream, clientStreamWriter,
serverConnection, cancellationTokenSource, cancellationToken);
closeServerConnection = true;
return false;
}
// construct the web request that we are going to issue on behalf of the client.
await handleHttpSessionRequest(serverConnection, args);
return true;
}, generator, connection);
//update connection to latest used
connection = result.LatestConnection;
//throw if exception happened
if(!result.IsSuccess)
{
throw result.Exception;
}
if(!result.Continue)
{
return;
}
//user requested
if (args.WebSession.CloseServerConnection)
{
closeServerConnection = true;
return;
}
// if connection is closing exit
if (!response.KeepAlive)
{
closeServerConnection = true;
return;
}
if (cancellationTokenSource.IsCancellationRequested)
{
throw new Exception("Session was terminated by user.");
}
//Get/release server connection for each HTTP session instead of per client connection.
//This will be more efficient especially when client is idly holding server connection
//between sessions without using it.
//Do not release authenticated connections for performance reasons.
//Otherwise it will keep authenticating per session.
if (EnableConnectionPool && connection!=null
&& !connection.IsWinAuthenticated)
{
await tcpConnectionFactory.Release(connection);
connection = null;
}
}
catch (Exception e) when (!(e is ProxyHttpException))
{
throw new ProxyHttpException("Error occured whilst handling session request", e, args);
}
}
catch (Exception e)
{
args.Exception = e;
closeServerConnection = true;
throw;
}
finally
{
await invokeAfterResponse(args);
args.Dispose();
}
}
}
finally
{
await tcpConnectionFactory.Release(connection,
closeServerConnection);
await tcpConnectionFactory.Release(prefetchTask, closeServerConnection);
}
}
/// <summary>
/// Handle a specific session (request/response sequence)
/// </summary>
/// <param name="serverConnection">The tcp connection.</param>
/// <param name="args">The session event arguments.</param>
/// <returns></returns>
private async Task handleHttpSessionRequest(TcpServerConnection serverConnection, SessionEventArgs args)
{
var cancellationToken = args.CancellationTokenSource.Token;
var request = args.WebSession.Request;
request.Locked = true;
var body = request.CompressBodyAndUpdateContentLength();
// if expect continue is enabled then send the headers first
// and see if server would return 100 conitinue
if (request.ExpectContinue)
{
args.WebSession.SetConnection(serverConnection);
await args.WebSession.SendRequest(Enable100ContinueBehaviour, args.IsTransparent,
cancellationToken);
}
// If 100 continue was the response inform that to the client
if (Enable100ContinueBehaviour)
{
var clientStreamWriter = args.ProxyClient.ClientStreamWriter;
if (request.Is100Continue)
{
await clientStreamWriter.WriteResponseStatusAsync(args.WebSession.Response.HttpVersion,
(int)HttpStatusCode.Continue, "Continue", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
else if (request.ExpectationFailed)
{
await clientStreamWriter.WriteResponseStatusAsync(args.WebSession.Response.HttpVersion,
(int)HttpStatusCode.ExpectationFailed, "Expectation Failed", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
}
// If expect continue is not enabled then set the connectio and send request headers
if (!request.ExpectContinue)
{
args.WebSession.SetConnection(serverConnection);
await args.WebSession.SendRequest(Enable100ContinueBehaviour, args.IsTransparent,
cancellationToken);
}
// check if content-length is > 0
if (request.ContentLength > 0)
{
if (request.IsBodyRead)
{
var writer = args.WebSession.ServerConnection.StreamWriter;
await writer.WriteBodyAsync(body, request.IsChunked, cancellationToken);
}
else
{
if (!request.ExpectationFailed)
{
if (request.HasBody)
{
HttpWriter writer = args.WebSession.ServerConnection.StreamWriter;
await args.CopyRequestBodyAsync(writer, TransformationMode.None, cancellationToken);
}
}
}
}
// If not expectation failed response was returned by server then parse response
if (!request.ExpectationFailed)
{
await handleHttpSessionResponse(args);
}
args.TimeLine["Response Sent"] = DateTime.Now;
}
/// <summary>
/// Prepare the request headers so that we can avoid encodings not parsable by this proxy
/// </summary>
private void prepareRequestHeaders(HeaderCollection requestHeaders)
{
string acceptEncoding = requestHeaders.GetHeaderValueOrNull(KnownHeaders.AcceptEncoding);
if (acceptEncoding != null)
{
var supportedAcceptEncoding = new List<string>();
//only allow proxy supported compressions
supportedAcceptEncoding.AddRange(acceptEncoding.Split(',')
.Select(x => x.Trim())
.Where(x => ProxyConstants.ProxySupportedCompressions.Contains(x)));
//uncompressed is always supported by proxy
supportedAcceptEncoding.Add("identity");
requestHeaders.SetOrAddHeaderValue(KnownHeaders.AcceptEncoding,
string.Join(",", supportedAcceptEncoding));
}
requestHeaders.FixProxyHeaders();
}
/// <summary>
/// Invoke before request handler if it is set.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <returns></returns>
private async Task invokeBeforeRequest(SessionEventArgs args)
{
if (BeforeRequest != null)
{
await BeforeRequest.InvokeAsync(this, args, ExceptionFunc);
}
}
}
}
using System;
using System.Net;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Network.WinAuth.Security;
namespace Titanium.Web.Proxy
{
/// <summary>
/// Handle the response from server.
/// </summary>
public partial class ProxyServer
{
/// <summary>
/// Called asynchronously when a request was successfull and we received the response.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <returns> The task.</returns>
private async Task handleHttpSessionResponse(SessionEventArgs args)
{
var cancellationToken = args.CancellationTokenSource.Token;
// read response & headers from server
await args.WebSession.ReceiveResponse(cancellationToken);
args.TimeLine["Response Received"] = DateTime.Now;
var response = args.WebSession.Response;
args.ReRequest = false;
// check for windows authentication
if (isWindowsAuthenticationEnabledAndSupported)
{
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
await handle401UnAuthorized(args);
}
else
{
WinAuthEndPoint.AuthenticatedResponse(args.WebSession.Data);
}
}
//save original values so that if user changes them
//we can still use original values when syphoning out data from attached tcp connection.
response.SetOriginalHeaders();
// if user requested call back then do it
if (!response.Locked)
{
await invokeBeforeResponse(args);
}
// it may changed in the user event
response = args.WebSession.Response;
var clientStreamWriter = args.ProxyClient.ClientStreamWriter;
//user set custom response by ignoring original response from server.
if (response.Locked)
{
//write custom user response with body and return.
await clientStreamWriter.WriteResponseAsync(response, cancellationToken: cancellationToken);
if(args.WebSession.ServerConnection != null
&& !args.WebSession.CloseServerConnection)
{
// syphon out the original response body from server connection
// so that connection will be good to be reused.
await args.SyphonOutBodyAsync(false, cancellationToken);
}
return;
}
// if user requested to send request again
// likely after making modifications from User Response Handler
if (args.ReRequest)
{
// clear current response
await args.ClearResponse(cancellationToken);
await handleHttpSessionRequest(args.WebSession.ServerConnection, args);
return;
}
response.Locked = true;
// Write back to client 100-conitinue response if that's what server returned
if (response.Is100Continue)
{
await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion,
(int)HttpStatusCode.Continue, "Continue", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
else if (response.ExpectationFailed)
{
await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion,
(int)HttpStatusCode.ExpectationFailed, "Expectation Failed", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
if (!args.IsTransparent)
{
response.Headers.FixProxyHeaders();
}
if (response.IsBodyRead)
{
await clientStreamWriter.WriteResponseAsync(response, cancellationToken: cancellationToken);
}
else
{
// Write back response status to client
await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion, response.StatusCode,
response.StatusDescription, cancellationToken);
await clientStreamWriter.WriteHeadersAsync(response.Headers, cancellationToken: cancellationToken);
// Write body if exists
if (response.HasBody)
{
await args.CopyResponseBodyAsync(clientStreamWriter, TransformationMode.None,
cancellationToken);
}
}
}
/// <summary>
/// Invoke before response if it is set.
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private async Task invokeBeforeResponse(SessionEventArgs args)
{
if (BeforeResponse != null)
{
await BeforeResponse.InvokeAsync(this, args, ExceptionFunc);
}
}
/// <summary>
/// Invoke after response if it is set.
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private async Task invokeAfterResponse(SessionEventArgs args)
{
if (AfterResponse != null)
{
await AfterResponse.InvokeAsync(this, args, ExceptionFunc);
}
}
}
}
using System;
using System.Net;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Network.WinAuth.Security;
namespace Titanium.Web.Proxy
{
/// <summary>
/// Handle the response from server.
/// </summary>
public partial class ProxyServer
{
/// <summary>
/// Called asynchronously when a request was successfull and we received the response.
/// </summary>
/// <param name="args">The session event arguments.</param>
/// <returns> The task.</returns>
private async Task handleHttpSessionResponse(SessionEventArgs args)
{
var cancellationToken = args.CancellationTokenSource.Token;
// read response & headers from server
await args.WebSession.ReceiveResponse(cancellationToken);
args.TimeLine["Response Received"] = DateTime.Now;
var response = args.WebSession.Response;
args.ReRequest = false;
// check for windows authentication
if (isWindowsAuthenticationEnabledAndSupported)
{
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
await handle401UnAuthorized(args);
}
else
{
WinAuthEndPoint.AuthenticatedResponse(args.WebSession.Data);
}
}
//save original values so that if user changes them
//we can still use original values when syphoning out data from attached tcp connection.
response.SetOriginalHeaders();
// if user requested call back then do it
if (!response.Locked)
{
await invokeBeforeResponse(args);
}
// it may changed in the user event
response = args.WebSession.Response;
var clientStreamWriter = args.ProxyClient.ClientStreamWriter;
//user set custom response by ignoring original response from server.
if (response.Locked)
{
//write custom user response with body and return.
await clientStreamWriter.WriteResponseAsync(response, cancellationToken: cancellationToken);
if(args.WebSession.ServerConnection != null
&& !args.WebSession.CloseServerConnection)
{
// syphon out the original response body from server connection
// so that connection will be good to be reused.
await args.SyphonOutBodyAsync(false, cancellationToken);
}
return;
}
// if user requested to send request again
// likely after making modifications from User Response Handler
if (args.ReRequest)
{
// clear current response
await args.ClearResponse(cancellationToken);
await handleHttpSessionRequest(args.WebSession.ServerConnection, args);
return;
}
response.Locked = true;
// Write back to client 100-conitinue response if that's what server returned
if (response.Is100Continue)
{
await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion,
(int)HttpStatusCode.Continue, "Continue", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
else if (response.ExpectationFailed)
{
await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion,
(int)HttpStatusCode.ExpectationFailed, "Expectation Failed", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
if (!args.IsTransparent)
{
response.Headers.FixProxyHeaders();
}
if (response.IsBodyRead)
{
await clientStreamWriter.WriteResponseAsync(response, cancellationToken: cancellationToken);
}
else
{
// Write back response status to client
await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion, response.StatusCode,
response.StatusDescription, cancellationToken);
await clientStreamWriter.WriteHeadersAsync(response.Headers, cancellationToken: cancellationToken);
// Write body if exists
if (response.HasBody)
{
await args.CopyResponseBodyAsync(clientStreamWriter, TransformationMode.None,
cancellationToken);
}
}
}
/// <summary>
/// Invoke before response if it is set.
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private async Task invokeBeforeResponse(SessionEventArgs args)
{
if (BeforeResponse != null)
{
await BeforeResponse.InvokeAsync(this, args, ExceptionFunc);
}
}
/// <summary>
/// Invoke after response if it is set.
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private async Task invokeAfterResponse(SessionEventArgs args)
{
if (AfterResponse != null)
{
await AfterResponse.InvokeAsync(this, args, ExceptionFunc);
}
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
<RootNamespace>Titanium.Web.Proxy</RootNamespace>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>StrongNameKey.snk</AssemblyOriginatorKeyFile>
<DelaySign>False</DelaySign>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Portable.BouncyCastle" Version="1.8.2" />
<PackageReference Include="StreamExtended" Version="1.0.179" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Win32.Registry">
<Version>4.4.0</Version>
</PackageReference>
<PackageReference Include="System.Security.Principal.Windows">
<Version>4.4.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<PackageReference Include="Microsoft.Win32.Registry">
<Version>4.4.0</Version>
</PackageReference>
<PackageReference Include="System.Security.Principal.Windows">
<Version>4.4.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net45'">
<Reference Include="System.Web" />
</ItemGroup>
<ItemGroup>
<Compile Update="Network\WinAuth\Security\Common.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
<Compile Update="Network\WinAuth\Security\LittleEndian.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
<Compile Update="Network\WinAuth\Security\Message.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
<Compile Update="Network\WinAuth\Security\State.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
<Compile Update="Properties\AssemblyInfo.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
<RootNamespace>Titanium.Web.Proxy</RootNamespace>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>StrongNameKey.snk</AssemblyOriginatorKeyFile>
<DelaySign>False</DelaySign>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Portable.BouncyCastle" Version="1.8.2" />
<PackageReference Include="StreamExtended" Version="1.0.179" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Win32.Registry">
<Version>4.4.0</Version>
</PackageReference>
<PackageReference Include="System.Security.Principal.Windows">
<Version>4.4.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<PackageReference Include="Microsoft.Win32.Registry">
<Version>4.4.0</Version>
</PackageReference>
<PackageReference Include="System.Security.Principal.Windows">
<Version>4.4.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net45'">
<Reference Include="System.Web" />
</ItemGroup>
<ItemGroup>
<Compile Update="Network\WinAuth\Security\Common.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
<Compile Update="Network\WinAuth\Security\LittleEndian.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
<Compile Update="Network\WinAuth\Security\Message.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
<Compile Update="Network\WinAuth\Security\State.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
<Compile Update="Properties\AssemblyInfo.cs">
<ExcludeFromSourceAnalysis>True</ExcludeFromSourceAnalysis>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>
</Compile>
</ItemGroup>
</Project>
\ No newline at end of file
......@@ -56,8 +56,8 @@
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj">
<Project>{8d73a1be-868c-42d2-9ece-f32cc1a02906}</Project>
<ProjectReference Include="..\..\src\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj">
<Project>{91018b6d-a7a9-45be-9cb3-79cbb8b169a6}</Project>
<Name>Titanium.Web.Proxy</Name>
</ProjectReference>
</ItemGroup>
......
......@@ -65,13 +65,13 @@
<Compile Include="WinAuthTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj">
<Project>{8d73a1be-868c-42d2-9ece-f32cc1a02906}</Project>
<Name>Titanium.Web.Proxy</Name>
</ProjectReference>
<None Include="StrongNameKey.snk" />
</ItemGroup>
<ItemGroup>
<None Include="StrongNameKey.snk" />
<ProjectReference Include="..\..\src\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj">
<Project>{91018b6d-a7a9-45be-9cb3-79cbb8b169a6}</Project>
<Name>Titanium.Web.Proxy</Name>
</ProjectReference>
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment