/// <summary> /// Called asynchronously when a request was successfully and we received the response /// </summary> /// <param name="args"></param> /// <returns>true if client/server connection was terminated (and disposed) </returns> private async Task <bool> HandleHttpSessionResponse(SessionEventArgs args) { try { //read response & headers from server await args.WebSession.ReceiveResponse(); var response = args.WebSession.Response; #if NET45 //check for windows authentication if (EnableWinAuth && !RunTime.IsRunningOnMono && response.ResponseStatusCode == (int)HttpStatusCode.Unauthorized) { bool disposed = await Handle401UnAuthorized(args); if (disposed) { return(true); } } #endif args.ReRequest = false; //If user requested call back then do it if (BeforeResponse != null && !response.ResponseLocked) { await BeforeResponse.InvokeParallelAsync(this, args, ExceptionFunc); } //if user requested to send request again //likely after making modifications from User Response Handler if (args.ReRequest) { //clear current response await args.ClearResponse(); bool disposed = await HandleHttpSessionRequestInternal(args.WebSession.ServerConnection, args, false); return(disposed); } response.ResponseLocked = true; var clientStreamWriter = args.ProxyClient.ClientStreamWriter; //Write back to client 100-conitinue response if that's what server returned if (response.Is100Continue) { await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion, (int)HttpStatusCode.Continue, "Continue"); await clientStreamWriter.WriteLineAsync(); } else if (response.ExpectationFailed) { await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion, (int)HttpStatusCode.ExpectationFailed, "Expectation Failed"); await clientStreamWriter.WriteLineAsync(); } //Write back response status to client await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion, response.ResponseStatusCode, response.ResponseStatusDescription); response.ResponseHeaders.FixProxyHeaders(); if (response.ResponseBodyRead) { bool isChunked = response.IsChunked; string contentEncoding = response.ContentEncoding; if (contentEncoding != null) { response.ResponseBody = await GetCompressedResponseBody(contentEncoding, response.ResponseBody); if (isChunked == false) { response.ContentLength = response.ResponseBody.Length; } else { response.ContentLength = -1; } } await clientStreamWriter.WriteHeadersAsync(response.ResponseHeaders); await clientStreamWriter.WriteResponseBodyAsync(response.ResponseBody, isChunked); } else { await clientStreamWriter.WriteHeadersAsync(response.ResponseHeaders); //Write body if exists if (response.HasBody) { await clientStreamWriter.WriteResponseBodyAsync(BufferSize, args.WebSession.ServerConnection.StreamReader, response.IsChunked, response.ContentLength); } } await clientStreamWriter.FlushAsync(); } catch (Exception e) { ExceptionFunc(new ProxyHttpException("Error occured whilst handling session response", e, args)); Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader, args.ProxyClient.ClientStreamWriter, args.WebSession.ServerConnection); return(true); } return(false); }
/// <summary> /// This is the core request handler method for a particular connection from client /// Will create new session (request/response) sequence until /// client/server abruptly terminates connection or by normal HTTP termination /// </summary> /// <param name="client"></param> /// <param name="httpCmd"></param> /// <param name="clientStream"></param> /// <param name="clientStreamReader"></param> /// <param name="clientStreamWriter"></param> /// <param name="httpsConnectHostname"></param> /// <param name="endPoint"></param> /// <param name="connectRequest"></param> /// <param name="isTransparentEndPoint"></param> /// <returns></returns> private async Task <bool> HandleHttpSessionRequest(TcpClient client, string httpCmd, CustomBufferedStream clientStream, CustomBinaryReader clientStreamReader, HttpResponseWriter clientStreamWriter, string httpsConnectHostname, ProxyEndPoint endPoint, ConnectRequest connectRequest, bool isTransparentEndPoint = false) { bool disposed = false; TcpConnection connection = null; //Loop through each subsequest request on this particular client connection //(assuming HTTP connection is kept alive by client) while (true) { if (string.IsNullOrEmpty(httpCmd)) { break; } var args = new SessionEventArgs(BufferSize, endPoint, HandleHttpSessionResponse) { ProxyClient = { TcpClient = client }, WebSession = { ConnectRequest = connectRequest } }; try { string httpMethod; string httpUrl; Version version; Request.ParseRequestLine(httpCmd, out httpMethod, out httpUrl, out version); //Read the request headers in to unique and non-unique header collections await HeaderParser.ReadHeaders(clientStreamReader, args.WebSession.Request.RequestHeaders); var httpRemoteUri = new Uri(httpsConnectHostname == null ? isTransparentEndPoint ? string.Concat("http://", args.WebSession.Request.Host, httpUrl) : httpUrl : string.Concat("https://", args.WebSession.Request.Host ?? httpsConnectHostname, httpUrl)); args.WebSession.Request.RequestUri = httpRemoteUri; args.WebSession.Request.OriginalRequestUrl = httpUrl; args.WebSession.Request.Method = httpMethod; args.WebSession.Request.HttpVersion = version; args.ProxyClient.ClientStream = clientStream; args.ProxyClient.ClientStreamReader = clientStreamReader; args.ProxyClient.ClientStreamWriter = clientStreamWriter; //proxy authorization check if (httpsConnectHostname == null && await CheckAuthorization(clientStreamWriter, args) == false) { args.Dispose(); break; } PrepareRequestHeaders(args.WebSession.Request.RequestHeaders); args.WebSession.Request.Host = args.WebSession.Request.RequestUri.Authority; #if NET45 //if win auth is enabled //we need a cache of request body //so that we can send it after authentication in WinAuthHandler.cs if (EnableWinAuth && !RunTime.IsRunningOnMono && args.WebSession.Request.HasBody) { await args.GetRequestBody(); } #endif //If user requested interception do it if (BeforeRequest != null) { await BeforeRequest.InvokeParallelAsync(this, args, ExceptionFunc); } if (args.WebSession.Request.CancelRequest) { args.Dispose(); break; } //create a new connection if hostname changes if (connection != null && !connection.HostName.Equals(args.WebSession.Request.RequestUri.Host, StringComparison.OrdinalIgnoreCase)) { connection.Dispose(); UpdateServerConnectionCount(false); connection = null; } if (connection == null) { connection = await GetServerConnection(args, false); } //if upgrading to websocket then relay the requet without reading the contents if (args.WebSession.Request.UpgradeToWebSocket) { //prepare the prefix content var requestHeaders = args.WebSession.Request.RequestHeaders; byte[] requestBytes; using (var ms = new MemoryStream()) using (var writer = new HttpRequestWriter(ms)) { writer.WriteLine(httpCmd); writer.WriteHeaders(requestHeaders); requestBytes = ms.ToArray(); } await connection.Stream.WriteAsync(requestBytes, 0, requestBytes.Length); string httpStatus = await connection.StreamReader.ReadLineAsync(); Version responseVersion; int responseStatusCode; string responseStatusDescription; Response.ParseResponseLine(httpStatus, out responseVersion, out responseStatusCode, out responseStatusDescription); args.WebSession.Response.HttpVersion = responseVersion; args.WebSession.Response.ResponseStatusCode = responseStatusCode; args.WebSession.Response.ResponseStatusDescription = responseStatusDescription; await HeaderParser.ReadHeaders(connection.StreamReader, args.WebSession.Response.ResponseHeaders); await clientStreamWriter.WriteResponseAsync(args.WebSession.Response); //If user requested call back then do it if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked) { await BeforeResponse.InvokeParallelAsync(this, args, ExceptionFunc); } await TcpHelper.SendRaw(clientStream, connection.Stream, (buffer, offset, count) => { args.OnDataSent(buffer, offset, count); }, (buffer, offset, count) => { args.OnDataReceived(buffer, offset, count); }); args.Dispose(); break; } //construct the web request that we are going to issue on behalf of the client. disposed = await HandleHttpSessionRequestInternal(connection, args, false); if (disposed) { //already disposed inside above method args.Dispose(); break; } //if connection is closing exit if (args.WebSession.Response.ResponseKeepAlive == false) { args.Dispose(); break; } args.Dispose(); // read the next request httpCmd = await clientStreamReader.ReadLineAsync(); } catch (Exception e) { ExceptionFunc(new ProxyHttpException("Error occured whilst handling session request", e, args)); break; } } if (!disposed) { Dispose(clientStream, clientStreamReader, clientStreamWriter, connection); } return(true); }
/// <summary> /// Called asynchronously when a request was successfully and we received the response /// </summary> /// <param name="args"></param> /// <returns>true if client/server connection was terminated (and disposed) </returns> private async Task <bool> HandleHttpSessionResponse(SessionEventArgs args) { try { //read response & headers from server await args.WebSession.ReceiveResponse(); if (!args.WebSession.Response.ResponseBodyRead) { args.WebSession.Response.ResponseStream = args.WebSession.ServerConnection.Stream; } //check for windows authentication if (EnableWinAuth && !RunTime.IsRunningOnMono && args.WebSession.Response.ResponseStatusCode == "401") { var disposed = await Handle401UnAuthorized(args); if (disposed) { return(true); } } args.ReRequest = false; //If user requested call back then do it if (BeforeResponse != null && !args.WebSession.Response.ResponseLocked) { await BeforeResponse.InvokeParallelAsync(this, args); } //if user requested to send request again //likely after making modifications from User Response Handler if (args.ReRequest) { //clear current response await args.ClearResponse(); var disposed = await HandleHttpSessionRequestInternal(args.WebSession.ServerConnection, args, false); return(disposed); } args.WebSession.Response.ResponseLocked = true; //Write back to client 100-conitinue response if that's what server returned if (args.WebSession.Response.Is100Continue) { await WriteResponseStatus(args.WebSession.Response.HttpVersion, "100", "Continue", args.ProxyClient.ClientStreamWriter); await args.ProxyClient.ClientStreamWriter.WriteLineAsync(); } else if (args.WebSession.Response.ExpectationFailed) { await WriteResponseStatus(args.WebSession.Response.HttpVersion, "417", "Expectation Failed", args.ProxyClient.ClientStreamWriter); await args.ProxyClient.ClientStreamWriter.WriteLineAsync(); } //Write back response status to client await WriteResponseStatus(args.WebSession.Response.HttpVersion, args.WebSession.Response.ResponseStatusCode, args.WebSession.Response.ResponseStatusDescription, args.ProxyClient.ClientStreamWriter); if (args.WebSession.Response.ResponseBodyRead) { var isChunked = args.WebSession.Response.IsChunked; var contentEncoding = args.WebSession.Response.ContentEncoding; if (contentEncoding != null) { args.WebSession.Response.ResponseBody = await GetCompressedResponseBody(contentEncoding, args.WebSession.Response.ResponseBody); if (isChunked == false) { args.WebSession.Response.ContentLength = args.WebSession.Response.ResponseBody.Length; } else { args.WebSession.Response.ContentLength = -1; } } await WriteResponseHeaders(args.ProxyClient.ClientStreamWriter, args.WebSession.Response); await args.ProxyClient.ClientStream.WriteResponseBody(args.WebSession.Response.ResponseBody, isChunked); } else { await WriteResponseHeaders(args.ProxyClient.ClientStreamWriter, args.WebSession.Response); //Write body only if response is chunked or content length >0 //Is none are true then check if connection:close header exist, if so write response until server or client terminates the connection if (args.WebSession.Response.IsChunked || args.WebSession.Response.ContentLength > 0 || !args.WebSession.Response.ResponseKeepAlive) { await args.WebSession.ServerConnection.StreamReader .WriteResponseBody(BufferSize, args.ProxyClient.ClientStream, args.WebSession.Response.IsChunked, args.WebSession.Response.ContentLength); } //write response if connection:keep-alive header exist and when version is http/1.0 //Because in Http 1.0 server can return a response without content-length (expectation being client would read until end of stream) else if (args.WebSession.Response.ResponseKeepAlive && args.WebSession.Response.HttpVersion.Minor == 0) { await args.WebSession.ServerConnection.StreamReader .WriteResponseBody(BufferSize, args.ProxyClient.ClientStream, args.WebSession.Response.IsChunked, args.WebSession.Response.ContentLength); } } await args.ProxyClient.ClientStream.FlushAsync(); } catch (Exception e) { ExceptionFunc(new ProxyHttpException("Error occured whilst handling session response", e, args)); Dispose(args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamReader, args.ProxyClient.ClientStreamWriter, args.WebSession.ServerConnection); return(true); } return(false); }