Skip to content
This repository was archived by the owner on Jul 9, 2023. It is now read-only.

Commit 96d9775

Browse files
authored
Merge pull request #399 from justcoding121/develop
merge to beta
2 parents ad78422 + 9fb2b18 commit 96d9775

File tree

5 files changed

+133
-10
lines changed

5 files changed

+133
-10
lines changed

Titanium.Web.Proxy/Extensions/StringExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@ internal static bool ContainsIgnoreCase(this string str, string value)
1414
{
1515
return CultureInfo.CurrentCulture.CompareInfo.IndexOf(str, value, CompareOptions.IgnoreCase) >= 0;
1616
}
17+
18+
internal static int IndexOfIgnoreCase(this string str, string value)
19+
{
20+
return CultureInfo.CurrentCulture.CompareInfo.IndexOf(str, value, CompareOptions.IgnoreCase);
21+
}
1722
}
1823
}

Titanium.Web.Proxy/Network/WinAuth/Security/State.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,24 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
77
/// </summary>
88
internal class State
99
{
10+
/// <summary>
11+
/// States during Windows Authentication
12+
/// </summary>
13+
public enum WinAuthState
14+
{
15+
UNAUTHORIZED,
16+
INITIAL_TOKEN,
17+
FINAL_TOKEN,
18+
AUTHORIZED
19+
};
20+
1021
internal State()
1122
{
1223
Credentials = new Common.SecurityHandle(0);
1324
Context = new Common.SecurityHandle(0);
1425

1526
LastSeen = DateTime.Now;
27+
AuthState = WinAuthState.UNAUTHORIZED;
1628
}
1729

1830
/// <summary>
@@ -30,10 +42,16 @@ internal State()
3042
/// </summary>
3143
internal DateTime LastSeen;
3244

45+
/// <summary>
46+
/// Current state of the authentication process
47+
/// </summary>
48+
internal WinAuthState AuthState;
49+
3350
internal void ResetHandles()
3451
{
3552
Credentials.Reset();
3653
Context.Reset();
54+
AuthState = WinAuthState.UNAUTHORIZED;
3755
}
3856

3957
internal void UpdatePresence()

Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ internal static byte[] AcquireInitialSecurityToken(string hostname, string authS
3838

3939
try
4040
{
41-
int result;
42-
4341
var state = new State();
4442

45-
result = AcquireCredentialsHandle(
43+
int result = AcquireCredentialsHandle(
4644
WindowsIdentity.GetCurrent().Name,
4745
authScheme,
4846
SecurityCredentialsOutbound,
@@ -79,6 +77,7 @@ internal static byte[] AcquireInitialSecurityToken(string hostname, string authS
7977
return null;
8078
}
8179

80+
state.AuthState = State.WinAuthState.INITIAL_TOKEN;
8281
token = clientToken.GetBytes();
8382
authStates.Add(requestId, state);
8483
}
@@ -109,13 +108,11 @@ internal static byte[] AcquireFinalSecurityToken(string hostname, byte[] serverC
109108

110109
try
111110
{
112-
int result;
113-
114111
var state = authStates[requestId];
115112

116113
state.UpdatePresence();
117114

118-
result = InitializeSecurityContext(ref state.Credentials,
115+
int result = InitializeSecurityContext(ref state.Credentials,
119116
ref state.Context,
120117
hostname,
121118
StandardContextAttributes,
@@ -135,7 +132,7 @@ internal static byte[] AcquireFinalSecurityToken(string hostname, byte[] serverC
135132
return null;
136133
}
137134

138-
authStates.Remove(requestId);
135+
state.AuthState = State.WinAuthState.FINAL_TOKEN;
139136
token = clientToken.GetBytes();
140137
}
141138
finally
@@ -166,6 +163,49 @@ internal static async void ClearIdleStates(int stateCacheTimeOutMinutes)
166163
await Task.Delay(1000 * 60);
167164
}
168165

166+
/// <summary>
167+
/// Validates that the current WinAuth state of the connection matches the
168+
/// expectation, used to detect failed authentication
169+
/// </summary>
170+
/// <param name="requestId"></param>
171+
/// <param name="expectedAuthState"></param>
172+
/// <returns></returns>
173+
internal static bool ValidateWinAuthState(Guid requestId, State.WinAuthState expectedAuthState)
174+
{
175+
var stateExists = authStates.TryGetValue(requestId, out var state);
176+
177+
if (expectedAuthState == State.WinAuthState.UNAUTHORIZED)
178+
{
179+
// Validation before initial token
180+
return stateExists == false ||
181+
state.AuthState == State.WinAuthState.UNAUTHORIZED ||
182+
state.AuthState == State.WinAuthState.AUTHORIZED; // Server may require re-authentication on an open connection
183+
}
184+
185+
if (expectedAuthState == State.WinAuthState.INITIAL_TOKEN)
186+
{
187+
// Validation before final token
188+
return stateExists &&
189+
(state.AuthState == State.WinAuthState.INITIAL_TOKEN ||
190+
state.AuthState == State.WinAuthState.AUTHORIZED); // Server may require re-authentication on an open connection
191+
}
192+
193+
throw new Exception("Unsupported validation of WinAuthState");
194+
}
195+
196+
/// <summary>
197+
/// Set the AuthState to authorized and update the connection state lifetime
198+
/// </summary>
199+
/// <param name="requestId"></param>
200+
internal static void AuthenticatedResponse(Guid requestId)
201+
{
202+
if (authStates.TryGetValue(requestId, out var state))
203+
{
204+
state.AuthState = State.WinAuthState.AUTHORIZED;
205+
state.UpdatePresence();
206+
}
207+
}
208+
169209
#region Native calls to secur32.dll
170210

171211
[DllImport("secur32.dll", SetLastError = true)]

Titanium.Web.Proxy/ResponseHandler.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Titanium.Web.Proxy.EventArguments;
88
using Titanium.Web.Proxy.Exceptions;
99
using Titanium.Web.Proxy.Extensions;
10+
using Titanium.Web.Proxy.Network.WinAuth.Security;
1011

1112
namespace Titanium.Web.Proxy
1213
{
@@ -31,9 +32,16 @@ private async Task HandleHttpSessionResponse(SessionEventArgs args)
3132
args.ReRequest = false;
3233

3334
//check for windows authentication
34-
if (isWindowsAuthenticationEnabledAndSupported && response.StatusCode == (int)HttpStatusCode.Unauthorized)
35+
if (isWindowsAuthenticationEnabledAndSupported)
3536
{
36-
await Handle401UnAuthorized(args);
37+
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
38+
{
39+
await Handle401UnAuthorized(args);
40+
}
41+
else
42+
{
43+
WinAuthEndPoint.AuthenticatedResponse(args.WebSession.RequestId);
44+
}
3745
}
3846

3947
response.OriginalHasBody = response.HasBody;

Titanium.Web.Proxy/WinAuthHandler.cs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
using System.Linq;
44
using System.Threading.Tasks;
55
using Titanium.Web.Proxy.EventArguments;
6+
using Titanium.Web.Proxy.Extensions;
67
using Titanium.Web.Proxy.Http;
78
using Titanium.Web.Proxy.Models;
89
using Titanium.Web.Proxy.Network.WinAuth;
10+
using Titanium.Web.Proxy.Network.WinAuth.Security;
911

1012
namespace Titanium.Web.Proxy
1113
{
@@ -84,6 +86,15 @@ internal async Task Handle401UnAuthorized(SessionEventArgs args)
8486
{
8587
string scheme = authSchemes.FirstOrDefault(x => authHeader.Value.Equals(x, StringComparison.OrdinalIgnoreCase));
8688

89+
var expectedAuthState = scheme == null ? State.WinAuthState.INITIAL_TOKEN : State.WinAuthState.UNAUTHORIZED;
90+
91+
if (!WinAuthEndPoint.ValidateWinAuthState(args.WebSession.RequestId, expectedAuthState))
92+
{
93+
// Invalid state, create proper error message to client
94+
await RewriteUnauthorizedResponse(args);
95+
return;
96+
}
97+
8798
var request = args.WebSession.Request;
8899

89100
//clear any existing headers to avoid confusing bad servers
@@ -108,7 +119,7 @@ internal async Task Handle401UnAuthorized(SessionEventArgs args)
108119
//challenge value will start with any of the scheme selected
109120
else
110121
{
111-
scheme = authSchemes.FirstOrDefault(x => authHeader.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase) &&
122+
scheme = authSchemes.First(x => authHeader.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase) &&
112123
authHeader.Value.Length > x.Length + 1);
113124

114125
string serverToken = authHeader.Value.Substring(scheme.Length + 1);
@@ -134,5 +145,46 @@ internal async Task Handle401UnAuthorized(SessionEventArgs args)
134145
args.ReRequest = true;
135146
}
136147
}
148+
149+
/// <summary>
150+
/// Rewrites the response body for failed authentication
151+
/// </summary>
152+
/// <param name="args"></param>
153+
/// <returns></returns>
154+
internal async Task RewriteUnauthorizedResponse(SessionEventArgs args)
155+
{
156+
var response = args.WebSession.Response;
157+
158+
// Strip authentication headers to avoid credentials prompt in client web browser
159+
foreach (var authHeaderName in authHeaderNames)
160+
{
161+
response.Headers.RemoveHeader(authHeaderName);
162+
}
163+
164+
// Add custom div to body to clarify that the proxy (not the client browser) failed authentication
165+
string authErrorMessage = "<div class=\"inserted-by-proxy\"><h2>NTLM authentication through Titanium.Web.Proxy (" +
166+
args.ProxyClient.TcpClient.Client.LocalEndPoint +
167+
") failed. Please check credentials.</h2></div>";
168+
string originalErrorMessage = "<div class=\"inserted-by-proxy\"><h3>Response from remote web server below.</h3></div><br/>";
169+
string body = await args.GetResponseBodyAsString();
170+
int idx = body.IndexOfIgnoreCase("<body>");
171+
if (idx >= 0)
172+
{
173+
var bodyPos = idx + "<body>".Length;
174+
body = body.Insert(bodyPos, authErrorMessage + originalErrorMessage);
175+
}
176+
else
177+
{
178+
// Cannot parse response body, replace it
179+
body = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" +
180+
"<html xmlns=\"http://www.w3.org/1999/xhtml\">" +
181+
"<body>" +
182+
authErrorMessage +
183+
"</body>" +
184+
"</html>";
185+
}
186+
187+
args.SetResponseBodyString(body);
188+
}
137189
}
138190
}

0 commit comments

Comments
 (0)