Commit 9fcf5c3b authored by jmh76's avatar jmh76

Fixing Expect 100-continue requests #553

parent 4e203c3a
# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
###############################
# Core EditorConfig Options #
###############################
root = true
# All files
[*]
indent_style = space
# Code files
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
charset = utf-8-bom
###############################
# .NET Coding Conventions #
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_readonly_field = true:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
dotnet_prefer_inferred_tuple_names = true:suggestion
dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
###############################
# Naming Conventions #
###############################
# Style Definitions
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
###############################
# C# Coding Conventions #
###############################
[*.cs]
# var preferences
csharp_style_var_for_built_in_types = true:silent
csharp_style_var_when_type_is_apparent = true:silent
csharp_style_var_elsewhere = true:silent
# Expression-bodied members
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
# Null-checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
# Expression-level preferences
csharp_prefer_braces = true:silent
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
###############################
# C# Formatting Rules #
###############################
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Space preferences
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
###############################
# VB Coding Conventions #
###############################
[*.vb]
# Modifier preferences
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
using System;
using System;
using System.IO;
using System.Net;
using System.Text;
......@@ -127,40 +127,18 @@ namespace Titanium.Web.Proxy.Http
await writer.WriteAsync(headerBuilder.ToString(), cancellationToken);
if (enable100ContinueBehaviour)
if (enable100ContinueBehaviour && Request.ExpectContinue)
{
if (Request.ExpectContinue)
// wait for expectation response from server
await ReceiveResponse(cancellationToken);
if (Response.StatusCode == (int)HttpStatusCode.Continue)
{
Request.ExpectationSucceeded = true;
}
else
{
string httpStatus;
try
{
httpStatus = await Connection.Stream.ReadLineAsync(cancellationToken);
if (httpStatus == null)
{
throw new ServerConnectionException("Server connection was closed.");
}
}
catch (Exception e) when (!(e is ServerConnectionException))
{
throw new ServerConnectionException("Server connection was closed.");
}
Response.ParseResponseLine(httpStatus, out _, out int responseStatusCode,
out string responseStatusDescription);
// find if server is willing for expect continue
if (responseStatusCode == (int)HttpStatusCode.Continue
&& responseStatusDescription.EqualsIgnoreCase("continue"))
{
Request.Is100Continue = true;
await Connection.Stream.ReadLineAsync(cancellationToken);
}
else if (responseStatusCode == (int)HttpStatusCode.ExpectationFailed
&& responseStatusDescription.EqualsIgnoreCase("expectation failed"))
{
Request.ExpectationFailed = true;
await Connection.Stream.ReadLineAsync(cancellationToken);
}
Request.ExpectationFailed = true;
}
}
}
......@@ -202,33 +180,6 @@ namespace Titanium.Web.Proxy.Http
Response.StatusCode = statusCode;
Response.StatusDescription = statusDescription;
// For HTTP 1.1 comptibility server may send expect-continue even if not asked for it in request
if (Response.StatusCode == (int)HttpStatusCode.Continue
&& Response.StatusDescription.EqualsIgnoreCase("continue"))
{
// Read the next line after 100-continue
Response.Is100Continue = true;
Response.StatusCode = 0;
await Connection.Stream.ReadLineAsync(cancellationToken);
// now receive response
await ReceiveResponse(cancellationToken);
return;
}
if (Response.StatusCode == (int)HttpStatusCode.ExpectationFailed
&& Response.StatusDescription.EqualsIgnoreCase("expectation failed"))
{
// read next line after expectation failed response
Response.ExpectationFailed = true;
Response.StatusCode = 0;
await Connection.Stream.ReadLineAsync(cancellationToken);
// now receive response
await ReceiveResponse(cancellationToken);
return;
}
// Read the response headers in to unique and non-unique header collections
await HeaderParser.ReadHeaders(Connection.Stream, Response.Headers, cancellationToken);
}
......
......@@ -123,12 +123,12 @@ namespace Titanium.Web.Proxy.Http
}
/// <summary>
/// Did server responsed positively for 100 continue request?
/// Did server respond positively for 100 continue request?
/// </summary>
public bool Is100Continue { get; internal set; }
public bool ExpectationSucceeded { get; internal set; }
/// <summary>
/// Did server responsed negatively for the request for 100 continue?
/// Did server respond negatively for 100 continue request?
/// </summary>
public bool ExpectationFailed { get; internal set; }
......
......@@ -92,16 +92,6 @@ namespace Titanium.Web.Proxy.Http
}
}
/// <summary>
/// Is response 100-continue
/// </summary>
public bool Is100Continue { get; internal set; }
/// <summary>
/// expectation failed returned by server?
/// </summary>
public bool ExpectationFailed { get; internal set; }
/// <summary>
/// Gets the header text.
/// </summary>
......
......@@ -279,7 +279,7 @@ namespace Titanium.Web.Proxy.Network.Tcp
};
//linux has a bug with socket reuse in .net core.
if (proxyServer.ReuseSocket && RunTime.IsWindows || RunTime.IsRunningOnMono)
if (proxyServer.ReuseSocket && (RunTime.IsWindows || RunTime.IsRunningOnMono))
{
tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
......
......@@ -20,6 +20,12 @@ using System.Runtime.InteropServices;
"de7f3ba0bdad35ec2d6057ee1846091b34be2abc3f97dc7e72c16fd4958c15126b12923df76964" +
"7d84922c3f4f3b80ee0ae8e4cb40bc1973b782afb90bb00519fd16adf960f217e23696e7c31654" +
"01d0acd6")]
[assembly: InternalsVisibleTo("Titanium.Web.Proxy.IntegrationTests, PublicKey=" +
"0024000004800000940000000602000000240000525341310004000001000100e7368e0ccc717e" +
"eb4d57d35ad6a8305cbbed14faa222e13869405e92c83856266d400887d857005f1393ffca2b92" +
"de7f3ba0bdad35ec2d6057ee1846091b34be2abc3f97dc7e72c16fd4958c15126b12923df76964" +
"7d84922c3f4f3b80ee0ae8e4cb40bc1973b782afb90bb00519fd16adf960f217e23696e7c31654" +
"01d0acd6")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
......
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
......@@ -152,7 +152,7 @@ namespace Titanium.Web.Proxy
/// <summary>
/// Does this proxy uses the HTTP protocol 100 continue behaviour strictly?
/// Broken 100 contunue implementations on server/client may cause problems if enabled.
/// Broken 100 continue implementations on server/client may cause problems if enabled.
/// Defaults to false.
/// </summary>
public bool Enable100ContinueBehaviour { get; set; }
......
......@@ -169,8 +169,11 @@ namespace Titanium.Web.Proxy
if (request.CancelRequest)
{
// syphon out the request body from client before setting the new body
await args.SyphonOutBodyAsync(true, cancellationToken);
if (!(Enable100ContinueBehaviour && request.ExpectContinue))
{
// syphon out the request body from client before setting the new body
await args.SyphonOutBodyAsync(true, cancellationToken);
}
await handleHttpSessionResponse(args);
......@@ -318,71 +321,42 @@ namespace Titanium.Web.Proxy
var body = request.CompressBodyAndUpdateContentLength();
// if expect continue is enabled then send the headers first
// and see if server would return 100 conitinue
if (request.ExpectContinue)
{
args.HttpClient.SetConnection(connection);
await args.HttpClient.SendRequest(Enable100ContinueBehaviour, args.IsTransparent,
cancellationToken);
}
// set the connection and send request headers
args.HttpClient.SetConnection(connection);
await args.HttpClient.SendRequest(Enable100ContinueBehaviour, args.IsTransparent,
cancellationToken);
// If 100 continue was the response inform that to the client
if (Enable100ContinueBehaviour)
// If a successful 100 continue request was made, inform that to the client and reset response
if (request.ExpectationSucceeded)
{
var clientStreamWriter = args.ProxyClient.ClientStreamWriter;
if (request.Is100Continue)
{
await clientStreamWriter.WriteResponseStatusAsync(args.HttpClient.Response.HttpVersion,
(int)HttpStatusCode.Continue, "Continue", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
else if (request.ExpectationFailed)
{
await clientStreamWriter.WriteResponseStatusAsync(args.HttpClient.Response.HttpVersion,
(int)HttpStatusCode.ExpectationFailed, "Expectation Failed", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
var response = args.HttpClient.Response;
await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion, response.StatusCode,
response.StatusDescription, cancellationToken);
await clientStreamWriter.WriteHeadersAsync(response.Headers, cancellationToken: cancellationToken);
await args.ClearResponse(cancellationToken);
}
// If expect continue is not enabled then set the connectio and send request headers
if (!request.ExpectContinue)
{
args.HttpClient.SetConnection(connection);
await args.HttpClient.SendRequest(Enable100ContinueBehaviour, args.IsTransparent,
cancellationToken);
}
// check if content-length is > 0
if (request.ContentLength > 0)
// send body to server if available
if (request.HasBody)
{
if (request.IsBodyRead)
{
var writer = args.HttpClient.Connection.StreamWriter;
await writer.WriteBodyAsync(body, request.IsChunked, cancellationToken);
}
else
else if (!request.ExpectationFailed)
{
if (!request.ExpectationFailed)
{
if (request.HasBody)
{
HttpWriter writer = args.HttpClient.Connection.StreamWriter;
await args.CopyRequestBodyAsync(writer, TransformationMode.None, cancellationToken);
}
}
// get the request body unless an unsuccessful 100 continue request was made
HttpWriter writer = args.HttpClient.Connection.StreamWriter;
await args.CopyRequestBodyAsync(writer, TransformationMode.None, cancellationToken);
}
}
args.TimeLine["Request Sent"] = DateTime.Now;
// If not expectation failed response was returned by server then parse response
if (!request.ExpectationFailed)
{
await handleHttpSessionResponse(args);
}
// parse and send response
await handleHttpSessionResponse(args);
}
/// <summary>
......
......@@ -25,6 +25,14 @@ namespace Titanium.Web.Proxy
// read response & headers from server
await args.HttpClient.ReceiveResponse(cancellationToken);
// Server may send expect-continue even if not asked for it in request.
// According to spec "the client can simply discard this interim response."
if (args.HttpClient.Response.StatusCode == (int)HttpStatusCode.Continue)
{
await args.ClearResponse(cancellationToken);
await args.HttpClient.ReceiveResponse(cancellationToken);
}
args.TimeLine["Response Received"] = DateTime.Now;
var response = args.HttpClient.Response;
......@@ -92,20 +100,6 @@ namespace Titanium.Web.Proxy
response.Locked = true;
// 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", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
else if (response.ExpectationFailed)
{
await clientStreamWriter.WriteResponseStatusAsync(response.HttpVersion,
(int)HttpStatusCode.ExpectationFailed, "Expectation Failed", cancellationToken);
await clientStreamWriter.WriteLineAsync(cancellationToken);
}
if (!args.IsTransparent)
{
response.Headers.FixProxyHeaders();
......
# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
###############################
# Core EditorConfig Options #
###############################
root = true
# All files
[*]
indent_style = space
# Code files
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
charset = utf-8-bom
###############################
# .NET Coding Conventions #
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_readonly_field = true:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
dotnet_prefer_inferred_tuple_names = true:suggestion
dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
###############################
# Naming Conventions #
###############################
# Style Definitions
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
###############################
# C# Coding Conventions #
###############################
[*.cs]
# var preferences
csharp_style_var_for_built_in_types = true:silent
csharp_style_var_when_type_is_apparent = true:silent
csharp_style_var_elsewhere = true:silent
# Expression-bodied members
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
# Null-checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
# Expression-level preferences
csharp_prefer_braces = true:silent
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
###############################
# C# Formatting Rules #
###############################
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Space preferences
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
###############################
# VB Coding Conventions #
###############################
[*.vb]
# Modifier preferences
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
using Microsoft.AspNetCore.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.IntegrationTests.Helpers;
namespace Titanium.Web.Proxy.IntegrationTests
{
[TestClass]
public class ExpectContinueTests
{
[TestMethod]
public async Task ReverseProxy_GotContinueAndOkResponse()
{
var testSuite = new TestSuite();
var server = testSuite.GetServer();
var continueServer = new HttpContinueServer()
{
ExpectationResponse = HttpStatusCode.Continue,
ResponseBody = "I am server. I received your greetings."
};
server.HandleTcpRequest(continueServer.HandleRequest);
var proxy = testSuite.GetReverseProxy();
proxy.Enable100ContinueBehaviour = true;
proxy.BeforeRequest += (sender, e) =>
{
e.HttpClient.Request.RequestUri = new Uri(server.ListeningTcpUrl);
return Task.CompletedTask;
};
var client = new HttpContinueClient();
var response = await client.Post("localhost", proxy.ProxyEndPoints[0].Port, "Hello server. I am a client.");
Assert.IsNotNull(response, "No response to 'expect: 100-continue' request");
Assert.AreEqual((int)HttpStatusCode.OK, response.StatusCode);
Assert.AreEqual(continueServer.ResponseBody, response.BodyString);
}
[TestMethod]
public async Task ReverseProxy_GotExpectationFailedResponse()
{
var testSuite = new TestSuite();
var server = testSuite.GetServer();
var continueServer = new HttpContinueServer() { ExpectationResponse = HttpStatusCode.ExpectationFailed };
server.HandleTcpRequest(continueServer.HandleRequest);
var proxy = testSuite.GetReverseProxy();
proxy.Enable100ContinueBehaviour = true;
proxy.BeforeRequest += (sender, e) =>
{
e.HttpClient.Request.RequestUri = new Uri(server.ListeningTcpUrl);
return Task.CompletedTask;
};
var client = new HttpContinueClient();
var response = await client.Post("localhost", proxy.ProxyEndPoints[0].Port, "Hello server. I am a client.");
Assert.IsNotNull(response, "No response to 'expect: 100-continue' request");
Assert.AreEqual((int)HttpStatusCode.ExpectationFailed, response.StatusCode);
}
[TestMethod]
public async Task ReverseProxy_GotNotFoundResponse()
{
var testSuite = new TestSuite();
var server = testSuite.GetServer();
var continueServer = new HttpContinueServer() { ExpectationResponse = HttpStatusCode.NotFound };
server.HandleTcpRequest(continueServer.HandleRequest);
var proxy = testSuite.GetReverseProxy();
proxy.Enable100ContinueBehaviour = true;
proxy.BeforeRequest += (sender, e) =>
{
e.HttpClient.Request.RequestUri = new Uri(server.ListeningTcpUrl);
return Task.CompletedTask;
};
var client = new HttpContinueClient();
var response = await client.Post("localhost", proxy.ProxyEndPoints[0].Port, "Hello server. I am a client.");
Assert.IsNotNull(response, "No response to 'expect: 100-continue' request");
Assert.AreEqual((int)HttpStatusCode.NotFound, response.StatusCode);
}
[TestMethod]
public async Task ReverseProxy_BeforeRequestThrows()
{
var testSuite = new TestSuite();
var server = testSuite.GetServer();
var continueServer = new HttpContinueServer() { ExpectationResponse = HttpStatusCode.Continue };
server.HandleTcpRequest(continueServer.HandleRequest);
var dbzEx = new DivideByZeroException("Undefined");
var dbzString = $"{dbzEx.GetType()}: {dbzEx.Message}";
var proxy = testSuite.GetReverseProxy();
proxy.Enable100ContinueBehaviour = true;
proxy.BeforeRequest += (sender, e) =>
{
try
{
e.HttpClient.Request.RequestUri = new Uri(server.ListeningTcpUrl);
throw dbzEx;
}
catch
{
var serverError = new Response(Encoding.ASCII.GetBytes(dbzString))
{
HttpVersion = new Version(1, 1),
StatusCode = (int)HttpStatusCode.InternalServerError,
StatusDescription = HttpStatusCode.InternalServerError.ToString()
};
e.Respond(serverError);
}
return Task.CompletedTask;
};
var client = new HttpContinueClient();
var response = await client.Post("localhost", proxy.ProxyEndPoints[0].Port, "Hello server. I am a client.");
Assert.IsNotNull(response, "No response to 'expect: 100-continue' request");
Assert.AreEqual(response.StatusCode, (int)HttpStatusCode.InternalServerError);
Assert.AreEqual(response.BodyString, dbzString);
}
}
}
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
namespace Titanium.Web.Proxy.IntegrationTests.Helpers
{
class HttpContinueClient
{
private static Encoding MsgEncoding = HttpHelper.GetEncodingFromContentType(null);
public async Task<Response> Post(string server, int port, string content)
{
var message = MsgEncoding.GetBytes(content);
var client = new TcpClient(server, port);
client.SendTimeout = client.ReceiveTimeout = 500;
var request = new Request
{
Method = "POST",
OriginalUrl = "/",
HttpVersion = new Version(1, 1)
};
request.Headers.AddHeader(KnownHeaders.Host, server);
request.Headers.AddHeader(KnownHeaders.ContentLength, message.Length.ToString());
request.Headers.AddHeader(KnownHeaders.Expect, KnownHeaders.Expect100Continue);
var header = MsgEncoding.GetBytes(request.HeaderText);
await client.GetStream().WriteAsync(header, 0, header.Length);
var buffer = new byte[1024];
var responseMsg = string.Empty;
Response response = null;
while ((response = HttpMessageParsing.ParseResponse(responseMsg)) == null)
{
var readTask = client.GetStream().ReadAsync(buffer, 0, 1024);
if (!readTask.Wait(200))
return null;
responseMsg += MsgEncoding.GetString(buffer, 0, readTask.Result);
}
if (response.StatusCode == 100)
{
await client.GetStream().WriteAsync(message);
responseMsg = string.Empty;
while ((response = HttpMessageParsing.ParseResponse(responseMsg)) == null)
{
var readTask = client.GetStream().ReadAsync(buffer, 0, 1024);
if (!readTask.Wait(200))
return null;
responseMsg += MsgEncoding.GetString(buffer, 0, readTask.Result);
}
return response;
}
else
{
return response;
}
}
}
}
using System;
using System.IO.Pipelines;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Http;
namespace Titanium.Web.Proxy.IntegrationTests.Helpers
{
class HttpContinueServer
{
public HttpStatusCode ExpectationResponse;
public string ResponseBody;
private static Encoding MsgEncoding = HttpHelper.GetEncodingFromContentType(null);
public async Task HandleRequest(ConnectionContext context)
{
var request = await ReadHeaders(context.Transport.Input);
if (request.ExpectContinue)
{
var respondContinue = new Response
{
HttpVersion = request.HttpVersion,
StatusCode = (int)ExpectationResponse,
StatusDescription = ExpectationResponse.ToString()
};
await context.Transport.Output.WriteAsync(MsgEncoding.GetBytes(respondContinue.HeaderText));
if (ExpectationResponse != HttpStatusCode.Continue)
return;
}
request = await ReadBody(request, context.Transport.Input);
var responseMsg = MsgEncoding.GetBytes(ResponseBody);
var respondOk = new Response(responseMsg)
{
HttpVersion = new Version(1, 1),
StatusCode = (int)HttpStatusCode.OK,
StatusDescription = HttpStatusCode.OK.ToString()
};
await context.Transport.Output.WriteAsync(MsgEncoding.GetBytes(respondOk.HeaderText));
await context.Transport.Output.WriteAsync(responseMsg);
context.Transport.Output.Complete();
}
private async Task<Request> ReadHeaders(PipeReader input)
{
Request request = null;
try
{
var requestMsg = string.Empty;
while ((request = HttpMessageParsing.ParseRequest(requestMsg, false)) == null)
{
var result = await input.ReadAsync();
foreach (var seg in result.Buffer)
requestMsg += MsgEncoding.GetString(seg.Span);
input.AdvanceTo(result.Buffer.End);
}
}
catch (Exception ex)
{
Console.WriteLine($"{ex.GetType()}: {ex.Message}");
}
return request;
}
private async Task<Request> ReadBody(Request request, PipeReader input)
{
var msg = request.HeaderText;
try
{
while ((request = HttpMessageParsing.ParseRequest(msg, true)) == null)
{
var result = await input.ReadAsync();
foreach (var seg in result.Buffer)
msg += MsgEncoding.GetString(seg.Span);
input.AdvanceTo(result.Buffer.End);
}
}
catch (Exception ex)
{
Console.WriteLine($"{ex.GetType()}: {ex.Message}");
}
return request;
}
}
}
using System.IO;
using System.Text;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Shared;
namespace Titanium.Web.Proxy.IntegrationTests.Helpers
{
internal static class HttpMessageParsing
{
/// <summary>
/// This is a terribly inefficient way of reading & parsing an
/// http request, but it's good enough for testing purposes.
/// </summary>
/// <param name="messageText">The request message</param>
/// <returns>Request object if message complete, null otherwise</returns>
internal static Request ParseRequest(string messageText, bool requireBody)
{
var reader = new StringReader(messageText);
var line = reader.ReadLine();
if (string.IsNullOrEmpty(line))
return null;
try
{
Request.ParseRequestLine(line, out var method, out var url, out var version);
RequestResponseBase request = new Request()
{
Method = method,
OriginalUrl = url,
HttpVersion = version
};
while (!string.IsNullOrEmpty(line = reader.ReadLine()))
{
var header = line.Split(ProxyConstants.ColonSplit, 2);
request.Headers.AddHeader(header[0], header[1]);
}
// First zero-length line denotes end of headers. If we
// didn't get one, then we're not done with request
if (line?.Length != 0)
return null;
if (!requireBody)
return request as Request;
if (ParseBody(reader, ref request))
return request as Request;
}
catch { }
return null;
}
/// <summary>
/// This is a terribly inefficient way of reading & parsing an
/// http response, but it's good enough for testing purposes.
/// </summary>
/// <param name="messageText">The response message</param>
/// <returns>Response object if message complete, null otherwise</returns>
internal static Response ParseResponse(string messageText)
{
var reader = new StringReader(messageText);
var line = reader.ReadLine();
if (string.IsNullOrEmpty(line))
return null;
try
{
Response.ParseResponseLine(line, out var version, out var status, out var desc);
RequestResponseBase response = new Response()
{
HttpVersion = version,
StatusCode = status,
StatusDescription = desc
};
while (!string.IsNullOrEmpty(line = reader.ReadLine()))
{
var header = line.Split(ProxyConstants.ColonSplit, 2);
response.Headers.AddHeader(header[0], header[1]);
}
// First zero-length line denotes end of headers. If we
// didn't get one, then we're not done with response
if (line?.Length != 0)
return null;
if (ParseBody(reader, ref response))
return response as Response;
}
catch { }
return null;
}
private static bool ParseBody(StringReader reader, ref RequestResponseBase obj)
{
obj.OriginalContentLength = obj.ContentLength;
if (obj.ContentLength <= 0)
{
// no body, done
return true;
}
else
{
obj.Body = Encoding.ASCII.GetBytes(reader.ReadToEnd());
if (obj.ContentLength == obj.OriginalContentLength)
return true; // done reading body
else
return false; // not done reading body
}
}
}
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using System;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace Titanium.Web.Proxy.IntegrationTests.Setup
......@@ -18,9 +18,11 @@ namespace Titanium.Web.Proxy.IntegrationTests.Setup
{
public string ListeningHttpUrl => $"http://localhost:{HttpListeningPort}";
public string ListeningHttpsUrl => $"https://localhost:{HttpsListeningPort}";
public string ListeningTcpUrl => $"http://localhost:{TcpListeningPort}";
public int HttpListeningPort { get; private set; }
public int HttpsListeningPort { get; private set; }
public int TcpListeningPort { get; private set; }
private IWebHost host;
public TestServer(X509Certificate2 serverCertificate)
......@@ -39,6 +41,18 @@ namespace Titanium.Web.Proxy.IntegrationTests.Setup
{
listenOptions.UseHttps(serverCertificate);
});
options.Listen(IPAddress.Loopback, 0, listenOptions =>
{
listenOptions.Run(context =>
{
if (tcpRequestHandler == null)
{
throw new Exception("Test server not configured to handle tcp request.");
}
return tcpRequestHandler(context);
});
});
})
.Build();
......@@ -53,14 +67,24 @@ namespace Titanium.Web.Proxy.IntegrationTests.Setup
string httpsAddress = addresses[1];
HttpsListeningPort = int.Parse(httpsAddress.Split(':')[2]);
string tcpAddress = addresses[2];
TcpListeningPort = int.Parse(tcpAddress.Split(':')[2]);
}
Func<HttpContext, Task> requestHandler = null;
Func<ConnectionContext, Task> tcpRequestHandler = null;
public void HandleRequest(Func<HttpContext, Task> requestHandler)
{
this.requestHandler = requestHandler;
}
public void HandleTcpRequest(Func<ConnectionContext, Task> tcpRequestHandler)
{
this.tcpRequestHandler = tcpRequestHandler;
}
public void Dispose()
{
host.StopAsync().Wait();
......
......@@ -2,6 +2,8 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AssemblyOriginatorKeyFile>StrongNameKey.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<ItemGroup>
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\.build\lib\rootCert.pfx" Link="rootCert.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.4.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj" />
</ItemGroup>
</Project>

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28307.136
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Titanium.Web.Proxy.IntegrationTests", "Titanium.Web.Proxy.IntegrationTests.csproj", "{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Titanium.Web.Proxy", "..\..\src\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj", "{F407A98E-290D-4A29-9451-22A6AE380586}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}.Debug|x64.ActiveCfg = Debug|Any CPU
{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}.Debug|x64.Build.0 = Debug|Any CPU
{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}.Release|Any CPU.Build.0 = Release|Any CPU
{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}.Release|x64.ActiveCfg = Release|Any CPU
{BE0BC910-468F-4F1A-AC3D-F0EC2C75108E}.Release|x64.Build.0 = Release|Any CPU
{F407A98E-290D-4A29-9451-22A6AE380586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F407A98E-290D-4A29-9451-22A6AE380586}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F407A98E-290D-4A29-9451-22A6AE380586}.Debug|x64.ActiveCfg = Debug|x64
{F407A98E-290D-4A29-9451-22A6AE380586}.Debug|x64.Build.0 = Debug|x64
{F407A98E-290D-4A29-9451-22A6AE380586}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F407A98E-290D-4A29-9451-22A6AE380586}.Release|Any CPU.Build.0 = Release|Any CPU
{F407A98E-290D-4A29-9451-22A6AE380586}.Release|x64.ActiveCfg = Release|x64
{F407A98E-290D-4A29-9451-22A6AE380586}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B518E259-A504-4396-8138-8BFF322FAC80}
EndGlobalSection
EndGlobal
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