Commit ea86d5a7 authored by Honfika's avatar Honfika

TunnelConnect event added to ProxyServer

common HeaderCollection class added and used in Request and Resposne classes
parent f6a32641
......@@ -43,6 +43,8 @@ namespace Titanium.Web.Proxy.Examples.Basic
{
proxyServer.BeforeRequest += OnRequest;
proxyServer.BeforeResponse += OnResponse;
proxyServer.TunnelConnectRequest += OnTunnelConnectRequest;
proxyServer.TunnelConnectResponse += OnTunnelConnectResponse;
proxyServer.ServerCertificateValidationCallback += OnCertificateValidation;
proxyServer.ClientCertificateSelectionCallback += OnCertificateSelection;
......@@ -105,6 +107,8 @@ namespace Titanium.Web.Proxy.Examples.Basic
public void Stop()
{
proxyServer.TunnelConnectRequest -= OnTunnelConnectRequest;
proxyServer.TunnelConnectResponse -= OnTunnelConnectResponse;
proxyServer.BeforeRequest -= OnRequest;
proxyServer.BeforeResponse -= OnResponse;
proxyServer.ServerCertificateValidationCallback -= OnCertificateValidation;
......@@ -116,6 +120,15 @@ namespace Titanium.Web.Proxy.Examples.Basic
//proxyServer.CertificateManager.RemoveTrustedRootCertificates();
}
private async Task OnTunnelConnectRequest(object sender, TunnelConnectEventArgs e)
{
Console.WriteLine("Tunnel to: " + e.ConnectRequest.Host);
}
private async Task OnTunnelConnectResponse(object sender, TunnelConnectEventArgs e)
{
}
//intecept & cancel redirect or update requests
public async Task OnRequest(object sender, SessionEventArgs e)
{
......
......@@ -434,7 +434,10 @@ namespace Titanium.Web.Proxy.EventArguments
if (headers != null && headers.Count > 0)
{
response.ResponseHeaders = headers;
foreach (var header in headers)
{
response.ResponseHeaders.AddHeader(header.Key, header.Value.Value);
}
}
response.HttpVersion = WebSession.Request.HttpVersion;
......@@ -501,7 +504,10 @@ namespace Titanium.Web.Proxy.EventArguments
if (headers != null && headers.Count > 0)
{
response.ResponseHeaders = headers;
foreach (var header in headers)
{
response.ResponseHeaders.AddHeader(header.Key, header.Value.Value);
}
}
response.HttpVersion = WebSession.Request.HttpVersion;
......@@ -523,7 +529,7 @@ namespace Titanium.Web.Proxy.EventArguments
var response = new RedirectResponse();
response.HttpVersion = WebSession.Request.HttpVersion;
response.ResponseHeaders.Add("Location", new HttpHeader("Location", url));
response.ResponseHeaders.AddHeader("Location", url);
response.ResponseBody = Encoding.ASCII.GetBytes(string.Empty);
await Respond(response);
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Http;
namespace Titanium.Web.Proxy.EventArguments
{
public class TunnelConnectEventArgs : EventArgs
{
public bool IsHttps { get; set; }
public Request ConnectRequest { get; set; }
public Request ConnectResponse { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Http
{
public class ConnectRequest : Request
{
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.Http
{
public class ConnectResponse : Response
{
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Http
{
public class HeaderCollection : IEnumerable<HttpHeader>
{
/// <summary>
/// Unique Request header collection
/// </summary>
public Dictionary<string, HttpHeader> Headers { get; set; }
/// <summary>
/// Non Unique headers
/// </summary>
public Dictionary<string, List<HttpHeader>> NonUniqueHeaders { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="HeaderCollection"/> class.
/// </summary>
public HeaderCollection()
{
Headers = new Dictionary<string, HttpHeader>(StringComparer.OrdinalIgnoreCase);
NonUniqueHeaders = new Dictionary<string, List<HttpHeader>>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// True if header exists
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public bool HeaderExists(string name)
{
return Headers.ContainsKey(name) || NonUniqueHeaders.ContainsKey(name);
}
/// <summary>
/// Returns all headers with given name if exists
/// Returns null if does'nt exist
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public List<HttpHeader> GetHeaders(string name)
{
if (Headers.ContainsKey(name))
{
return new List<HttpHeader>
{
Headers[name]
};
}
if (NonUniqueHeaders.ContainsKey(name))
{
return new List<HttpHeader>(NonUniqueHeaders[name]);
}
return null;
}
/// <summary>
/// Returns all headers
/// </summary>
/// <returns></returns>
public List<HttpHeader> GetAllHeaders()
{
var result = new List<HttpHeader>();
result.AddRange(Headers.Select(x => x.Value));
result.AddRange(NonUniqueHeaders.SelectMany(x => x.Value));
return result;
}
/// <summary>
/// Add a new header with given name and value
/// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
public void AddHeader(string name, string value)
{
AddHeader(new HttpHeader(name, value));
}
/// <summary>
/// Adds the given header object to Request
/// </summary>
/// <param name="newHeader"></param>
public void AddHeader(HttpHeader newHeader)
{
if (NonUniqueHeaders.ContainsKey(newHeader.Name))
{
NonUniqueHeaders[newHeader.Name].Add(newHeader);
return;
}
if (Headers.ContainsKey(newHeader.Name))
{
var existing = Headers[newHeader.Name];
Headers.Remove(newHeader.Name);
NonUniqueHeaders.Add(newHeader.Name, new List<HttpHeader>
{
existing,
newHeader
});
}
else
{
Headers.Add(newHeader.Name, newHeader);
}
}
/// <summary>
/// removes all headers with given name
/// </summary>
/// <param name="headerName"></param>
/// <returns>True if header was removed
/// False if no header exists with given name</returns>
public bool RemoveHeader(string headerName)
{
bool result = Headers.Remove(headerName);
// do not convert to '||' expression to avoid lazy evaluation
if (NonUniqueHeaders.Remove(headerName))
{
result = true;
}
return result;
}
/// <summary>
/// Removes given header object if it exist
/// </summary>
/// <param name="header">Returns true if header exists and was removed </param>
public bool RemoveHeader(HttpHeader header)
{
if (Headers.ContainsKey(header.Name))
{
if (Headers[header.Name].Equals(header))
{
Headers.Remove(header.Name);
return true;
}
}
else if (NonUniqueHeaders.ContainsKey(header.Name))
{
if (NonUniqueHeaders[header.Name].RemoveAll(x => x.Equals(header)) > 0)
{
return true;
}
}
return false;
}
internal string GetHeaderValueOrNull(string headerName)
{
HttpHeader header;
if (Headers.TryGetValue(headerName, out header))
{
return header.Value;
}
return null;
}
internal string SetOrAddHeaderValue(string headerName, string value)
{
HttpHeader header;
if (Headers.TryGetValue(headerName, out header))
{
header.Value = value;
}
else
{
Headers.Add(headerName, new HttpHeader(headerName, value));
}
return null;
}
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<HttpHeader> GetEnumerator()
{
return Headers.Values.Concat(NonUniqueHeaders.Values.SelectMany(x => x)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
......@@ -8,9 +8,11 @@ namespace Titanium.Web.Proxy.Http
{
internal static class HeaderParser
{
internal static async Task ReadHeaders(CustomBinaryReader reader, Dictionary<string, List<HttpHeader>> nonUniqueResponseHeaders,
Dictionary<string, HttpHeader> headers)
internal static async Task ReadHeaders(CustomBinaryReader reader, HeaderCollection headerCollection)
{
var nonUniqueResponseHeaders = headerCollection.NonUniqueHeaders;
var headers = headerCollection.Headers;
string tmpLine;
while (!string.IsNullOrEmpty(tmpLine = await reader.ReadLineAsync()))
{
......
......@@ -27,7 +27,7 @@ namespace Titanium.Web.Proxy.Http
/// <summary>
/// Headers passed with Connect.
/// </summary>
public List<HttpHeader> ConnectHeaders { get; set; }
public ConnectRequest ConnectRequest { get; set; }
/// <summary>
/// Web Request.
......@@ -94,28 +94,14 @@ namespace Titanium.Web.Proxy.Http
$"{ServerConnection.UpStreamHttpProxy.UserName}:{ServerConnection.UpStreamHttpProxy.Password}")));
}
//write request headers
foreach (var headerItem in Request.RequestHeaders)
foreach (var header in Request.RequestHeaders)
{
var header = headerItem.Value;
if (headerItem.Key != "Proxy-Authorization")
if (header.Name != "Proxy-Authorization")
{
requestLines.AppendLine($"{header.Name}: {header.Value}");
}
}
//write non unique request headers
foreach (var headerItem in Request.NonUniqueRequestHeaders)
{
var headers = headerItem.Value;
foreach (var header in headers)
{
if (headerItem.Key != "Proxy-Authorization")
{
requestLines.AppendLine($"{header.Name}: {header.Value}");
}
}
}
requestLines.AppendLine();
string request = requestLines.ToString();
......@@ -211,7 +197,7 @@ namespace Titanium.Web.Proxy.Http
}
//Read the response headers in to unique and non-unique header collections
await HeaderParser.ReadHeaders(ServerConnection.StreamReader, Response.NonUniqueResponseHeaders, Response.ResponseHeaders);
await HeaderParser.ReadHeaders(ServerConnection.StreamReader, Response.ResponseHeaders);
}
/// <summary>
......@@ -219,7 +205,7 @@ namespace Titanium.Web.Proxy.Http
/// </summary>
public void Dispose()
{
ConnectHeaders = null;
ConnectRequest = null;
Request.Dispose();
Response.Dispose();
......
This diff is collapsed.
This diff is collapsed.
......@@ -12,14 +12,14 @@ namespace Titanium.Web.Proxy
{
public partial class ProxyServer
{
private async Task<bool> CheckAuthorization(StreamWriter clientStreamWriter, IEnumerable<HttpHeader> headers)
private async Task<bool> CheckAuthorization(StreamWriter clientStreamWriter, Request request)
{
if (AuthenticateUserFunc == null)
{
return true;
}
var httpHeaders = headers as ICollection<HttpHeader> ?? headers.ToArray();
var httpHeaders = request.RequestHeaders.ToArray();
try
{
......@@ -67,14 +67,10 @@ namespace Titanium.Web.Proxy
private async Task SendAuthentication407Response(StreamWriter clientStreamWriter, string description)
{
await WriteResponseStatus(HttpHeader.Version11, "407", description, clientStreamWriter);
var response = new Response
{
ResponseHeaders = new Dictionary<string, HttpHeader>
{
{ "Proxy-Authenticate", new HttpHeader("Proxy-Authenticate", "Basic realm=\"TitaniumProxy\"") },
{ "Proxy-Connection", new HttpHeader("Proxy-Connection", "close") }
}
};
var response = new Response();
response.ResponseHeaders.AddHeader("Proxy-Authenticate", "Basic realm=\"TitaniumProxy\"");
response.ResponseHeaders.AddHeader("Proxy-Connection", "close");
await WriteResponseHeaders(clientStreamWriter, response);
await clientStreamWriter.WriteLineAsync();
......
......@@ -172,6 +172,10 @@ namespace Titanium.Web.Proxy
/// </summary>
public event Func<object, SessionEventArgs, Task> BeforeResponse;
public event Func<object, TunnelConnectEventArgs, Task> TunnelConnectRequest;
public event Func<object, TunnelConnectEventArgs, Task> TunnelConnectResponse;
/// <summary>
/// External proxy for Http
/// </summary>
......
......@@ -88,20 +88,13 @@ namespace Titanium.Web.Proxy
excluded = !endPoint.IncludedHttpsHostNameRegexList.Any(x => x.IsMatch(httpRemoteUri.Host));
}
List<HttpHeader> connectRequestHeaders = null;
ConnectRequest connectRequest = null;
//Client wants to create a secure tcp tunnel (probably its a HTTPS or Websocket request)
if (httpVerb == "CONNECT")
{
connectRequestHeaders = new List<HttpHeader>();
string tmpLine;
while (!string.IsNullOrEmpty(tmpLine = await clientStreamReader.ReadLineAsync()))
{
var header = tmpLine.Split(ProxyConstants.ColonSplit, 2);
var newHeader = new HttpHeader(header[0], header[1]);
connectRequestHeaders.Add(newHeader);
}
connectRequest = new ConnectRequest();
await HeaderParser.ReadHeaders(clientStreamReader, connectRequest.RequestHeaders);
if (httpRemoteUri.Port == 80)
{
......@@ -109,7 +102,7 @@ namespace Titanium.Web.Proxy
excluded = true;
}
if (!excluded && await CheckAuthorization(clientStreamWriter, connectRequestHeaders) == false)
if (!excluded && await CheckAuthorization(clientStreamWriter, connectRequest) == false)
{
return;
}
......@@ -117,12 +110,17 @@ namespace Titanium.Web.Proxy
//write back successfull CONNECT response
await WriteConnectResponse(clientStreamWriter, version);
if (!excluded && !await HttpsTools.IsClientHello(clientStream))
bool isClientHello = await HttpsTools.IsClientHello(clientStream);
if (TunnelConnectRequest != null)
{
excluded = true;
var args = new TunnelConnectEventArgs();
args.IsHttps = isClientHello;
args.ConnectRequest = connectRequest;
await TunnelConnectRequest.InvokeParallelAsync(this, args);
}
if (!excluded)
if (!excluded && isClientHello)
{
httpRemoteUri = new Uri("https://" + httpCmdSplit[1]);
......@@ -188,7 +186,7 @@ namespace Titanium.Web.Proxy
//Now create the request
disposed = await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
httpRemoteUri.Scheme == Uri.UriSchemeHttps ? httpRemoteUri.Host : null, endPoint, connectRequestHeaders);
httpRemoteUri.Scheme == Uri.UriSchemeHttps ? httpRemoteUri.Host : null, endPoint, connectRequest);
}
catch (Exception e)
{
......@@ -268,11 +266,11 @@ namespace Titanium.Web.Proxy
/// <param name="clientStreamWriter"></param>
/// <param name="httpsConnectHostname"></param>
/// <param name="endPoint"></param>
/// <param name="connectHeaders"></param>
/// <param name="connectRequest"></param>
/// <returns></returns>
private async Task<bool> HandleHttpSessionRequest(TcpClient client, string httpCmd, Stream clientStream,
CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, string httpsConnectHostname,
ProxyEndPoint endPoint, List<HttpHeader> connectHeaders)
ProxyEndPoint endPoint, ConnectRequest connectRequest)
{
bool disposed = false;
......@@ -290,7 +288,7 @@ namespace Titanium.Web.Proxy
var args = new SessionEventArgs(BufferSize, HandleHttpSessionResponse)
{
ProxyClient = { TcpClient = client },
WebSession = { ConnectHeaders = connectHeaders }
WebSession = { ConnectRequest = connectRequest }
};
args.WebSession.ProcessId = new Lazy<int>(() =>
......@@ -327,7 +325,7 @@ namespace Titanium.Web.Proxy
}
//Read the request headers in to unique and non-unique header collections
await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.NonUniqueRequestHeaders, args.WebSession.Request.RequestHeaders);
await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.RequestHeaders);
var httpRemoteUri = new Uri(httpsConnectHostname == null
? httpCmdSplit[1]
......@@ -342,7 +340,7 @@ namespace Titanium.Web.Proxy
args.ProxyClient.ClientStreamWriter = clientStreamWriter;
//proxy authorization check
if (httpsConnectHostname == null && await CheckAuthorization(clientStreamWriter, args.WebSession.Request.RequestHeaders.Values) == false)
if (httpsConnectHostname == null && await CheckAuthorization(clientStreamWriter, args.WebSession.Request) == false)
{
args.Dispose();
break;
......@@ -386,7 +384,7 @@ namespace Titanium.Web.Proxy
//if upgrading to websocket then relay the requet without reading the contents
if (args.WebSession.Request.UpgradeToWebSocket)
{
await TcpHelper.SendRaw(httpCmd, args.WebSession.Request.RequestHeaders.Values, clientStream, connection);
await TcpHelper.SendRaw(httpCmd, args.WebSession.Request.RequestHeaders, clientStream, connection);
args.Dispose();
break;
......@@ -604,12 +602,10 @@ namespace Titanium.Web.Proxy
/// </summary>
/// <param name="requestHeaders"></param>
/// <param name="webRequest"></param>
private void PrepareRequestHeaders(Dictionary<string, HttpHeader> requestHeaders, HttpWebClient webRequest)
private void PrepareRequestHeaders(HeaderCollection requestHeaders, HttpWebClient webRequest)
{
foreach (var headerItem in requestHeaders)
foreach (var header in requestHeaders)
{
var header = headerItem.Value;
switch (header.Name.ToLower())
{
//these are the only encoding this proxy can read
......
......@@ -174,17 +174,7 @@ namespace Titanium.Web.Proxy
foreach (var header in response.ResponseHeaders)
{
await header.Value.WriteToStream(responseWriter);
}
//write non unique request headers
foreach (var headerItem in response.NonUniqueResponseHeaders)
{
var headers = headerItem.Value;
foreach (var header in headers)
{
await header.WriteToStream(responseWriter);
}
await header.WriteToStream(responseWriter);
}
await responseWriter.WriteLineAsync();
......@@ -195,26 +185,15 @@ namespace Titanium.Web.Proxy
/// Fix proxy specific headers
/// </summary>
/// <param name="headers"></param>
private void FixProxyHeaders(Dictionary<string, HttpHeader> headers)
private void FixProxyHeaders(HeaderCollection headers)
{
//If proxy-connection close was returned inform to close the connection
bool hasProxyHeader = headers.ContainsKey("proxy-connection");
bool hasConnectionheader = headers.ContainsKey("connection");
string proxyHeader = headers.GetHeaderValueOrNull("proxy-connection");
headers.RemoveHeader("proxy-connection");
if (hasProxyHeader)
if (proxyHeader != null)
{
var proxyHeader = headers["proxy-connection"];
if (hasConnectionheader == false)
{
headers.Add("connection", new HttpHeader("connection", proxyHeader.Value));
}
else
{
var connectionHeader = headers["connection"];
connectionHeader.Value = proxyHeader.Value;
}
headers.Remove("proxy-connection");
headers.SetOrAddHeaderValue("connection", proxyHeader);
}
}
......
......@@ -73,6 +73,7 @@
<Compile Include="Decompression\IDecompression.cs" />
<Compile Include="EventArguments\CertificateSelectionEventArgs.cs" />
<Compile Include="EventArguments\CertificateValidationEventArgs.cs" />
<Compile Include="EventArguments\TunnelConnectEventArgs.cs" />
<Compile Include="Extensions\ByteArrayExtensions.cs" />
<Compile Include="Extensions\FuncExtensions.cs" />
<Compile Include="Helpers\HttpHelper.cs" />
......@@ -85,6 +86,9 @@
<Compile Include="Helpers\RunTime.cs" />
<Compile Include="Helpers\WinHttp\WinHttpHandle.cs" />
<Compile Include="Helpers\WinHttp\WinHttpWebProxyFinder.cs" />
<Compile Include="Http\ConnectRequest.cs" />
<Compile Include="Http\ConnectResponse.cs" />
<Compile Include="Http\HeaderCollection.cs" />
<Compile Include="Http\HeaderParser.cs" />
<Compile Include="Http\HttpsTools.cs" />
<Compile Include="Http\Responses\GenericResponse.cs" />
......
......@@ -44,7 +44,7 @@ namespace Titanium.Web.Proxy
//check in non-unique headers first
var header =
args.WebSession.Response.NonUniqueResponseHeaders.FirstOrDefault(
args.WebSession.Response.ResponseHeaders.NonUniqueHeaders.FirstOrDefault(
x => authHeaderNames.Any(y => x.Key.Equals(y, StringComparison.OrdinalIgnoreCase)));
if (!header.Equals(new KeyValuePair<string, List<HttpHeader>>()))
......@@ -54,7 +54,7 @@ namespace Titanium.Web.Proxy
if (headerName != null)
{
authHeader = args.WebSession.Response.NonUniqueResponseHeaders[headerName]
authHeader = args.WebSession.Response.ResponseHeaders.NonUniqueHeaders[headerName]
.FirstOrDefault(x => authSchemes.Any(y => x.Value.StartsWith(y, StringComparison.OrdinalIgnoreCase)));
}
......@@ -63,7 +63,7 @@ namespace Titanium.Web.Proxy
{
//check in non-unique headers first
var uHeader =
args.WebSession.Response.ResponseHeaders.FirstOrDefault(x => authHeaderNames.Any(y => x.Key.Equals(y, StringComparison.OrdinalIgnoreCase)));
args.WebSession.Response.ResponseHeaders.Headers.FirstOrDefault(x => authHeaderNames.Any(y => x.Key.Equals(y, StringComparison.OrdinalIgnoreCase)));
if (!uHeader.Equals(new KeyValuePair<string, HttpHeader>()))
{
......@@ -72,9 +72,9 @@ namespace Titanium.Web.Proxy
if (headerName != null)
{
authHeader = authSchemes.Any(x => args.WebSession.Response.ResponseHeaders[headerName].Value
authHeader = authSchemes.Any(x => args.WebSession.Response.ResponseHeaders.Headers[headerName].Value
.StartsWith(x, StringComparison.OrdinalIgnoreCase))
? args.WebSession.Response.ResponseHeaders[headerName]
? args.WebSession.Response.ResponseHeaders.Headers[headerName]
: null;
}
}
......@@ -84,9 +84,9 @@ namespace Titanium.Web.Proxy
string scheme = authSchemes.FirstOrDefault(x => authHeader.Value.Equals(x, StringComparison.OrdinalIgnoreCase));
//clear any existing headers to avoid confusing bad servers
if (args.WebSession.Request.NonUniqueRequestHeaders.ContainsKey("Authorization"))
if (args.WebSession.Request.RequestHeaders.NonUniqueHeaders.ContainsKey("Authorization"))
{
args.WebSession.Request.NonUniqueRequestHeaders.Remove("Authorization");
args.WebSession.Request.RequestHeaders.NonUniqueHeaders.Remove("Authorization");
}
//initial value will match exactly any of the schemes
......@@ -97,13 +97,13 @@ namespace Titanium.Web.Proxy
var auth = new HttpHeader("Authorization", string.Concat(scheme, clientToken));
//replace existing authorization header if any
if (args.WebSession.Request.RequestHeaders.ContainsKey("Authorization"))
if (args.WebSession.Request.RequestHeaders.Headers.ContainsKey("Authorization"))
{
args.WebSession.Request.RequestHeaders["Authorization"] = auth;
args.WebSession.Request.RequestHeaders.Headers["Authorization"] = auth;
}
else
{
args.WebSession.Request.RequestHeaders.Add("Authorization", auth);
args.WebSession.Request.RequestHeaders.Headers.Add("Authorization", auth);
}
//don't need to send body for Authorization request
......@@ -122,7 +122,7 @@ namespace Titanium.Web.Proxy
string clientToken = WinAuthHandler.GetFinalAuthToken(args.WebSession.Request.Host, serverToken, args.Id);
//there will be an existing header from initial client request
args.WebSession.Request.RequestHeaders["Authorization"] = new HttpHeader("Authorization", string.Concat(scheme, clientToken));
args.WebSession.Request.RequestHeaders.Headers["Authorization"] = new HttpHeader("Authorization", string.Concat(scheme, clientToken));
//send body for final auth request
if (args.WebSession.Request.HasBody)
......
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