From 77a28efa649ef8fc50177524a76a1a57518b2378 Mon Sep 17 00:00:00 2001 From: Samuele Pilleri Date: Wed, 11 Jan 2023 22:40:41 +0100 Subject: [PATCH] Working 407 handling for plain HTTP requests --- .../Handlers/WinAuthHandler.cs | 110 ++++++++++++++++++ .../Network/WinAuth/Security/Common.cs | 23 ++-- .../WinAuth/Security/WinAuthEndPoint.cs | 35 +++--- .../Network/WinAuth/WinAuthHandler.cs | 54 ++++++++- src/Titanium.Web.Proxy/RequestHandler.cs | 2 +- src/Titanium.Web.Proxy/ResponseHandler.cs | 11 +- 6 files changed, 200 insertions(+), 35 deletions(-) diff --git a/src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs b/src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs index d8b0d4bcc..5c7efe414 100644 --- a/src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs @@ -27,6 +27,11 @@ public partial class ProxyServer "KerberosAuthorization" }; + private static readonly HashSet proxyAuthHeaderNames = new(StringComparer.OrdinalIgnoreCase) + { + "Proxy-Authenticate" + }; + /// /// supported authentication schemes. /// @@ -141,6 +146,110 @@ private async Task Handle401UnAuthorized(SessionEventArgs args) } } + /// + /// Handle windows NTLM/Kerberos proxy authentication. + /// Note: NTLM/Kerberos cannot do a man in middle operation + /// we do for HTTPS requests. + /// As such we will be sending local credentials of current + /// User to server to authenticate requests. + /// To disable this set ProxyServer.EnableWinAuth to false. + /// + private async Task Handle407ProxyAuthorization(SessionEventArgs args) + { + string? headerName = null; + HttpHeader? authHeader = null; + + var response = args.HttpClient.Response; + + // check in non-unique headers first + var header = response.Headers.NonUniqueHeaders.FirstOrDefault(x => proxyAuthHeaderNames.Contains(x.Key)); + + if (!header.Equals(new KeyValuePair>())) headerName = header.Key; + + if (headerName != null) + authHeader = response.Headers.NonUniqueHeaders[headerName] + .FirstOrDefault( + x => authSchemes.Any(y => x.Value.StartsWith(y, StringComparison.OrdinalIgnoreCase))); + + // check in unique headers + if (authHeader == null) + { + headerName = null; + + // check in non-unique headers first + var uHeader = response.Headers.Headers.FirstOrDefault(x => proxyAuthHeaderNames.Contains(x.Key)); + + if (!uHeader.Equals(new KeyValuePair())) headerName = uHeader.Key; + + if (headerName != null) + authHeader = authSchemes.Any(x => response.Headers.Headers[headerName].Value + .StartsWith(x, StringComparison.OrdinalIgnoreCase)) + ? response.Headers.Headers[headerName] + : null; + } + + if (authHeader != null) + { + var scheme = authSchemes.Contains(authHeader.Value) ? authHeader.Value : null; + + var expectedAuthState = + scheme == null ? State.WinAuthState.InitialToken : State.WinAuthState.FinalToken; + + if (!WinAuthEndPoint.ValidateWinAuthState(args.HttpClient.Data, expectedAuthState)) + { + // Invalid state, create proper error message to client + await RewriteUnauthorizedResponse(args); + return; + } + + var request = args.HttpClient.Request; + + // clear any existing headers to avoid confusing bad servers + request.Headers.RemoveHeader(KnownHeaders.ProxyAuthorization); + + // initial value will match exactly any of the schemes + if (scheme != null) + { + var clientToken = WinAuthHandler.GetInitialProxyAuthToken(args.CustomUpStreamProxyUsed!.HostName, scheme, args.HttpClient.Data); + + var auth = string.Concat(scheme, clientToken); + + // replace existing authorization header if any + request.Headers.SetOrAddHeaderValue(KnownHeaders.ProxyAuthorization, auth); + + // don't need to send body for Authorization request + if (request.HasBody) request.ContentLength = 0; + } + else + { + // challenge value will start with any of the scheme selected + scheme = authSchemes.First(x => + authHeader.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase) && + authHeader.Value.Length > x.Length + 1); + + var serverToken = authHeader.Value.Substring(scheme.Length + 1); + var clientToken = WinAuthHandler.GetFinalProxyAuthToken(args.CustomUpStreamProxyUsed!.HostName, serverToken, args.HttpClient.Data); + + var auth = string.Concat(scheme, clientToken); + + // there will be an existing header from initial client request + request.Headers.SetOrAddHeaderValue(KnownHeaders.ProxyAuthorization, auth); + + // send body for final auth request + if (request.OriginalHasBody) request.ContentLength = request.Body.Length; + + args.HttpClient.Connection.IsWinAuthenticated = true; + } + + // Need to revisit this. + // Should we cache all Set-Cookie headers from server during auth process + // and send it to client after auth? + + // Let ResponseHandler send the updated request + args.ReRequest = true; + } + } + /// /// Rewrites the response body for failed authentication /// @@ -152,6 +261,7 @@ private async Task RewriteUnauthorizedResponse(SessionEventArgs args) // Strip authentication headers to avoid credentials prompt in client web browser foreach (var authHeaderName in authHeaderNames) response.Headers.RemoveHeader(authHeaderName); + foreach (var proxyAuthHeaderName in proxyAuthHeaderNames) response.Headers.RemoveHeader(proxyAuthHeaderName); // Add custom div to body to clarify that the proxy (not the client browser) failed authentication var authErrorMessage = diff --git a/src/Titanium.Web.Proxy/Network/WinAuth/Security/Common.cs b/src/Titanium.Web.Proxy/Network/WinAuth/Security/Common.cs index 2be30b8de..d4add5759 100644 --- a/src/Titanium.Web.Proxy/Network/WinAuth/Security/Common.cs +++ b/src/Titanium.Web.Proxy/Network/WinAuth/Security/Common.cs @@ -8,19 +8,12 @@ internal class Common internal static uint NewContextAttributes = 0; internal static SecurityInteger NewLifeTime = new(0); - #region Private constants + #region Internal constants - private const int IscReqReplayDetect = 0x00000004; - private const int IscReqSequenceDetect = 0x00000008; - private const int IscReqConfidentiality = 0x00000010; - private const int IscReqConnection = 0x00000800; - - #endregion - - #region internal constants - - internal const int StandardContextAttributes = - IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection; + internal const int IscReqReplayDetect = 0x00000004; + internal const int IscReqSequenceDetect = 0x00000008; + internal const int IscReqConfidentiality = 0x00000010; + internal const int IscReqConnection = 0x00000800; internal const int SecurityNativeDataRepresentation = 0x10; internal const int MaximumTokenSize = 12288; @@ -160,13 +153,13 @@ internal SecurityBuffer(byte[] secBufferBytes, SecurityBufferType bufferType) } [StructLayout(LayoutKind.Sequential)] - internal struct SecurityBufferDesciption + internal struct SecurityBufferDescription { internal int ulVersion; internal int cBuffers; internal IntPtr pBuffers; // Point to SecBuffer - internal SecurityBufferDesciption(int bufferSize) + internal SecurityBufferDescription(int bufferSize) { ulVersion = (int)SecurityBufferType.SecbufferVersion; cBuffers = 1; @@ -175,7 +168,7 @@ internal SecurityBufferDesciption(int bufferSize) Marshal.StructureToPtr(thisSecBuffer, pBuffers, false); } - internal SecurityBufferDesciption(byte[] secBufferBytes) + internal SecurityBufferDescription(byte[] secBufferBytes) { ulVersion = (int)SecurityBufferType.SecbufferVersion; cBuffers = 1; diff --git a/src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs b/src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs index 08e65439b..c34bbcb0a 100644 --- a/src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs +++ b/src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs @@ -19,15 +19,16 @@ internal class WinAuthEndPoint /// /// /// + /// /// - internal static byte[]? AcquireInitialSecurityToken(string hostname, string authScheme, InternalDataStore data) + internal static byte[]? AcquireInitialSecurityToken(string hostname, string authScheme, InternalDataStore data, int attributes) { byte[]? token; // null for initial call - var serverToken = new SecurityBufferDesciption(); + var serverToken = new SecurityBufferDescription(); - var clientToken = new SecurityBufferDesciption(MaximumTokenSize); + var clientToken = new SecurityBufferDescription(MaximumTokenSize); try { @@ -49,7 +50,7 @@ internal class WinAuthEndPoint result = InitializeSecurityContext(ref state.Credentials, IntPtr.Zero, hostname, - StandardContextAttributes, + attributes, 0, SecurityNativeDataRepresentation, ref serverToken, @@ -80,15 +81,16 @@ internal class WinAuthEndPoint /// /// /// + /// /// - internal static byte[]? AcquireFinalSecurityToken(string hostname, byte[] serverChallenge, InternalDataStore data) + internal static byte[]? AcquireFinalSecurityToken(string hostname, byte[] serverChallenge, InternalDataStore data, int attributes) { byte[]? token; // user server challenge - var serverToken = new SecurityBufferDesciption(serverChallenge); + var serverToken = new SecurityBufferDescription(serverChallenge); - var clientToken = new SecurityBufferDesciption(MaximumTokenSize); + var clientToken = new SecurityBufferDescription(MaximumTokenSize); try { @@ -99,7 +101,7 @@ internal class WinAuthEndPoint var result = InitializeSecurityContext(ref state.Credentials, ref state.Context, hostname, - StandardContextAttributes, + attributes, 0, SecurityNativeDataRepresentation, ref serverToken, @@ -123,7 +125,7 @@ internal class WinAuthEndPoint return token; } - private static void DisposeToken(SecurityBufferDesciption clientToken) + private static void DisposeToken(SecurityBufferDescription clientToken) { if (clientToken.pBuffers != IntPtr.Zero) { @@ -186,6 +188,11 @@ internal static bool ValidateWinAuthState(InternalDataStore data, State.WinAuthS (state!.AuthState == State.WinAuthState.InitialToken || state.AuthState == State.WinAuthState.Authorized); // Server may require re-authentication on an open connection + + if (expectedAuthState == State.WinAuthState.FinalToken) + return !stateExists || + (state!.AuthState == State.WinAuthState.FinalToken || + state.AuthState == State.WinAuthState.Authorized); throw new Exception("Unsupported validation of WinAuthState"); } @@ -212,24 +219,24 @@ private static extern int InitializeSecurityContext(ref SecurityHandle phCredent int fContextReq, int reserved1, int targetDataRep, - ref SecurityBufferDesciption pInput, // PSecBufferDesc SecBufferDesc + ref SecurityBufferDescription pInput, // PSecBufferDesc SecBufferDesc int reserved2, out SecurityHandle phNewContext, // PCtxtHandle - out SecurityBufferDesciption pOutput, // PSecBufferDesc SecBufferDesc + out SecurityBufferDescription pOutput, // PSecBufferDesc SecBufferDesc out uint pfContextAttr, // managed ulong == 64 bits!!! out SecurityInteger ptsExpiry); // PTimeStamp - [DllImport("secur32", CharSet = CharSet.Auto, SetLastError = true)] + [DllImport("secur32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern int InitializeSecurityContext(ref SecurityHandle phCredential, // PCredHandle ref SecurityHandle phContext, // PCtxtHandle string pszTargetName, int fContextReq, int reserved1, int targetDataRep, - ref SecurityBufferDesciption secBufferDesc, // PSecBufferDesc SecBufferDesc + ref SecurityBufferDescription secBufferDesc, // PSecBufferDesc SecBufferDesc int reserved2, out SecurityHandle phNewContext, // PCtxtHandle - out SecurityBufferDesciption pOutput, // PSecBufferDesc SecBufferDesc + out SecurityBufferDescription pOutput, // PSecBufferDesc SecBufferDesc out uint pfContextAttr, // managed ulong == 64 bits!!! out SecurityInteger ptsExpiry); // PTimeStamp diff --git a/src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs b/src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs index 1eb7e69df..eae099277 100644 --- a/src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs +++ b/src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs @@ -1,6 +1,7 @@ using System; using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Network.WinAuth.Security; +using static Titanium.Web.Proxy.Network.WinAuth.Security.Common; namespace Titanium.Web.Proxy.Network.WinAuth; @@ -21,7 +22,10 @@ internal static class WinAuthHandler /// internal static string GetInitialAuthToken(string serverHostname, string authScheme, InternalDataStore data) { - var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(serverHostname, authScheme, data); + var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(serverHostname, + authScheme, + data, + IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection); return string.Concat(" ", Convert.ToBase64String(tokenBytes)); } @@ -35,8 +39,52 @@ internal static string GetInitialAuthToken(string serverHostname, string authSch internal static string GetFinalAuthToken(string serverHostname, string serverToken, InternalDataStore data) { var tokenBytes = - WinAuthEndPoint.AcquireFinalSecurityToken(serverHostname, Convert.FromBase64String(serverToken), - data); + WinAuthEndPoint.AcquireFinalSecurityToken(serverHostname, + Convert.FromBase64String(serverToken), + data, + IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection); + + return string.Concat(" ", Convert.ToBase64String(tokenBytes)); + } + + // NTLM authentication with the proxy server works in a different way and different ISC_REQ_* flags need to be passed + // Chromium sets ISC_REQ_DELEGATE | ISC_REQ_MUTUAL_AUTH as seen in https://chromium.googlesource.com/chromium/src/net/+/b8c947c21ffb46f616ece1948ba0545e671cf23e/http/http_auth_sspi_win.cc#546 + // cURL uses no flags since commit https://github.com/curl/curl/commit/8ee182288af1bd828613fdcab2e7e8b551e91901 (now moved in lib/vauth/ntlm_sspi.c) + // .NET since 6.0.4 has chosen ISC_REQ_CONNECTION https://github.com/dotnet/runtime/pull/66305/files + // CNTLM (the new maintained version) instead passes ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION https://github.com/versat/cntlm/blob/d6a47bb5c2489503e3d97e52685b8dc10300da96/sspi.c#L239 + // This is Microsoft documentation for the InitializeSecurityContext function https://learn.microsoft.com/en-us/windows/win32/secauthn/initializesecuritycontext--general + // The whole thing seems pretty randomic... + + /// + /// Get the initial client token for proxy server + /// using credentials of user running the proxy server process + /// + /// + /// + /// + /// + internal static string GetInitialProxyAuthToken(string proxyHostname, string authScheme, InternalDataStore data) + { + var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(proxyHostname, + authScheme, + data, + 0); + return string.Concat(" ", Convert.ToBase64String(tokenBytes)); + } + + /// + /// Get the final token given the proxy server challenge token + /// + /// + /// + /// + /// + internal static string GetFinalProxyAuthToken(string proxyHostname, string serverToken, InternalDataStore data) + { + var tokenBytes = WinAuthEndPoint.AcquireFinalSecurityToken(proxyHostname, + Convert.FromBase64String(serverToken), + data, + 0); return string.Concat(" ", Convert.ToBase64String(tokenBytes)); } diff --git a/src/Titanium.Web.Proxy/RequestHandler.cs b/src/Titanium.Web.Proxy/RequestHandler.cs index beaf2f9e9..0aa0ddc44 100644 --- a/src/Titanium.Web.Proxy/RequestHandler.cs +++ b/src/Titanium.Web.Proxy/RequestHandler.cs @@ -372,7 +372,7 @@ private async Task OnBeforeRequest(SessionEventArgs args) /// /// Invoke before request handler if it is set. /// - /// The COONECT request. + /// The CONNECT request. /// internal async Task OnBeforeUpStreamConnectRequest(ConnectRequest request) { diff --git a/src/Titanium.Web.Proxy/ResponseHandler.cs b/src/Titanium.Web.Proxy/ResponseHandler.cs index bd460b3d9..509d8837b 100644 --- a/src/Titanium.Web.Proxy/ResponseHandler.cs +++ b/src/Titanium.Web.Proxy/ResponseHandler.cs @@ -42,6 +42,8 @@ private async Task HandleHttpSessionResponse(SessionEventArgs args) { if (response.StatusCode == (int)HttpStatusCode.Unauthorized) await Handle401UnAuthorized(args); + else if (response.StatusCode == (int)HttpStatusCode.ProxyAuthenticationRequired) + await Handle407ProxyAuthorization(args); else WinAuthEndPoint.AuthenticatedResponse(args.HttpClient.Data); } @@ -76,11 +78,16 @@ private async Task HandleHttpSessionResponse(SessionEventArgs args) // likely after making modifications from User Response Handler if (args.ReRequest) { - if (args.HttpClient.HasConnection) await TcpConnectionFactory.Release(args.HttpClient.Connection); + var serverConnection = args.HttpClient.Connection; + if (args.HttpClient.HasConnection && response.StatusCode != (int)HttpStatusCode.ProxyAuthenticationRequired) + { + serverConnection = null; + await TcpConnectionFactory.Release(args.HttpClient.Connection); + } // clear current response await args.ClearResponse(cancellationToken); - var result = await HandleHttpSessionRequest(args, null, args.ClientConnection.NegotiatedApplicationProtocol, + var result = await HandleHttpSessionRequest(args, serverConnection, args.ClientConnection.NegotiatedApplicationProtocol, cancellationToken, args.CancellationTokenSource); if (result.LatestConnection != null) args.HttpClient.SetConnection(result.LatestConnection);