Commit 255edefd authored by Björn Weström's avatar Björn Weström Committed by GitHub

Merge pull request #1 from justcoding121/develop

Merge to latest
parents dbe023ba 4b2f944a
......@@ -78,7 +78,7 @@ namespace Titanium.Web.Proxy.Examples.Basic
//proxyServer.EnableWinAuth = true;
explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, 8000, true)
explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, 8000)
{
//You can set only one of the ExcludedHttpsHostNameRegex and IncludedHttpsHostNameRegex properties, otherwise ArgumentException will be thrown
......
......@@ -483,7 +483,6 @@ namespace Titanium.Web.Proxy.EventArguments
}
request.Body = body;
request.UpdateContentLength();
}
/// <summary>
......
......@@ -14,5 +14,10 @@ namespace Titanium.Web.Proxy.Extensions
{
return CultureInfo.CurrentCulture.CompareInfo.IndexOf(str, value, CompareOptions.IgnoreCase) >= 0;
}
internal static int IndexOfIgnoreCase(this string str, string value)
{
return CultureInfo.CurrentCulture.CompareInfo.IndexOf(str, value, CompareOptions.IgnoreCase);
}
}
}
......@@ -102,6 +102,11 @@ namespace Titanium.Web.Proxy.Http
internal override void EnsureBodyAvailable(bool throwWhenNotReadYet = true)
{
if (BodyInternal != null)
{
return;
}
//GET request don't have a request body to read
if (!HasBody)
{
......
......@@ -16,7 +16,7 @@ namespace Titanium.Web.Proxy.Http
/// <summary>
/// Cached body content as byte array
/// </summary>
private byte[] body;
protected byte[] BodyInternal;
/// <summary>
/// Cached body as string
......@@ -127,11 +127,11 @@ namespace Titanium.Web.Proxy.Http
get
{
EnsureBodyAvailable();
return body;
return BodyInternal;
}
internal set
{
body = value;
BodyInternal = value;
bodyString = null;
//If there is a content length header update it
......@@ -173,6 +173,11 @@ namespace Titanium.Web.Proxy.Http
internal byte[] CompressBodyAndUpdateContentLength()
{
if (!IsBodyRead && BodyInternal == null)
{
return null;
}
bool isChunked = IsChunked;
string contentEncoding = ContentEncoding;
......@@ -219,7 +224,7 @@ namespace Titanium.Web.Proxy.Http
internal void UpdateContentLength()
{
ContentLength = IsChunked ? -1 : body?.Length ?? 0;
ContentLength = IsChunked ? -1 : BodyInternal?.Length ?? 0;
}
/// <summary>
......@@ -229,7 +234,7 @@ namespace Titanium.Web.Proxy.Http
{
if (!KeepBody)
{
body = null;
BodyInternal = null;
bodyString = null;
}
}
......
......@@ -81,6 +81,11 @@ namespace Titanium.Web.Proxy.Http
internal override void EnsureBodyAvailable(bool throwWhenNotReadYet = true)
{
if (BodyInternal != null)
{
return;
}
if (!IsBodyRead && throwWhenNotReadYet)
{
throw new Exception("Response body is not read yet. " +
......@@ -123,6 +128,21 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Constructor.
/// </summary>
public Response()
{
}
/// <summary>
/// Constructor.
/// </summary>
public Response(byte[] body)
{
Body = body;
}
internal static string CreateResponseLine(Version version, int statusCode, string statusDescription)
{
return $"HTTP/{version.Major}.{version.Minor} {statusCode} {statusDescription}";
......
......@@ -15,5 +15,13 @@ namespace Titanium.Web.Proxy.Http.Responses
StatusCode = (int)HttpStatusCode.OK;
StatusDescription = "OK";
}
/// <summary>
/// Constructor.
/// </summary>
public OkResponse(byte[] body) : this()
{
Body = body;
}
}
}
......@@ -17,6 +17,11 @@ namespace Titanium.Web.Proxy.Models
internal bool IsSystemHttpsProxy { get; set; }
/// <summary>
/// Enable SSL?
/// </summary>
public bool DecryptSsl { get; }
/// <summary>
/// Generic certificate to use for SSL decryption.
/// </summary>
......@@ -40,9 +45,10 @@ namespace Titanium.Web.Proxy.Models
/// </summary>
/// <param name="ipAddress"></param>
/// <param name="port"></param>
/// <param name="enableSsl"></param>
public ExplicitProxyEndPoint(IPAddress ipAddress, int port, bool enableSsl) : base(ipAddress, port, enableSsl)
/// <param name="decryptSsl"></param>
public ExplicitProxyEndPoint(IPAddress ipAddress, int port, bool decryptSsl = true) : base(ipAddress, port)
{
this.DecryptSsl = decryptSsl;
}
internal async Task InvokeBeforeTunnelConnectRequest(ProxyServer proxyServer, TunnelConnectSessionEventArgs connectArgs, ExceptionHandler exceptionFunc)
......
......@@ -17,11 +17,10 @@ namespace Titanium.Web.Proxy.Models
/// <param name="ipAddress"></param>
/// <param name="port"></param>
/// <param name="enableSsl"></param>
protected ProxyEndPoint(IPAddress ipAddress, int port, bool enableSsl)
protected ProxyEndPoint(IPAddress ipAddress, int port)
{
IpAddress = ipAddress;
Port = port;
EnableSsl = enableSsl;
}
/// <summary>
......@@ -39,10 +38,6 @@ namespace Titanium.Web.Proxy.Models
/// </summary>
public int Port { get; internal set; }
/// <summary>
/// Enable SSL?
/// </summary>
public bool EnableSsl { get; }
/// <summary>
/// Is IPv6 enabled?
......
......@@ -20,7 +20,7 @@ namespace Titanium.Web.Proxy.Models
/// <param name="ipAddress"></param>
/// <param name="port"></param>
/// <param name="enableSsl"></param>
public TransparentProxyEndPoint(IPAddress ipAddress, int port, bool enableSsl) : base(ipAddress, port, enableSsl)
public TransparentProxyEndPoint(IPAddress ipAddress, int port) : base(ipAddress, port)
{
GenericCertificateName = "localhost";
}
......
......@@ -7,12 +7,24 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
/// </summary>
internal class State
{
/// <summary>
/// States during Windows Authentication
/// </summary>
public enum WinAuthState
{
UNAUTHORIZED,
INITIAL_TOKEN,
FINAL_TOKEN,
AUTHORIZED
};
internal State()
{
Credentials = new Common.SecurityHandle(0);
Context = new Common.SecurityHandle(0);
LastSeen = DateTime.Now;
AuthState = WinAuthState.UNAUTHORIZED;
}
/// <summary>
......@@ -30,10 +42,16 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
/// </summary>
internal DateTime LastSeen;
/// <summary>
/// Current state of the authentication process
/// </summary>
internal WinAuthState AuthState;
internal void ResetHandles()
{
Credentials.Reset();
Context.Reset();
AuthState = WinAuthState.UNAUTHORIZED;
}
internal void UpdatePresence()
......
......@@ -38,11 +38,9 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
try
{
int result;
var state = new State();
result = AcquireCredentialsHandle(
int result = AcquireCredentialsHandle(
WindowsIdentity.GetCurrent().Name,
authScheme,
SecurityCredentialsOutbound,
......@@ -79,6 +77,7 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
return null;
}
state.AuthState = State.WinAuthState.INITIAL_TOKEN;
token = clientToken.GetBytes();
authStates.Add(requestId, state);
}
......@@ -109,13 +108,11 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
try
{
int result;
var state = authStates[requestId];
state.UpdatePresence();
result = InitializeSecurityContext(ref state.Credentials,
int result = InitializeSecurityContext(ref state.Credentials,
ref state.Context,
hostname,
StandardContextAttributes,
......@@ -135,7 +132,7 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
return null;
}
authStates.Remove(requestId);
state.AuthState = State.WinAuthState.FINAL_TOKEN;
token = clientToken.GetBytes();
}
finally
......@@ -166,6 +163,49 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
await Task.Delay(1000 * 60);
}
/// <summary>
/// Validates that the current WinAuth state of the connection matches the
/// expectation, used to detect failed authentication
/// </summary>
/// <param name="requestId"></param>
/// <param name="expectedAuthState"></param>
/// <returns></returns>
internal static bool ValidateWinAuthState(Guid requestId, State.WinAuthState expectedAuthState)
{
var stateExists = authStates.TryGetValue(requestId, out var state);
if (expectedAuthState == State.WinAuthState.UNAUTHORIZED)
{
// Validation before initial token
return stateExists == false ||
state.AuthState == State.WinAuthState.UNAUTHORIZED ||
state.AuthState == State.WinAuthState.AUTHORIZED; // Server may require re-authentication on an open connection
}
if (expectedAuthState == State.WinAuthState.INITIAL_TOKEN)
{
// Validation before final token
return stateExists &&
(state.AuthState == State.WinAuthState.INITIAL_TOKEN ||
state.AuthState == State.WinAuthState.AUTHORIZED); // Server may require re-authentication on an open connection
}
throw new Exception("Unsupported validation of WinAuthState");
}
/// <summary>
/// Set the AuthState to authorized and update the connection state lifetime
/// </summary>
/// <param name="requestId"></param>
internal static void AuthenticatedResponse(Guid requestId)
{
if (authStates.TryGetValue(requestId, out var state))
{
state.AuthState = State.WinAuthState.AUTHORIZED;
state.UpdatePresence();
}
}
#region Native calls to secur32.dll
[DllImport("secur32.dll", SetLastError = true)]
......
......@@ -319,10 +319,6 @@ namespace Titanium.Web.Proxy
if (isHttps)
{
if (!endPoint.EnableSsl)
{
throw new Exception("Endpoint do not support Https connections");
}
CertificateManager.EnsureRootCertificate();
......
......@@ -79,7 +79,7 @@ namespace Titanium.Web.Proxy
await endPoint.InvokeBeforeTunnelConnectRequest(this, connectArgs, ExceptionFunc);
//filter out excluded host names
bool excluded = connectArgs.Excluded;
bool excluded = !endPoint.DecryptSsl || connectArgs.Excluded;
if (await CheckAuthorization(connectArgs) == false)
{
......@@ -98,6 +98,7 @@ namespace Titanium.Web.Proxy
await clientStreamWriter.WriteResponseAsync(response);
var clientHelloInfo = await SslTools.PeekClientHello(clientStream);
bool isClientHello = clientHelloInfo != null;
if (isClientHello)
{
......@@ -130,7 +131,7 @@ namespace Titanium.Web.Proxy
clientStreamReader = new CustomBinaryReader(clientStream, BufferSize);
clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize);
}
catch(Exception e)
catch (Exception e)
{
ExceptionFunc(new Exception($"Could'nt authenticate client '{connectHostname}' with fake certificate.", e));
sslStream?.Dispose();
......@@ -226,38 +227,47 @@ namespace Titanium.Web.Proxy
var clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize);
try
{
if (endPoint.EnableSsl)
{
var clientHelloInfo = await SslTools.PeekClientHello(clientStream);
if (clientHelloInfo != null)
{
var sslStream = new SslStream(clientStream);
clientStream = new CustomBufferedStream(sslStream, BufferSize);
var isHttps = clientHelloInfo != null;
string httpsHostName = null;
string sniHostName = clientHelloInfo.GetServerName() ?? endPoint.GenericCertificateName;
if (isHttps)
{
SslStream sslStream = null;
string certName = HttpHelper.GetWildCardDomainName(sniHostName);
var certificate = await CertificateManager.CreateCertificateAsync(certName);
try
{
sslStream = new SslStream(clientStream);
httpsHostName = clientHelloInfo.GetServerName() ?? endPoint.GenericCertificateName;
string certName = HttpHelper.GetWildCardDomainName(httpsHostName);
var certificate = await CertificateManager.CreateCertificateAsync(certName);
//Successfully managed to authenticate the client using the fake certificate
await sslStream.AuthenticateAsServerAsync(certificate, false, SslProtocols.Tls, false);
//HTTPS server created - we can now decrypt the client's traffic
clientStream = new CustomBufferedStream(sslStream, BufferSize);
clientStreamReader.Dispose();
clientStreamReader = new CustomBinaryReader(clientStream, BufferSize);
clientStreamWriter = new HttpResponseWriter(clientStream, BufferSize);
}
catch (Exception e)
{
ExceptionFunc(new Exception($"Could'nt authenticate client '{sniHostName}' with fake certificate.", e));
ExceptionFunc(new Exception($"Could'nt authenticate client '{httpsHostName}' with fake certificate.", e));
sslStream?.Dispose();
return;
}
}
//HTTPS server created - we can now decrypt the client's traffic
}
//Now create the request
await HandleHttpSessionRequest(tcpClient, clientStream, clientStreamReader, clientStreamWriter,
endPoint.EnableSsl ? endPoint.GenericCertificateName : null, endPoint, null, true);
isHttps ? httpsHostName : null, endPoint, null, true);
}
finally
{
......@@ -605,7 +615,7 @@ namespace Titanium.Web.Proxy
/// <param name="requestHeaders"></param>
private void PrepareRequestHeaders(HeaderCollection requestHeaders)
{
if(requestHeaders.HeaderExists(KnownHeaders.AcceptEncoding))
if (requestHeaders.HeaderExists(KnownHeaders.AcceptEncoding))
{
requestHeaders.SetOrAddHeaderValue(KnownHeaders.AcceptEncoding, "gzip,deflate");
}
......
......@@ -7,6 +7,7 @@ using Titanium.Web.Proxy.Compression;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Exceptions;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Network.WinAuth.Security;
namespace Titanium.Web.Proxy
{
......@@ -28,14 +29,22 @@ namespace Titanium.Web.Proxy
await args.WebSession.ReceiveResponse();
var response = args.WebSession.Response;
args.ReRequest = false;
//check for windows authentication
if (isWindowsAuthenticationEnabledAndSupported && response.StatusCode == (int)HttpStatusCode.Unauthorized)
if (isWindowsAuthenticationEnabledAndSupported)
{
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
await Handle401UnAuthorized(args);
}
else
{
WinAuthEndPoint.AuthenticatedResponse(args.WebSession.RequestId);
}
}
args.ReRequest = false;
response.OriginalHasBody = response.HasBody;
//if user requested call back then do it
if (!response.Locked)
......
......@@ -3,30 +3,32 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network.WinAuth;
using Titanium.Web.Proxy.Network.WinAuth.Security;
namespace Titanium.Web.Proxy
{
public partial class ProxyServer
{
//possible header names
private static readonly List<string> authHeaderNames = new List<string>
private static readonly HashSet<string> authHeaderNames = new HashSet<string>
{
"WWW-Authenticate",
"WWW-Authenticate".ToLower(),
//IIS 6.0 messed up names below
"WWWAuthenticate",
"NTLMAuthorization",
"NegotiateAuthorization",
"KerberosAuthorization"
"WWWAuthenticate".ToLower(),
"NTLMAuthorization".ToLower(),
"NegotiateAuthorization".ToLower(),
"KerberosAuthorization".ToLower()
};
private static readonly List<string> authSchemes = new List<string>
private static readonly HashSet<string> authSchemes = new HashSet<string>
{
"NTLM",
"Negotiate",
"Kerberos"
"NTLM".ToLower(),
"Negotiate".ToLower(),
"Kerberos".ToLower()
};
/// <summary>
......@@ -47,7 +49,7 @@ namespace Titanium.Web.Proxy
//check in non-unique headers first
var header = response.Headers.NonUniqueHeaders.FirstOrDefault(
x => authHeaderNames.Any(y => x.Key.Equals(y, StringComparison.OrdinalIgnoreCase)));
x => authHeaderNames.Contains(x.Key.ToLower()));
if (!header.Equals(new KeyValuePair<string, List<HttpHeader>>()))
{
......@@ -63,8 +65,10 @@ namespace Titanium.Web.Proxy
//check in unique headers
if (authHeader == null)
{
headerName = null;
//check in non-unique headers first
var uHeader = response.Headers.Headers.FirstOrDefault(x => authHeaderNames.Any(y => x.Key.Equals(y, StringComparison.OrdinalIgnoreCase)));
var uHeader = response.Headers.Headers.FirstOrDefault(x => authHeaderNames.Contains(x.Key.ToLower()));
if (!uHeader.Equals(new KeyValuePair<string, HttpHeader>()))
{
......@@ -82,7 +86,16 @@ namespace Titanium.Web.Proxy
if (authHeader != null)
{
string scheme = authSchemes.FirstOrDefault(x => authHeader.Value.Equals(x, StringComparison.OrdinalIgnoreCase));
string scheme = authSchemes.Contains(authHeader.Value.ToLower()) ? authHeader.Value.ToLower() : null;
var expectedAuthState = scheme == null ? State.WinAuthState.INITIAL_TOKEN : State.WinAuthState.UNAUTHORIZED;
if (!WinAuthEndPoint.ValidateWinAuthState(args.WebSession.RequestId, expectedAuthState))
{
// Invalid state, create proper error message to client
await RewriteUnauthorizedResponse(args);
return;
}
var request = args.WebSession.Request;
......@@ -108,7 +121,7 @@ namespace Titanium.Web.Proxy
//challenge value will start with any of the scheme selected
else
{
scheme = authSchemes.FirstOrDefault(x => authHeader.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase) &&
scheme = authSchemes.First(x => authHeader.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase) &&
authHeader.Value.Length > x.Length + 1);
string serverToken = authHeader.Value.Substring(scheme.Length + 1);
......@@ -130,13 +143,50 @@ namespace Titanium.Web.Proxy
//Should we cache all Set-Cokiee headers from server during auth process
//and send it to client after auth?
//clear current server response
await args.ClearResponse();
// Let ResponseHandler send the updated request
args.ReRequest = true;
}
}
/// <summary>
/// Rewrites the response body for failed authentication
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
internal async Task RewriteUnauthorizedResponse(SessionEventArgs args)
{
var response = args.WebSession.Response;
// Strip authentication headers to avoid credentials prompt in client web browser
foreach (var authHeaderName in authHeaderNames)
{
response.Headers.RemoveHeader(authHeaderName);
}
//request again with updated authorization header
//and server cookies
await HandleHttpSessionRequestInternal(args.WebSession.ServerConnection, args);
// Add custom div to body to clarify that the proxy (not the client browser) failed authentication
string authErrorMessage = "<div class=\"inserted-by-proxy\"><h2>NTLM authentication through Titanium.Web.Proxy (" +
args.ProxyClient.TcpClient.Client.LocalEndPoint +
") failed. Please check credentials.</h2></div>";
string originalErrorMessage = "<div class=\"inserted-by-proxy\"><h3>Response from remote web server below.</h3></div><br/>";
string body = await args.GetResponseBodyAsString();
int idx = body.IndexOfIgnoreCase("<body>");
if (idx >= 0)
{
var bodyPos = idx + "<body>".Length;
body = body.Insert(bodyPos, authErrorMessage + originalErrorMessage);
}
else
{
// Cannot parse response body, replace it
body = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" +
"<html xmlns=\"http://www.w3.org/1999/xhtml\">" +
"<body>" +
authErrorMessage +
"</body>" +
"</html>";
}
args.SetResponseBodyString(body);
}
}
}
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