diff --git a/.build/default.ps1 b/.build/default.ps1 index fe8d6955b..441263257 100644 --- a/.build/default.ps1 +++ b/.build/default.ps1 @@ -23,6 +23,10 @@ if(!$Configuration) { $Configuration = "Release" } if(!$Version) { $Version = $env:APPVEYOR_BUILD_VERSION } if(!$Version) { $Version = "1.0.$BuildNumber" } +if(!$Branch) { $Branch = $env:APPVEYOR_REPO_BRANCH } +if(!$Branch) { $Branch = "local" } +if($Branch -eq "release" ) { $Version = "$Version-beta" } + Import-Module "$Here\Common" -DisableNameChecking $NuGet = Join-Path $SolutionRoot ".nuget\nuget.exe" diff --git a/README.md b/README.md index 25f2027cb..e6f6146c8 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,12 @@ Sample request and response event handlers //Test On Request, intercept requests public void OnRequest(object sender, SessionEventArgs e) { - Console.WriteLine(e.RequestURL); - + Console.WriteLine(e.ProxySession.Request.RequestUrl); + //read request headers - var requestHeaders = e.RequestHeaders; + var requestHeaders = e.ProxySession.Request.RequestHeaders; - if ((e.RequestMethod.ToUpper() == "POST" || e.RequestMethod.ToUpper() == "PUT") && e.RequestContentLength > 0) + if ((e.RequestMethod.ToUpper() == "POST" || e.RequestMethod.ToUpper() == "PUT")) { //Get/Set request body bytes byte[] bodyBytes = e.GetRequestBody(); @@ -78,7 +78,7 @@ Sample request and response event handlers //To cancel a request with a custom HTML content //Filter URL - if (e.RequestURL.Contains("google.com")) + if (e.ProxySession.Request.RequestUrl.Contains("google.com")) { e.Ok("

Website Blocked

Blocked by titanium web proxy.

"); } @@ -86,10 +86,11 @@ Sample request and response event handlers public void OnResponse(object sender, SessionEventArgs e) { - //read response headers - var responseHeaders = e.ResponseHeaders; + ////read response headers + var responseHeaders = e.ProxySession.Response.ResponseHeaders; + - if (e.ResponseStatusCode == HttpStatusCode.OK) + if (e.ResponseStatusCode == "200") { if (e.ResponseContentType.Trim().ToLower().Contains("text/html")) { diff --git a/Titanium.Web.Proxy.Test/App.config b/Titanium.Web.Proxy.Test/App.config index ce3bd0ddd..c9ec1e68f 100644 --- a/Titanium.Web.Proxy.Test/App.config +++ b/Titanium.Web.Proxy.Test/App.config @@ -4,4 +4,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Titanium.Web.Proxy.Test/ProxyTestController.cs b/Titanium.Web.Proxy.Test/ProxyTestController.cs index 039891890..aade8282e 100644 --- a/Titanium.Web.Proxy.Test/ProxyTestController.cs +++ b/Titanium.Web.Proxy.Test/ProxyTestController.cs @@ -1,4 +1,5 @@ using System; +using System.Text.RegularExpressions; using Titanium.Web.Proxy.EventArguments; namespace Titanium.Web.Proxy.Test @@ -43,10 +44,10 @@ public void Stop() //Read browser URL send back to proxy by the injection script in OnResponse event public void OnRequest(object sender, SessionEventArgs e) { - Console.WriteLine(e.RequestUrl); + Console.WriteLine(e.ProxySession.Request.Url); ////read request headers - //var requestHeaders = e.RequestHeaders; + //var requestHeaders = e.ProxySession.Request.RequestHeaders; //if ((e.RequestMethod.ToUpper() == "POST" || e.RequestMethod.ToUpper() == "PUT")) //{ @@ -63,7 +64,7 @@ public void OnRequest(object sender, SessionEventArgs e) ////To cancel a request with a custom HTML content ////Filter URL - //if (e.RequestURL.Contains("google.com")) + //if (e.ProxySession.Request.RequestUrl.Contains("google.com")) //{ // e.Ok("

Website Blocked

Blocked by titanium web proxy.

"); //} @@ -73,27 +74,19 @@ public void OnRequest(object sender, SessionEventArgs e) //Insert script to read the Browser URL and send it back to proxy public void OnResponse(object sender, SessionEventArgs e) { + ////read response headers - //var responseHeaders = e.ResponseHeaders; - - - //if (e.ResponseStatusCode == HttpStatusCode.OK) + // var responseHeaders = e.ProxySession.Response.ResponseHeaders; + + //if (!e.ProxySession.Request.Hostname.Equals("medeczane.sgk.gov.tr")) return; + //if (e.RequestMethod == "GET" || e.RequestMethod == "POST") //{ - // if (e.ResponseContentType.Trim().ToLower().Contains("text/html")) + // if (e.ProxySession.Response.ResponseStatusCode == "200") // { - // //Get/Set response body bytes - // byte[] responseBodyBytes = e.GetResponseBody(); - // e.SetResponseBody(responseBodyBytes); - - // //Get response body as string - // string responseBody = e.GetResponseBodyAsString(); - - // //Modify e.ServerResponse - // Regex rex = new Regex("", RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Multiline); - // string modified = rex.Replace(responseBody, "", 1); - - // //Set modifed response Html Body - // e.SetResponseBodyString(modified); + // if (e.ProxySession.Response.ContentType.Trim().ToLower().Contains("text/html")) + // { + // string body = e.GetResponseBodyAsString(); + // } // } //} } diff --git a/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs b/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs index aef5ef20c..d0ad2fc35 100644 --- a/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs +++ b/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs @@ -8,10 +8,22 @@ using System.Text; using Titanium.Web.Proxy.Exceptions; using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Network; using Titanium.Web.Proxy.Models; namespace Titanium.Web.Proxy.EventArguments { + public class Client + { + internal TcpClient TcpClient { get; set; } + internal Stream ClientStream { get; set; } + internal CustomBinaryReader ClientStreamReader { get; set; } + internal StreamWriter ClientStreamWriter { get; set; } + + public int ClientPort { get; internal set; } + public IPAddress ClientIpAddress { get; internal set; } + + } public class SessionEventArgs : EventArgs, IDisposable { readonly int _bufferSize; @@ -19,110 +31,72 @@ public class SessionEventArgs : EventArgs, IDisposable internal SessionEventArgs(int bufferSize) { _bufferSize = bufferSize; + Client = new Client(); + ProxySession = new HttpWebSession(); } - internal TcpClient Client { get; set; } - internal Stream ClientStream { get; set; } - internal CustomBinaryReader ClientStreamReader { get; set; } - internal StreamWriter ClientStreamWriter { get; set; } - + internal Client Client { get; set; } public bool IsHttps { get; internal set; } - public string RequestUrl { get; internal set; } - public string RequestHostname { get; internal set; } - public int ClientPort { get; internal set; } - public IPAddress ClientIpAddress { get; internal set; } + public HttpWebSession ProxySession { get; set; } - internal Encoding RequestEncoding { get; set; } - internal Version RequestHttpVersion { get; set; } - internal bool RequestIsAlive { get; set; } - internal bool CancelRequest { get; set; } - internal byte[] RequestBody { get; set; } - internal string RequestBodyString { get; set; } - internal bool RequestBodyRead { get; set; } - public List RequestHeaders { get; internal set; } - internal bool RequestLocked { get; set; } - internal HttpWebRequest ProxyRequest { get; set; } - - internal Encoding ResponseEncoding { get; set; } - internal Stream ResponseStream { get; set; } - internal byte[] ResponseBody { get; set; } - internal string ResponseBodyString { get; set; } - internal bool ResponseBodyRead { get; set; } - public List ResponseHeaders { get; internal set; } - internal bool ResponseLocked { get; set; } - internal HttpWebResponse ServerResponse { get; set; } public int RequestContentLength { get { - if (RequestHeaders.All(x => x.Name.ToLower() != "content-length")) return -1; - int contentLen; - int.TryParse(RequestHeaders.First(x => x.Name.ToLower() == "content-length").Value, out contentLen); - if (contentLen != 0) - return contentLen; - return -1; + return ProxySession.Request.ContentLength; } } public string RequestMethod { - get { return ProxyRequest.Method; } + get { return ProxySession.Request.Method; } } - public HttpStatusCode ResponseStatusCode + public string ResponseStatusCode { - get { return ServerResponse.StatusCode; } + get { return ProxySession.Response.ResponseStatusCode; } } public string ResponseContentType { get { - return ResponseHeaders.Any(x => x.Name.ToLower() == "content-type") - ? ResponseHeaders.First(x => x.Name.ToLower() == "content-type").Value - : null; + return ProxySession.Response.ContentType; } } public void Dispose() { - if (ProxyRequest != null) - ProxyRequest.Abort(); - if (ResponseStream != null) - ResponseStream.Dispose(); - - if (ServerResponse != null) - ServerResponse.Close(); } private void ReadRequestBody() { - if ((ProxyRequest.Method.ToUpper() != "POST" && ProxyRequest.Method.ToUpper() != "PUT")) + if ((ProxySession.Request.Method.ToUpper() != "POST" && ProxySession.Request.Method.ToUpper() != "PUT")) { throw new BodyNotFoundException("Request don't have a body." + "Please verify that this request is a Http POST/PUT and request content length is greater than zero before accessing the body."); } - if (RequestBody == null) + if (ProxySession.Request.RequestBody == null) { var isChunked = false; string requestContentEncoding = null; - if (RequestHeaders.Any(x => x.Name.ToLower() == "content-encoding")) + if (ProxySession.Request.RequestHeaders.Any(x => x.Name.ToLower() == "content-encoding")) { - requestContentEncoding = RequestHeaders.First(x => x.Name.ToLower() == "content-encoding").Value; + requestContentEncoding = ProxySession.Request.RequestHeaders.First(x => x.Name.ToLower() == "content-encoding").Value; } - if (RequestHeaders.Any(x => x.Name.ToLower() == "transfer-encoding")) + if (ProxySession.Request.RequestHeaders.Any(x => x.Name.ToLower() == "transfer-encoding")) { var transferEncoding = - RequestHeaders.First(x => x.Name.ToLower() == "transfer-encoding").Value.ToLower(); + ProxySession.Request.RequestHeaders.First(x => x.Name.ToLower() == "transfer-encoding").Value.ToLower(); if (transferEncoding.Contains("chunked")) { isChunked = true; @@ -131,7 +105,7 @@ private void ReadRequestBody() if (requestContentEncoding == null && !isChunked) - RequestBody = ClientStreamReader.ReadBytes(RequestContentLength); + ProxySession.Request.RequestBody = this.Client.ClientStreamReader.ReadBytes(RequestContentLength); else { using (var requestBodyStream = new MemoryStream()) @@ -140,219 +114,233 @@ private void ReadRequestBody() { while (true) { - var chuchkHead = ClientStreamReader.ReadLine(); + var chuchkHead = this.Client.ClientStreamReader.ReadLine(); var chunkSize = int.Parse(chuchkHead, NumberStyles.HexNumber); if (chunkSize != 0) { - var buffer = ClientStreamReader.ReadBytes(chunkSize); + var buffer = this.Client.ClientStreamReader.ReadBytes(chunkSize); requestBodyStream.Write(buffer, 0, buffer.Length); //chunk trail - ClientStreamReader.ReadLine(); + this.Client.ClientStreamReader.ReadLine(); } else { - ClientStreamReader.ReadLine(); + this.Client.ClientStreamReader.ReadLine(); break; } } } + try { switch (requestContentEncoding) { case "gzip": - RequestBody = CompressionHelper.DecompressGzip(requestBodyStream); + ProxySession.Request.RequestBody = CompressionHelper.DecompressGzip(requestBodyStream.ToArray()); break; case "deflate": - RequestBody = CompressionHelper.DecompressDeflate(requestBodyStream); + ProxySession.Request.RequestBody = CompressionHelper.DecompressDeflate(requestBodyStream); break; case "zlib": - RequestBody = CompressionHelper.DecompressZlib(requestBodyStream); + ProxySession.Request.RequestBody = CompressionHelper.DecompressZlib(requestBodyStream); break; default: - RequestBody = requestBodyStream.ToArray(); + ProxySession.Request.RequestBody = requestBodyStream.ToArray(); break; } } catch { - RequestBody = requestBodyStream.ToArray(); + ProxySession.Request.RequestBody = requestBodyStream.ToArray(); } } } } - RequestBodyRead = true; + ProxySession.Request.RequestBodyRead = true; } private void ReadResponseBody() { - if (ResponseBody == null) + if (ProxySession.Response.ResponseBody == null) { - switch (ServerResponse.ContentEncoding) + using (var responseBodyStream = new MemoryStream()) { - case "gzip": - ResponseBody = CompressionHelper.DecompressGzip(ResponseStream); - break; - case "deflate": - ResponseBody = CompressionHelper.DecompressDeflate(ResponseStream); - break; - case "zlib": - ResponseBody = CompressionHelper.DecompressZlib(ResponseStream); - break; - default: - ResponseBody = DecodeData(ResponseStream); - break; - } - - ResponseBodyRead = true; - } - } + if (ProxySession.Response.IsChunked) + { + while (true) + { + var chuchkHead = ProxySession.ProxyClient.ServerStreamReader.ReadLine(); + var chunkSize = int.Parse(chuchkHead, NumberStyles.HexNumber); + if (chunkSize != 0) + { + var buffer = ProxySession.ProxyClient.ServerStreamReader.ReadBytes(chunkSize); + responseBodyStream.Write(buffer, 0, buffer.Length); + //chunk trail + ProxySession.ProxyClient.ServerStreamReader.ReadLine(); + } + else + { + ProxySession.ProxyClient.ServerStreamReader.ReadLine(); + break; + } + } + } + else + { + var buffer = ProxySession.ProxyClient.ServerStreamReader.ReadBytes(ProxySession.Response.ContentLength); + responseBodyStream.Write(buffer, 0, buffer.Length); + } - //stream reader not recomended for images - private byte[] DecodeData(Stream responseStream) - { - var buffer = new byte[_bufferSize]; - using (var ms = new MemoryStream()) - { - int read; - while ((read = responseStream.Read(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); + switch (ProxySession.Response.ContentEncoding) + { + case "gzip": + ProxySession.Response.ResponseBody = CompressionHelper.DecompressGzip(responseBodyStream.ToArray()); + break; + case "deflate": + ProxySession.Response.ResponseBody = CompressionHelper.DecompressDeflate(responseBodyStream); + break; + case "zlib": + ProxySession.Response.ResponseBody = CompressionHelper.DecompressZlib(responseBodyStream); + break; + default: + ProxySession.Response.ResponseBody = responseBodyStream.ToArray(); + break; + } } - return ms.ToArray(); + + ProxySession.Response.ResponseBodyRead = true; } } + public Encoding GetRequestBodyEncoding() { - if (RequestLocked) throw new Exception("You cannot call this function after request is made to server."); + if (ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function after request is made to server."); - return RequestEncoding; + return ProxySession.Request.Encoding; } public byte[] GetRequestBody() { - if (RequestLocked) throw new Exception("You cannot call this function after request is made to server."); + if (ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function after request is made to server."); ReadRequestBody(); - return RequestBody; + return ProxySession.Request.RequestBody; } public string GetRequestBodyAsString() { - if (RequestLocked) throw new Exception("You cannot call this function after request is made to server."); + if (ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function after request is made to server."); ReadRequestBody(); - return RequestBodyString ?? (RequestBodyString = RequestEncoding.GetString(RequestBody)); + return ProxySession.Request.RequestBodyString ?? (ProxySession.Request.RequestBodyString = ProxySession.Request.Encoding.GetString(ProxySession.Request.RequestBody)); } public void SetRequestBody(byte[] body) { - if (RequestLocked) throw new Exception("You cannot call this function after request is made to server."); + if (ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function after request is made to server."); - if (!RequestBodyRead) + if (!ProxySession.Request.RequestBodyRead) { ReadRequestBody(); } - RequestBody = body; - RequestBodyRead = true; + ProxySession.Request.RequestBody = body; + ProxySession.Request.RequestBodyRead = true; } public void SetRequestBodyString(string body) { - if (RequestLocked) throw new Exception("You cannot call this function after request is made to server."); + if (ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function after request is made to server."); - if (!RequestBodyRead) + if (!ProxySession.Request.RequestBodyRead) { ReadRequestBody(); } - RequestBody = RequestEncoding.GetBytes(body); - RequestBodyRead = true; + ProxySession.Request.RequestBody = ProxySession.Request.Encoding.GetBytes(body); + ProxySession.Request.RequestBodyRead = true; } public Encoding GetResponseBodyEncoding() { - if (!RequestLocked) throw new Exception("You cannot call this function before request is made to server."); + if (!ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function before request is made to server."); - return ResponseEncoding; + return ProxySession.Response.Encoding; } public byte[] GetResponseBody() { - if (!RequestLocked) throw new Exception("You cannot call this function before request is made to server."); + if (!ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function before request is made to server."); ReadResponseBody(); - return ResponseBody; + return ProxySession.Response.ResponseBody; } public string GetResponseBodyAsString() { - if (!RequestLocked) throw new Exception("You cannot call this function before request is made to server."); + if (!ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function before request is made to server."); GetResponseBody(); - return ResponseBodyString ?? (ResponseBodyString = ResponseEncoding.GetString(ResponseBody)); + return ProxySession.Response.ResponseBodyString ?? (ProxySession.Response.ResponseBodyString = ProxySession.Response.Encoding.GetString(ProxySession.Response.ResponseBody)); } public void SetResponseBody(byte[] body) { - if (!RequestLocked) throw new Exception("You cannot call this function before request is made to server."); + if (!ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function before request is made to server."); - if (ResponseBody == null) + if (ProxySession.Response.ResponseBody == null) { GetResponseBody(); } - ResponseBody = body; + ProxySession.Response.ResponseBody = body; } public void SetResponseBodyString(string body) { - if (!RequestLocked) throw new Exception("You cannot call this function before request is made to server."); + if (!ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function before request is made to server."); - if (ResponseBody == null) + if (ProxySession.Response.ResponseBody == null) { GetResponseBody(); } - var bodyBytes = ResponseEncoding.GetBytes(body); + var bodyBytes = ProxySession.Response.Encoding.GetBytes(body); SetResponseBody(bodyBytes); } public void Ok(string html) { - if (RequestLocked) throw new Exception("You cannot call this function after request is made to server."); + if (ProxySession.Request.RequestLocked) throw new Exception("You cannot call this function after request is made to server."); if (html == null) html = string.Empty; var result = Encoding.Default.GetBytes(html); - var connectStreamWriter = new StreamWriter(ClientStream); - var s = string.Format("HTTP/{0}.{1} {2} {3}", RequestHttpVersion.Major, RequestHttpVersion.Minor, 200, "Ok"); - connectStreamWriter.WriteLine(s); + var connectStreamWriter = new StreamWriter(this.Client.ClientStream); + connectStreamWriter.WriteLine(string.Format("{0} {2} {3}", ProxySession.Request.HttpVersion, 200, "Ok")); connectStreamWriter.WriteLine("Timestamp: {0}", DateTime.Now); connectStreamWriter.WriteLine("content-length: " + result.Length); connectStreamWriter.WriteLine("Cache-Control: no-cache, no-store, must-revalidate"); connectStreamWriter.WriteLine("Pragma: no-cache"); connectStreamWriter.WriteLine("Expires: 0"); - connectStreamWriter.WriteLine(RequestIsAlive ? "Connection: Keep-Alive" : "Connection: close"); + connectStreamWriter.WriteLine(ProxySession.Request.IsAlive ? "Connection: Keep-Alive" : "Connection: close"); connectStreamWriter.WriteLine(); connectStreamWriter.Flush(); - ClientStream.Write(result, 0, result.Length); - + this.Client.ClientStream.Write(result, 0, result.Length); - CancelRequest = true; + ProxySession.Request.CancelRequest = true; } } } \ No newline at end of file diff --git a/Titanium.Web.Proxy/Extensions/HttpWebRequestExtensions.cs b/Titanium.Web.Proxy/Extensions/HttpWebRequestExtensions.cs index b105faeaa..de6457693 100644 --- a/Titanium.Web.Proxy/Extensions/HttpWebRequestExtensions.cs +++ b/Titanium.Web.Proxy/Extensions/HttpWebRequestExtensions.cs @@ -1,17 +1,18 @@ using System.Net; using System.Text; +using Titanium.Web.Proxy.Network; namespace Titanium.Web.Proxy.Extensions { public static class HttpWebRequestExtensions { - public static Encoding GetEncoding(this HttpWebRequest request) + public static Encoding GetEncoding(this HttpWebSession request) { try { - if (request.ContentType == null) return Encoding.GetEncoding("ISO-8859-1"); + if (request.Request.ContentType == null) return Encoding.GetEncoding("ISO-8859-1"); - var contentTypes = request.ContentType.Split(';'); + var contentTypes = request.Request.ContentType.Split(';'); foreach (var contentType in contentTypes) { var encodingSplit = contentType.Split('='); diff --git a/Titanium.Web.Proxy/Extensions/HttpWebResponseExtensions.cs b/Titanium.Web.Proxy/Extensions/HttpWebResponseExtensions.cs index de9e6e764..770e7942f 100644 --- a/Titanium.Web.Proxy/Extensions/HttpWebResponseExtensions.cs +++ b/Titanium.Web.Proxy/Extensions/HttpWebResponseExtensions.cs @@ -1,14 +1,15 @@ using System.Net; using System.Text; +using Titanium.Web.Proxy.Network; namespace Titanium.Web.Proxy.Extensions { public static class HttpWebResponseExtensions { - public static Encoding GetEncoding(this HttpWebResponse response) + public static Encoding GetResponseEncoding(this HttpWebSession response) { - if (string.IsNullOrEmpty(response.CharacterSet)) return Encoding.GetEncoding("ISO-8859-1"); - return Encoding.GetEncoding(response.CharacterSet.Replace(@"""",string.Empty)); + if (string.IsNullOrEmpty(response.Response.CharacterSet)) return Encoding.GetEncoding("ISO-8859-1"); + return Encoding.GetEncoding(response.Response.CharacterSet.Replace(@"""", string.Empty)); } } } \ No newline at end of file diff --git a/Titanium.Web.Proxy/Helpers/Compression.cs b/Titanium.Web.Proxy/Helpers/Compression.cs index dc00b9323..0990c16e4 100644 --- a/Titanium.Web.Proxy/Helpers/Compression.cs +++ b/Titanium.Web.Proxy/Helpers/Compression.cs @@ -50,11 +50,10 @@ public static byte[] CompressGzip(byte[] bytes) } } - public static byte[] DecompressGzip(Stream input) + //identify why passing stream instead of bytes returns empty result + public static byte[] DecompressGzip(byte[] gzip) { - using ( - var decompressor = new System.IO.Compression.GZipStream(input, - System.IO.Compression.CompressionMode.Decompress)) + using (var decompressor = new System.IO.Compression.GZipStream(new MemoryStream(gzip), System.IO.Compression.CompressionMode.Decompress)) { var buffer = new byte[BufferSize]; diff --git a/Titanium.Web.Proxy/Helpers/CustomBinaryReader.cs b/Titanium.Web.Proxy/Helpers/CustomBinaryReader.cs index 331f85961..68c3ece93 100644 --- a/Titanium.Web.Proxy/Helpers/CustomBinaryReader.cs +++ b/Titanium.Web.Proxy/Helpers/CustomBinaryReader.cs @@ -5,7 +5,7 @@ namespace Titanium.Web.Proxy.Helpers { - internal class CustomBinaryReader : BinaryReader + public class CustomBinaryReader : BinaryReader { internal CustomBinaryReader(Stream stream, Encoding encoding) : base(stream, encoding) diff --git a/Titanium.Web.Proxy/Helpers/NetFramework.cs b/Titanium.Web.Proxy/Helpers/NetFramework.cs deleted file mode 100644 index 5264f0d89..000000000 --- a/Titanium.Web.Proxy/Helpers/NetFramework.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Net.Configuration; -using System.Reflection; - -namespace Titanium.Web.Proxy.Helpers -{ - public class NetFrameworkHelper - { - //Fix bug in .Net 4.0 HttpWebRequest (don't use this for 4.5 and above) - //http://stackoverflow.com/questions/856885/httpwebrequest-to-url-with-dot-at-the-end - public static void UrlPeriodFix() - { - var getSyntax = typeof (UriParser).GetMethod("GetSyntax", BindingFlags.Static | BindingFlags.NonPublic); - var flagsField = typeof (UriParser).GetField("m_Flags", BindingFlags.Instance | BindingFlags.NonPublic); - if (getSyntax != null && flagsField != null) - { - foreach (var scheme in new[] {"http", "https"}) - { - var parser = (UriParser) getSyntax.Invoke(null, new object[] {scheme}); - if (parser != null) - { - var flagsValue = (int) flagsField.GetValue(parser); - - if ((flagsValue & 0x1000000) != 0) - flagsField.SetValue(parser, flagsValue & ~0x1000000); - } - } - } - } - - // Enable/disable useUnsafeHeaderParsing. - // See http://o2platform.wordpress.com/2010/10/20/dealing-with-the-server-committed-a-protocol-violation-sectionresponsestatusline/ - public static bool ToggleAllowUnsafeHeaderParsing(bool enable) - { - //Get the assembly that contains the internal class - var assembly = Assembly.GetAssembly(typeof (SettingsSection)); - if (assembly != null) - { - //Use the assembly in order to get the internal type for the internal class - var settingsSectionType = assembly.GetType("System.Net.Configuration.SettingsSectionInternal"); - if (settingsSectionType != null) - { - //Use the internal static property to get an instance of the internal settings class. - //If the static instance isn't created already invoking the property will create it for us. - var anInstance = settingsSectionType.InvokeMember("Section", - BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.NonPublic, null, null, - new object[] {}); - if (anInstance != null) - { - //Locate the private bool field that tells the framework if unsafe header parsing is allowed - var aUseUnsafeHeaderParsing = settingsSectionType.GetField("useUnsafeHeaderParsing", - BindingFlags.NonPublic | BindingFlags.Instance); - if (aUseUnsafeHeaderParsing != null) - { - aUseUnsafeHeaderParsing.SetValue(anInstance, enable); - return true; - } - } - } - } - return false; - } - } -} \ No newline at end of file diff --git a/Titanium.Web.Proxy/Network/HttpWebClient.cs b/Titanium.Web.Proxy/Network/HttpWebClient.cs new file mode 100644 index 000000000..d30567565 --- /dev/null +++ b/Titanium.Web.Proxy/Network/HttpWebClient.cs @@ -0,0 +1,155 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Models; + +namespace Titanium.Web.Proxy.Network +{ + public class Request + { + public string Method { get; internal set; } + public Uri RequestUri { get; internal set; } + public string HttpVersion { get; internal set; } + + public string Status { get; internal set; } + public int ContentLength { get; internal set; } + public bool SendChunked { get; internal set; } + public string ContentType { get; internal set; } + public bool KeepAlive { get; internal set; } + public string Hostname { get; internal set; } + + public string Url { get; internal set; } + + internal Encoding Encoding { get; set; } + internal bool IsAlive { get; set; } + internal bool CancelRequest { get; set; } + internal byte[] RequestBody { get; set; } + internal string RequestBodyString { get; set; } + internal bool RequestBodyRead { get; set; } + internal bool UpgradeToWebSocket { get; set; } + public List RequestHeaders { get; internal set; } + internal bool RequestLocked { get; set; } + + public Request() + { + this.RequestHeaders = new List(); + } + + } + + public class Response + { + + internal Encoding Encoding { get; set; } + internal Stream ResponseStream { get; set; } + internal byte[] ResponseBody { get; set; } + internal string ResponseBodyString { get; set; } + internal bool ResponseBodyRead { get; set; } + internal bool ResponseLocked { get; set; } + public List ResponseHeaders { get; internal set; } + internal string CharacterSet { get; set; } + internal string ContentEncoding { get; set; } + internal string HttpVersion { get; set; } + public string ResponseStatusCode { get; internal set; } + public string ResponseStatusDescription { get; internal set; } + internal bool ResponseKeepAlive { get; set; } + public string ContentType { get; internal set; } + internal int ContentLength { get; set; } + internal bool IsChunked { get; set; } + + public Response() + { + this.ResponseHeaders = new List(); + this.ResponseKeepAlive = true; + } + } + + public class HttpWebSession + { + private const string Space = " "; + + public bool IsSecure + { + get + { + return this.Request.RequestUri.Scheme == Uri.UriSchemeHttps; + } + } + + public Request Request { get; set; } + public Response Response { get; set; } + internal TcpConnection ProxyClient { get; set; } + + public void SetConnection(TcpConnection Connection) + { + Connection.LastAccess = DateTime.Now; + ProxyClient = Connection; + } + + public HttpWebSession() + { + this.Request = new Request(); + this.Response = new Response(); + } + + public void SendRequest() + { + Stream stream = ProxyClient.Stream; + + StringBuilder requestLines = new StringBuilder(); + + requestLines.AppendLine(string.Join(" ", new string[3] + { + this.Request.Method, + this.Request.RequestUri.PathAndQuery, + this.Request.HttpVersion + })); + + foreach (HttpHeader httpHeader in this.Request.RequestHeaders) + { + requestLines.AppendLine(httpHeader.Name + ':' + httpHeader.Value); + } + + requestLines.AppendLine(); + + string request = requestLines.ToString(); + byte[] requestBytes = Encoding.ASCII.GetBytes(request); + stream.Write(requestBytes, 0, requestBytes.Length); + stream.Flush(); + } + + public void ReceiveResponse() + { + var httpResult = ProxyClient.ServerStreamReader.ReadLine().Split(new char[] { ' ' }, 3); + + if(string.IsNullOrEmpty(httpResult[0])) + { + var s = ProxyClient.ServerStreamReader.ReadLine(); + } + + this.Response.HttpVersion = httpResult[0]; + this.Response.ResponseStatusCode = httpResult[1]; + string status = httpResult[2]; + + this.Response.ResponseStatusDescription = status; + + List responseLines = ProxyClient.ServerStreamReader.ReadAllLines(); + + for (int index = 0; index < responseLines.Count; ++index) + { + string[] strArray = responseLines[index].Split(new char[] { ':' }, 2); + this.Response.ResponseHeaders.Add(new HttpHeader(strArray[0], strArray[1])); + } + } + + } + + +} diff --git a/Titanium.Web.Proxy/Network/TcpConnectionManager.cs b/Titanium.Web.Proxy/Network/TcpConnectionManager.cs new file mode 100644 index 000000000..616adc3a7 --- /dev/null +++ b/Titanium.Web.Proxy/Network/TcpConnectionManager.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.IO; +using System.Net.Security; +using Titanium.Web.Proxy.Helpers; +using System.Threading; + +namespace Titanium.Web.Proxy.Network +{ + public class TcpConnection + { + public string HostName { get; set; } + public int port { get; set; } + public bool IsSecure { get; set; } + + public TcpClient TcpClient { get; set; } + public CustomBinaryReader ServerStreamReader { get; set; } + public Stream Stream { get; set; } + + public DateTime LastAccess { get; set; } + + public TcpConnection() + { + LastAccess = DateTime.Now; + } + } + + internal class TcpConnectionManager + { + static List ConnectionCache = new List(); + + public static TcpConnection GetClient(string Hostname, int port, bool IsSecure) + { + TcpConnection cached = null; + while (true) + { + lock (ConnectionCache) + { + cached = ConnectionCache.FirstOrDefault(x => x.HostName == Hostname && x.port == port && x.IsSecure == IsSecure && x.TcpClient.Connected); + + if (cached != null) + ConnectionCache.Remove(cached); + } + + if (cached != null && !cached.TcpClient.Client.IsConnected()) + continue; + + if (cached == null) + break; + } + + if (cached == null) + cached = CreateClient(Hostname, port, IsSecure); + + if (ConnectionCache.Where(x => x.HostName == Hostname && x.port == port && x.IsSecure == IsSecure && x.TcpClient.Connected).Count() < 2) + { + Task.Factory.StartNew(() => ReleaseClient(CreateClient(Hostname, port, IsSecure))); + } + + return cached; + } + + private static TcpConnection CreateClient(string Hostname, int port, bool IsSecure) + { + var client = new TcpClient(Hostname, port); + var stream = (Stream)client.GetStream(); + + if (IsSecure) + { + var sslStream = (SslStream)null; + try + { + sslStream = new SslStream(stream); + sslStream.AuthenticateAsClient(Hostname); + stream = (Stream)sslStream; + } + catch + { + if (sslStream != null) + sslStream.Dispose(); + throw; + } + } + + return new TcpConnection() + { + HostName = Hostname, + port = port, + IsSecure = IsSecure, + TcpClient = client, + ServerStreamReader = new CustomBinaryReader(stream, Encoding.ASCII), + Stream = stream + }; + } + + public static void ReleaseClient(TcpConnection Connection) + { + Connection.LastAccess = DateTime.Now; + ConnectionCache.Add(Connection); + } + + public static void ClearIdleConnections() + { + while (true) + { + lock (ConnectionCache) + { + var cutOff = DateTime.Now.AddSeconds(-60); + + ConnectionCache + .Where(x => x.LastAccess < cutOff) + .ToList() + .ForEach(x => x.TcpClient.Close()); + + ConnectionCache.RemoveAll(x => x.LastAccess < cutOff); + } + + Thread.Sleep(1000 * 60 * 3); + } + + } + + + } +} diff --git a/Titanium.Web.Proxy/Network/TcpExtensions.cs b/Titanium.Web.Proxy/Network/TcpExtensions.cs new file mode 100644 index 000000000..a45f80ffa --- /dev/null +++ b/Titanium.Web.Proxy/Network/TcpExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + + +namespace Titanium.Web.Proxy.Network +{ + + internal static class TcpExtensions + { + public static bool IsConnected(this Socket client) + { + // This is how you can determine whether a socket is still connected. + bool blockingState = client.Blocking; + try + { + byte[] tmp = new byte[1]; + + client.Blocking = false; + client.Send(tmp, 0, 0); + return true; + } + catch (SocketException e) + { + // 10035 == WSAEWOULDBLOCK + if (e.NativeErrorCode.Equals(10035)) + return true; + else + { + return false; + } + } + finally + { + client.Blocking = blockingState; + } + } + } + +} diff --git a/Titanium.Web.Proxy/ProxyServer.cs b/Titanium.Web.Proxy/ProxyServer.cs index cd20a539c..010b14fb1 100644 --- a/Titanium.Web.Proxy/ProxyServer.cs +++ b/Titanium.Web.Proxy/ProxyServer.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Network; namespace Titanium.Web.Proxy { @@ -25,7 +26,7 @@ public partial class ProxyServer private static readonly Regex CookieSplitRegEx = new Regex(@",(?! )"); - private static readonly byte[] ChunkTrail = Encoding.ASCII.GetBytes(Environment.NewLine); + private static readonly byte[] NewLineBytes = Encoding.ASCII.GetBytes(Environment.NewLine); private static readonly byte[] ChunkEnd = Encoding.ASCII.GetBytes(0.ToString("x2") + Environment.NewLine + Environment.NewLine); @@ -58,27 +59,8 @@ static ProxyServer() public static event EventHandler BeforeResponse; public static void Initialize() - { - ServicePointManager.Expect100Continue = false; - WebRequest.DefaultWebProxy = null; - ServicePointManager.DefaultConnectionLimit = int.MaxValue; - ServicePointManager.DnsRefreshTimeout = 3 * 60 * 1000; //3 minutes - ServicePointManager.MaxServicePointIdleTime = 3 * 60 * 1000; - - //HttpWebRequest certificate validation callback - ServicePointManager.ServerCertificateValidationCallback = - delegate(object s, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - if (sslPolicyErrors == SslPolicyErrors.None) return true; - return false; - }; - -#if NET40 - //Fix a bug in .NET 4.0 - NetFrameworkHelper.UrlPeriodFix(); - //useUnsafeHeaderParsing -#endif - NetFrameworkHelper.ToggleAllowUnsafeHeaderParsing(true); + { + Task.Factory.StartNew(()=>TcpConnectionManager.ClearIdleConnections()); } @@ -100,7 +82,10 @@ public static bool Start() { SystemProxyHelper.EnableProxyHttp( Equals(ListeningIpAddress, IPAddress.Any) ? "127.0.0.1" : ListeningIpAddress.ToString(), ListeningPort); + +#if !DEBUG FireFoxHelper.AddFirefox(); +#endif if (EnableSsl) @@ -142,7 +127,9 @@ public static void Stop() if (SetAsSystemProxy) { SystemProxyHelper.DisableAllProxy(); +#if !DEBUG FireFoxHelper.RemoveFirefox(); +#endif } _listener.Stop(); diff --git a/Titanium.Web.Proxy/RequestHandler.cs b/Titanium.Web.Proxy/RequestHandler.cs index 6942a3e0d..4bffa5f5f 100644 --- a/Titanium.Web.Proxy/RequestHandler.cs +++ b/Titanium.Web.Proxy/RequestHandler.cs @@ -13,6 +13,7 @@ using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Extensions; using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Network; using Titanium.Web.Proxy.Models; namespace Titanium.Web.Proxy @@ -29,6 +30,7 @@ private static void HandleClient(TcpClient client) try { //read the first line HTTP command + var httpCmd = clientStreamReader.ReadLine(); if (string.IsNullOrEmpty(httpCmd)) @@ -87,7 +89,7 @@ private static void HandleClient(TcpClient client) httpCmd = clientStreamReader.ReadLine(); - + } else if (httpVerb.ToUpper() == "CONNECT") { @@ -113,16 +115,19 @@ private static void HandleClient(TcpClient client) private static void HandleHttpSessionRequest(TcpClient client, string httpCmd, Stream clientStream, CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, string secureTunnelHostName) { + TcpConnection connection = null; + string lastRequestHostName = null; + while (true) { if (string.IsNullOrEmpty(httpCmd)) { Dispose(client, clientStream, clientStreamReader, clientStreamWriter, null); - return; + break; } var args = new SessionEventArgs(BUFFER_SIZE); - args.Client = client; + args.Client.TcpClient = client; try { @@ -149,74 +154,72 @@ private static void HandleHttpSessionRequest(TcpClient client, string httpCmd, S args.IsHttps = true; } - args.RequestHeaders = new List(); - string tmpLine; + args.ProxySession.Request.RequestHeaders = new List(); + string tmpLine; while (!string.IsNullOrEmpty(tmpLine = clientStreamReader.ReadLine())) { - var header = tmpLine.Split(ColonSpaceSplit, 2, StringSplitOptions.None); - args.RequestHeaders.Add(new HttpHeader(header[0], header[1])); + var header = tmpLine.Split(new char[] { ':' }, 2); + args.ProxySession.Request.RequestHeaders.Add(new HttpHeader(header[0], header[1])); } - for (var i = 0; i < args.RequestHeaders.Count; i++) - { - var rawHeader = args.RequestHeaders[i]; + SetRequestHeaders(args.ProxySession.Request.RequestHeaders, args.ProxySession); - - //if request was upgrade to web-socket protocol then relay the request without proxying - if ((rawHeader.Name.ToLower() == "upgrade") && (rawHeader.Value.ToLower() == "websocket")) - { - TcpHelper.SendRaw(clientStreamReader.BaseStream, httpCmd, args.RequestHeaders, + if (args.ProxySession.Request.UpgradeToWebSocket) + { + TcpHelper.SendRaw(clientStreamReader.BaseStream, httpCmd, args.ProxySession.Request.RequestHeaders, httpRemoteUri.Host, httpRemoteUri.Port, httpRemoteUri.Scheme == Uri.UriSchemeHttps); - Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args); - return; - } + Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args); + return; } - //construct the web request that we are going to issue on behalf of the client. - args.ProxyRequest = (HttpWebRequest)WebRequest.Create(httpRemoteUri); - args.ProxyRequest.Proxy = null; - args.ProxyRequest.UseDefaultCredentials = true; - args.ProxyRequest.Method = httpMethod; - args.ProxyRequest.ProtocolVersion = version; - args.ClientStream = clientStream; - args.ClientStreamReader = clientStreamReader; - args.ClientStreamWriter = clientStreamWriter; - args.ProxyRequest.AllowAutoRedirect = false; - args.ProxyRequest.AutomaticDecompression = DecompressionMethods.None; - args.RequestHostname = args.ProxyRequest.RequestUri.Host; - args.RequestUrl = args.ProxyRequest.RequestUri.OriginalString; - args.ClientPort = ((IPEndPoint)client.Client.RemoteEndPoint).Port; - args.ClientIpAddress = ((IPEndPoint)client.Client.RemoteEndPoint).Address; - args.RequestHttpVersion = version; - args.RequestIsAlive = args.ProxyRequest.KeepAlive; - args.ProxyRequest.AllowWriteStreamBuffering = true; + args.ProxySession.Request.RequestUri = httpRemoteUri; + + args.ProxySession.Request.Method = httpMethod; + args.ProxySession.Request.HttpVersion = httpVersion; + args.Client.ClientStream = clientStream; + args.Client.ClientStreamReader = clientStreamReader; + args.Client.ClientStreamWriter = clientStreamWriter; + args.ProxySession.Request.Hostname = args.ProxySession.Request.RequestUri.Host; + args.ProxySession.Request.Url = args.ProxySession.Request.RequestUri.OriginalString; + args.Client.ClientPort = ((IPEndPoint)client.Client.RemoteEndPoint).Port; + args.Client.ClientIpAddress = ((IPEndPoint)client.Client.RemoteEndPoint).Address; //If requested interception if (BeforeRequest != null) { - args.RequestEncoding = args.ProxyRequest.GetEncoding(); + args.ProxySession.Request.Encoding = args.ProxySession.GetEncoding(); BeforeRequest(null, args); } - args.RequestLocked = true; + args.ProxySession.Request.RequestLocked = true; - if (args.CancelRequest) + if (args.ProxySession.Request.CancelRequest) { Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args); - return; + break; } - SetRequestHeaders(args.RequestHeaders, args.ProxyRequest); + + //construct the web request that we are going to issue on behalf of the client. + connection = connection == null ? + TcpConnectionManager.GetClient(args.ProxySession.Request.RequestUri.Host, args.ProxySession.Request.RequestUri.Port, args.IsHttps) + : lastRequestHostName != args.ProxySession.Request.Hostname ? TcpConnectionManager.GetClient(args.ProxySession.Request.RequestUri.Host, args.ProxySession.Request.RequestUri.Port, args.IsHttps) + : connection; + + lastRequestHostName = args.ProxySession.Request.Hostname; + + args.ProxySession.SetConnection(connection); + args.ProxySession.SendRequest(); //If request was modified by user - if (args.RequestBodyRead) + if (args.ProxySession.Request.RequestBodyRead) { - args.ProxyRequest.ContentLength = args.RequestBody.Length; - var newStream = args.ProxyRequest.GetRequestStream(); - newStream.Write(args.RequestBody, 0, args.RequestBody.Length); + args.ProxySession.Request.ContentLength = args.ProxySession.Request.RequestBody.Length; + var newStream = args.ProxySession.ProxyClient.ServerStreamReader.BaseStream; + newStream.Write(args.ProxySession.Request.RequestBody, 0, args.ProxySession.Request.RequestBody.Length); } else { @@ -230,8 +233,9 @@ private static void HandleHttpSessionRequest(TcpClient client, string httpCmd, S HandleHttpSessionResponse(args); //if connection is closing exit - if (args.ResponseHeaders.Any(x => x.Name.ToLower() == "connection" && x.Value.ToLower() == "close")) + if (args.ProxySession.Response.ResponseKeepAlive == false) { + connection.TcpClient.Close(); Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args); return; } @@ -243,188 +247,162 @@ private static void HandleHttpSessionRequest(TcpClient client, string httpCmd, S catch { Dispose(client, clientStream, clientStreamReader, clientStreamWriter, args); - return; + break; } + } + + if (connection != null) + TcpConnectionManager.ReleaseClient(connection); } private static void WriteConnectResponse(StreamWriter clientStreamWriter, string httpVersion) { clientStreamWriter.WriteLine(httpVersion + " 200 Connection established"); clientStreamWriter.WriteLine("Timestamp: {0}", DateTime.Now); - //clientStreamWriter.WriteLine("connection:close"); clientStreamWriter.WriteLine(); clientStreamWriter.Flush(); } - private static void SetRequestHeaders(List requestHeaders, HttpWebRequest webRequest) + private static void SetRequestHeaders(List requestHeaders, HttpWebSession webRequest) { for (var i = 0; i < requestHeaders.Count; i++) { switch (requestHeaders[i].Name.ToLower()) { - case "accept": - webRequest.Accept = requestHeaders[i].Value; - break; case "accept-encoding": - webRequest.Headers.Add("Accept-Encoding", "gzip,deflate,zlib"); - break; - case "cookie": - webRequest.Headers["Cookie"] = requestHeaders[i].Value; + requestHeaders[i].Value = "gzip,deflate,zlib"; break; case "connection": if (requestHeaders[i].Value.ToLower() == "keep-alive") - webRequest.KeepAlive = true; - + webRequest.Request.KeepAlive = true; break; case "content-length": int contentLen; int.TryParse(requestHeaders[i].Value, out contentLen); if (contentLen != 0) - webRequest.ContentLength = contentLen; + webRequest.Request.ContentLength = contentLen; break; case "content-type": - webRequest.ContentType = requestHeaders[i].Value; - break; - case "expect": - if (requestHeaders[i].Value.ToLower() == "100-continue") - webRequest.ServicePoint.Expect100Continue = true; - else - webRequest.Expect = requestHeaders[i].Value; + webRequest.Request.ContentType = requestHeaders[i].Value; break; case "host": - webRequest.Host = requestHeaders[i].Value; - break; - case "if-modified-since": - var sb = requestHeaders[i].Value.Trim().Split(SemiSplit); - DateTime d; - if (DateTime.TryParse(sb[0], out d)) - webRequest.IfModifiedSince = d; + webRequest.Request.Hostname = requestHeaders[i].Value; break; case "proxy-connection": if (requestHeaders[i].Value.ToLower() == "keep-alive") - webRequest.KeepAlive = true; + webRequest.Request.KeepAlive = true; else if (requestHeaders[i].Value.ToLower() == "close") - webRequest.KeepAlive = false; - break; - case "range": - var startEnd = requestHeaders[i].Value.Replace(Environment.NewLine, "").Remove(0, 6).Split('-'); - if (startEnd.Length > 1) - { - if (!string.IsNullOrEmpty(startEnd[1])) - webRequest.AddRange(int.Parse(startEnd[0]), int.Parse(startEnd[1])); - else webRequest.AddRange(int.Parse(startEnd[0])); - } - else - webRequest.AddRange(int.Parse(startEnd[0])); + webRequest.Request.KeepAlive = false; break; - case "referer": - webRequest.Referer = requestHeaders[i].Value; - break; - case "user-agent": - webRequest.UserAgent = requestHeaders[i].Value; + + case "upgrade": + if (requestHeaders[i].Value.ToLower() == "websocket") + webRequest.Request.UpgradeToWebSocket = true; break; //revisit this, transfer-encoding is not a request header according to spec //But how to identify if client is sending chunked body for PUT/POST? case "transfer-encoding": if (requestHeaders[i].Value.ToLower().Contains("chunked")) - webRequest.SendChunked = true; + webRequest.Request.SendChunked = true; else - webRequest.SendChunked = false; - break; - case "upgrade": - if (requestHeaders[i].Value.ToLower() == "http/1.1") - webRequest.Headers.Add("Upgrade", requestHeaders[i].Value); + webRequest.Request.SendChunked = false; break; default: - webRequest.Headers.Add(requestHeaders[i].Name, requestHeaders[i].Value); - break; } } + FixRequestProxyHeaders(requestHeaders); + webRequest.Request.RequestHeaders = requestHeaders; } + private static void FixRequestProxyHeaders(List headers) + { + //If proxy-connection close was returned inform to close the connection + var proxyHeader = headers.FirstOrDefault(x => x.Name.ToLower() == "proxy-connection"); + var connectionheader = headers.FirstOrDefault(x => x.Name.ToLower() == "connection"); + + if (proxyHeader != null) + if (connectionheader == null) + { + headers.Add(new HttpHeader("connection", proxyHeader.Value)); + } + else + { + connectionheader.Value = proxyHeader.Value; + } + headers.RemoveAll(x => x.Name.ToLower() == "proxy-connection"); + } //This is called when the request is PUT/POST to read the body private static void SendClientRequestBody(SessionEventArgs args) { // End the operation - var postStream = args.ProxyRequest.GetRequestStream(); + var postStream = args.ProxySession.ProxyClient.Stream; - if (args.ProxyRequest.ContentLength > 0) + if (args.ProxySession.Request.ContentLength > 0) { - args.ProxyRequest.AllowWriteStreamBuffering = true; + //args.ProxyRequest.AllowWriteStreamBuffering = true; try { var totalbytesRead = 0; int bytesToRead; - if (args.ProxyRequest.ContentLength < BUFFER_SIZE) + if (args.ProxySession.Request.ContentLength < BUFFER_SIZE) { - bytesToRead = (int)args.ProxyRequest.ContentLength; + bytesToRead = (int)args.ProxySession.Request.ContentLength; } else bytesToRead = BUFFER_SIZE; - while (totalbytesRead < (int)args.ProxyRequest.ContentLength) + while (totalbytesRead < (int)args.ProxySession.Request.ContentLength) { - var buffer = args.ClientStreamReader.ReadBytes(bytesToRead); + var buffer = args.Client.ClientStreamReader.ReadBytes(bytesToRead); totalbytesRead += buffer.Length; - var remainingBytes = (int)args.ProxyRequest.ContentLength - totalbytesRead; + var remainingBytes = (int)args.ProxySession.Request.ContentLength - totalbytesRead; if (remainingBytes < bytesToRead) { bytesToRead = remainingBytes; } postStream.Write(buffer, 0, buffer.Length); } - - postStream.Close(); } catch { - postStream.Close(); - postStream.Dispose(); throw; } } //Need to revist, find any potential bugs - else if (args.ProxyRequest.SendChunked) + else if (args.ProxySession.Request.SendChunked) { - args.ProxyRequest.AllowWriteStreamBuffering = true; - try { while (true) { - var chuchkHead = args.ClientStreamReader.ReadLine(); + var chuchkHead = args.Client.ClientStreamReader.ReadLine(); var chunkSize = int.Parse(chuchkHead, NumberStyles.HexNumber); if (chunkSize != 0) { - var buffer = args.ClientStreamReader.ReadBytes(chunkSize); + var buffer = args.Client.ClientStreamReader.ReadBytes(chunkSize); postStream.Write(buffer, 0, buffer.Length); //chunk trail - args.ClientStreamReader.ReadLine(); + args.Client.ClientStreamReader.ReadLine(); } else { - args.ClientStreamReader.ReadLine(); + args.Client.ClientStreamReader.ReadLine(); break; } } - - postStream.Close(); } catch { - postStream.Close(); - postStream.Dispose(); - throw; } } diff --git a/Titanium.Web.Proxy/ResponseHandler.cs b/Titanium.Web.Proxy/ResponseHandler.cs index 946cbfc7a..444098b70 100644 --- a/Titanium.Web.Proxy/ResponseHandler.cs +++ b/Titanium.Web.Proxy/ResponseHandler.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; +using System.Threading.Tasks; using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Extensions; using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Network; using Titanium.Web.Proxy.Models; namespace Titanium.Web.Proxy @@ -17,73 +20,64 @@ partial class ProxyServer //Called asynchronously when a request was successfully and we received the response private static void HandleHttpSessionResponse(SessionEventArgs args) { + args.ProxySession.ReceiveResponse(); try { - args.ServerResponse = (HttpWebResponse)args.ProxyRequest.GetResponse(); - } - catch (WebException webEx) - { - //Things line 404, 500 etc - args.ServerResponse = webEx.Response as HttpWebResponse; - } - - try - { - if (args.ServerResponse != null) - { - args.ResponseHeaders = ReadResponseHeaders(args.ServerResponse); - args.ResponseStream = args.ServerResponse.GetResponseStream(); + args.ProxySession.Response.ResponseHeaders = ReadResponseHeaders(args.ProxySession); + args.ProxySession.Response.ResponseStream = args.ProxySession.ProxyClient.ServerStreamReader.BaseStream; - if (BeforeResponse != null) - { - args.ResponseEncoding = args.ServerResponse.GetEncoding(); - BeforeResponse(null, args); - } - args.ResponseLocked = true; + if (BeforeResponse != null) + { + args.ProxySession.Response.Encoding = args.ProxySession.GetResponseEncoding(); + BeforeResponse(null, args); + } - if (args.ResponseBodyRead) - { - var isChunked = args.ServerResponse.GetResponseHeader("transfer-encoding").ToLower().Contains("chunked"); - var contentEncoding = args.ServerResponse.ContentEncoding; + args.ProxySession.Response.ResponseLocked = true; - switch (contentEncoding.ToLower()) - { - case "gzip": - args.ResponseBody = CompressionHelper.CompressGzip(args.ResponseBody); - break; - case "deflate": - args.ResponseBody = CompressionHelper.CompressDeflate(args.ResponseBody); - break; - case "zlib": - args.ResponseBody = CompressionHelper.CompressZlib(args.ResponseBody); - break; - } + if (args.ProxySession.Response.ResponseBodyRead) + { + var isChunked = args.ProxySession.Response.IsChunked; + var contentEncoding = args.ProxySession.Response.ContentEncoding; - WriteResponseStatus(args.ServerResponse.ProtocolVersion, args.ServerResponse.StatusCode, - args.ServerResponse.StatusDescription, args.ClientStreamWriter); - WriteResponseHeaders(args.ClientStreamWriter, args.ResponseHeaders, args.ResponseBody.Length, - isChunked); - WriteResponseBody(args.ClientStream, args.ResponseBody, isChunked); - } - else + if(contentEncoding!=null) + switch (contentEncoding) { - var isChunked = args.ServerResponse.GetResponseHeader("transfer-encoding").ToLower().Contains("chunked"); - - WriteResponseStatus(args.ServerResponse.ProtocolVersion, args.ServerResponse.StatusCode, - args.ServerResponse.StatusDescription, args.ClientStreamWriter); - WriteResponseHeaders(args.ClientStreamWriter, args.ResponseHeaders); - WriteResponseBody(args.ResponseStream, args.ClientStream, isChunked); + case "gzip": + args.ProxySession.Response.ResponseBody = CompressionHelper.CompressGzip(args.ProxySession.Response.ResponseBody); + break; + case "deflate": + args.ProxySession.Response.ResponseBody = CompressionHelper.CompressDeflate(args.ProxySession.Response.ResponseBody); + break; + case "zlib": + args.ProxySession.Response.ResponseBody = CompressionHelper.CompressZlib(args.ProxySession.Response.ResponseBody); + break; } - args.ClientStream.Flush(); + WriteResponseStatus(args.ProxySession.Response.HttpVersion, args.ProxySession.Response.ResponseStatusCode, + args.ProxySession.Response.ResponseStatusDescription, args.Client.ClientStreamWriter); + WriteResponseHeaders(args.Client.ClientStreamWriter, args.ProxySession.Response.ResponseHeaders, args.ProxySession.Response.ResponseBody.Length, + isChunked); + WriteResponseBody(args.Client.ClientStream, args.ProxySession.Response.ResponseBody, isChunked); + } + else + { + WriteResponseStatus(args.ProxySession.Response.HttpVersion, args.ProxySession.Response.ResponseStatusCode, + args.ProxySession.Response.ResponseStatusDescription, args.Client.ClientStreamWriter); + WriteResponseHeaders(args.Client.ClientStreamWriter, args.ProxySession.Response.ResponseHeaders); + + if (args.ProxySession.Response.IsChunked || args.ProxySession.Response.ContentLength > 0) + WriteResponseBody(args.ProxySession.ProxyClient.ServerStreamReader, args.Client.ClientStream, args.ProxySession.Response.IsChunked, args.ProxySession.Response.ContentLength); } + + args.Client.ClientStream.Flush(); + } catch { - Dispose(args.Client, args.ClientStream, args.ClientStreamReader, args.ClientStreamWriter, args); + Dispose(args.Client.TcpClient, args.Client.ClientStream, args.Client.ClientStreamReader, args.Client.ClientStreamWriter, args); } finally { @@ -91,47 +85,60 @@ private static void HandleHttpSessionResponse(SessionEventArgs args) } } - private static List ReadResponseHeaders(HttpWebResponse response) + private static List ReadResponseHeaders(HttpWebSession response) { - var returnHeaders = new List(); - - string cookieHeaderName = null; - string cookieHeaderValue = null; - - foreach (string headerKey in response.Headers.Keys) + for (var i = 0; i < response.Response.ResponseHeaders.Count; i++) { - if (headerKey.ToLower() == "set-cookie") + switch (response.Response.ResponseHeaders[i].Name.ToLower()) { - cookieHeaderName = headerKey; - cookieHeaderValue = response.Headers[headerKey]; - } - else - returnHeaders.Add(new HttpHeader(headerKey, response.Headers[headerKey])); - } + case "content-length": + response.Response.ContentLength = int.Parse(response.Response.ResponseHeaders[i].Value.Trim()); + break; - if (!string.IsNullOrWhiteSpace(cookieHeaderValue)) - { - response.Headers.Remove(cookieHeaderName); - var cookies = CookieSplitRegEx.Split(cookieHeaderValue); - foreach (var cookie in cookies) - returnHeaders.Add(new HttpHeader("Set-Cookie", cookie)); + case "content-encoding": + response.Response.ContentEncoding = response.Response.ResponseHeaders[i].Value.Trim().ToLower(); + break; + + case "content-type": + if (response.Response.ResponseHeaders[i].Value.Contains(";")) + { + response.Response.ContentType = response.Response.ResponseHeaders[i].Value.Split(';')[0].Trim(); + response.Response.CharacterSet = response.Response.ResponseHeaders[i].Value.Split(';')[1].ToLower().Replace("charset=", string.Empty).Trim(); + } + else + response.Response.ContentType = response.Response.ResponseHeaders[i].Value.ToLower().Trim(); + break; + + case "transfer-encoding": + if (response.Response.ResponseHeaders[i].Value.ToLower().Contains("chunked")) + response.Response.IsChunked = true; + break; + + case "connection": + if (response.Response.ResponseHeaders[i].Value.ToLower().Contains("close")) + response.Response.ResponseKeepAlive = false; + break; + + default: + break; + } } - return returnHeaders; + return response.Response.ResponseHeaders; } - private static void WriteResponseStatus(Version version, HttpStatusCode code, string description, + + private static void WriteResponseStatus(string version, string code, string description, StreamWriter responseWriter) { - var s = string.Format("HTTP/{0}.{1} {2} {3}", version.Major, version.Minor, (int)code, description); - responseWriter.WriteLine(s); + responseWriter.WriteLine(string.Format("{0} {1} {2}", version, code, description)); } private static void WriteResponseHeaders(StreamWriter responseWriter, List headers) { if (headers != null) { - FixProxyHeaders(headers); + FixResponseProxyHeaders(headers); foreach (var header in headers) { @@ -142,23 +149,29 @@ private static void WriteResponseHeaders(StreamWriter responseWriter, List headers) + private static void FixResponseProxyHeaders(List headers) { //If proxy-connection close was returned inform to close the connection - if (headers.Any(x => x.Name.ToLower() == "proxy-connection" && x.Value.ToLower() == "close")) - if (headers.Any(x => x.Name.ToLower() == "connection") == false) + var proxyHeader = headers.FirstOrDefault(x => x.Name.ToLower() == "proxy-connection"); + var connectionHeader = headers.FirstOrDefault(x => x.Name.ToLower() == "connection"); + + if (proxyHeader != null) + if (connectionHeader == null) { - headers.Add(new HttpHeader("connection", "close")); - headers.RemoveAll(x => x.Name.ToLower() == "proxy-connection"); + headers.Add(new HttpHeader("connection", proxyHeader.Value)); } else - headers.Find(x => x.Name.ToLower() == "connection").Value = "close"; + { + connectionHeader.Value = "close"; + } + + headers.RemoveAll(x => x.Name.ToLower() == "proxy-connection"); } private static void WriteResponseHeaders(StreamWriter responseWriter, List headers, int length, bool isChunked) { - FixProxyHeaders(headers); + FixResponseProxyHeaders(headers); if (!isChunked) { @@ -193,39 +206,68 @@ private static void WriteResponseBody(Stream clientStream, byte[] data, bool isC WriteResponseBodyChunked(data, clientStream); } - private static void WriteResponseBody(Stream inStream, Stream outStream, bool isChunked) + private static void WriteResponseBody(CustomBinaryReader inStreamReader, Stream outStream, bool isChunked, int BodyLength) { if (!isChunked) { + int bytesToRead = BUFFER_SIZE; + + if (BodyLength < BUFFER_SIZE) + bytesToRead = BodyLength; + var buffer = new byte[BUFFER_SIZE]; - int bytesRead; - while ((bytesRead = inStream.Read(buffer, 0, buffer.Length)) > 0) + var bytesRead = 0; + var totalBytesRead = 0; + + while ((bytesRead += inStreamReader.BaseStream.Read(buffer, 0, bytesToRead)) > 0) { outStream.Write(buffer, 0, bytesRead); + totalBytesRead += bytesRead; + + if (totalBytesRead == BodyLength) + break; + + bytesRead = 0; + var remainingBytes = (BodyLength - totalBytesRead); + bytesToRead = remainingBytes > BUFFER_SIZE ? BUFFER_SIZE : remainingBytes; } } else - WriteResponseBodyChunked(inStream, outStream); + WriteResponseBodyChunked(inStreamReader, outStream); } //Send chunked response - private static void WriteResponseBodyChunked(Stream inStream, Stream outStream) + private static void WriteResponseBodyChunked(CustomBinaryReader inStreamReader, Stream outStream) { - var buffer = new byte[BUFFER_SIZE]; - - int bytesRead; - while ((bytesRead = inStream.Read(buffer, 0, buffer.Length)) > 0) + while (true) { - var chunkHead = Encoding.ASCII.GetBytes(bytesRead.ToString("x2")); + var chuchkHead = inStreamReader.ReadLine(); + var chunkSize = int.Parse(chuchkHead, NumberStyles.HexNumber); + + if (chunkSize != 0) + { + var buffer = inStreamReader.ReadBytes(chunkSize); + + var chunkHead = Encoding.ASCII.GetBytes(chunkSize.ToString("x2")); + + outStream.Write(chunkHead, 0, chunkHead.Length); + outStream.Write(NewLineBytes, 0, NewLineBytes.Length); + + outStream.Write(buffer, 0, chunkSize); + outStream.Write(NewLineBytes, 0, NewLineBytes.Length); - outStream.Write(chunkHead, 0, chunkHead.Length); - outStream.Write(ChunkTrail, 0, ChunkTrail.Length); - outStream.Write(buffer, 0, bytesRead); - outStream.Write(ChunkTrail, 0, ChunkTrail.Length); + inStreamReader.ReadLine(); + } + else + { + inStreamReader.ReadLine(); + outStream.Write(ChunkEnd, 0, ChunkEnd.Length); + break; + } } - outStream.Write(ChunkEnd, 0, ChunkEnd.Length); + } private static void WriteResponseBodyChunked(byte[] data, Stream outStream) @@ -233,9 +275,9 @@ private static void WriteResponseBodyChunked(byte[] data, Stream outStream) var chunkHead = Encoding.ASCII.GetBytes(data.Length.ToString("x2")); outStream.Write(chunkHead, 0, chunkHead.Length); - outStream.Write(ChunkTrail, 0, ChunkTrail.Length); + outStream.Write(NewLineBytes, 0, NewLineBytes.Length); outStream.Write(data, 0, data.Length); - outStream.Write(ChunkTrail, 0, ChunkTrail.Length); + outStream.Write(NewLineBytes, 0, NewLineBytes.Length); outStream.Write(ChunkEnd, 0, ChunkEnd.Length); } diff --git a/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj b/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj index c0c093880..723d04882 100644 --- a/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj +++ b/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj @@ -50,9 +50,28 @@ False ..\packages\DotNetZip.1.9.7\lib\net20\Ionic.Zip.dll + + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll + + + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll + + + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll + + + ..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.IO.dll + + + + ..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.Runtime.dll + + + ..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.Threading.Tasks.dll + @@ -66,12 +85,14 @@ + + + - @@ -80,6 +101,7 @@ + PreserveNewest @@ -98,6 +120,11 @@ + + + + +