From 5c7d284c9aa2bce0a3ce45c8b5b73a5e12fcbde3 Mon Sep 17 00:00:00 2001 From: KU Date: Tue, 24 Sep 2019 14:37:06 +0900 Subject: [PATCH] Fix: HttpConnectProxy partial support for Titanium/Apache, and add tests. --- Proxy/HttpConnectProxy.cs | 2 +- Test/HttpConnectProxyTest.cs | 204 ++++++++++++++++++++++++++++++----- Test/README.md | 29 +++++ Test/SearchMarkTest.cs | 27 +++++ 4 files changed, 235 insertions(+), 27 deletions(-) create mode 100644 Test/README.md diff --git a/Proxy/HttpConnectProxy.cs b/Proxy/HttpConnectProxy.cs index 741cabf..314da50 100644 --- a/Proxy/HttpConnectProxy.cs +++ b/Proxy/HttpConnectProxy.cs @@ -171,7 +171,7 @@ protected override void ProcessReceive(SocketAsyncEventArgs e) return; } - int responseLength = prevMatched > 0 ? (e.Offset - prevMatched) : (e.Offset + result); + int responseLength = (prevMatched > 0 && result == e.Offset) ? (e.Offset - prevMatched) : result; if (e.Offset + e.BytesTransferred > responseLength + m_LineSeparator.Length) { diff --git a/Test/HttpConnectProxyTest.cs b/Test/HttpConnectProxyTest.cs index d1abadd..53784e3 100644 --- a/Test/HttpConnectProxyTest.cs +++ b/Test/HttpConnectProxyTest.cs @@ -14,48 +14,200 @@ namespace SuperSocket.ClientEngine.Test public class HttpConnectProxyTest { [Fact] - public void TestMatchSecondTime() + public void TestHttp10SimpleOnePacket() { - var server = CreateSimplyRespond(Encoding.ASCII.GetBytes("OK")); - var proxyServer = CreateSimplyRespond(Encoding.ASCII.GetBytes("OK")); + SimulateHttpConnectProxy( + simulateResponseFromProxyServer: proxyServerPeer => + { + SendLine(proxyServerPeer, "HTTP/1.0 200 Connection Established\r\n\r\n"); + }, + verifyCompletedEvent: e => + { + Assert.Null(e.Exception); + Assert.True(e.Connected); + }, + testCopyDataFromLeftToRight: SendAndReceiveHello + ); + } - ManualResetEvent wait = new ManualResetEvent(false); + [Fact] + public void TestHttp11SimpleOnePacket() + { + SimulateHttpConnectProxy( + simulateResponseFromProxyServer: proxyServerPeer => + { + SendLine(proxyServerPeer, "HTTP/1.1 200 Connection Established\r\n\r\n"); + }, + verifyCompletedEvent: e => + { + Assert.Null(e.Exception); + Assert.True(e.Connected); + }, + testCopyDataFromLeftToRight: SendAndReceiveHello + ); + } + + [Fact] + public void TestHttp11ComplexOnePacket() + { + SimulateHttpConnectProxy( + simulateResponseFromProxyServer: proxyServerPeer => + { + SendLine(proxyServerPeer, + "HTTP/1.1 200 Connection Established\r\n" + + "Proxy-agent: Apache/2.2.29 (Win32)\r\n" + + "\r\n" + ); + }, + verifyCompletedEvent: e => + { + Assert.Null(e.Exception); + Assert.True(e.Connected); + }, + testCopyDataFromLeftToRight: SendAndReceiveHello + ); + } + + [Fact] + public void TestHttp11ComplexMultiPacket() + { + SimulateHttpConnectProxy( + simulateResponseFromProxyServer: proxyServerPeer => + { + // Actual response simulation from: Apache/2.2.29 (Win32) + SendLine(proxyServerPeer, "HTTP/1.1 200 Connection Established\r\n"); + + // This leads to "System.Exception: protocol error: more data has been received" + SendLine(proxyServerPeer, "Proxy-agent: Apache/2.2.29 (Win32)\r\n\r\n"); + }, + verifyCompletedEvent: e => + { + Assert.Null(e.Exception); + Assert.True(e.Connected); + }, + testCopyDataFromLeftToRight: SendAndReceiveHello + ); + } + + [Fact] + public void TestHttp11ComplexOnePacketAsForbidden() + { + SimulateHttpConnectProxy( + simulateResponseFromProxyServer: proxyServerPeer => + { + // Actual 403 response simulation from: Apache/2.2.29 (Win32) - var proxy = new HttpConnectProxy(proxyServer.LocalEndPoint); - ProxyEventArgs eventArgs = null; - proxy.Completed += (a, e) => + // This leads to "System.Exception: protocol error: more data has been received" + SendLine(proxyServerPeer, + string.Join("\r\n", + "HTTP/1.1 403 Forbidden", + "Date: Tue, 24 Sep 2019 04:35:48 GMT", + "Content-Length: 216", + "Content-Type: text/html; charset=iso-8859-1", + "", + "", + "", + "403 Forbidden", + "", + "

Forbidden

", + "

You don't have permission to access 192.168.2.181:7", + "on this server.

", + "" + ) + ); + }, + verifyCompletedEvent: e => + { + // This pattern is: NOT SUPPORTED FOR NOW! + Assert.NotNull(e.Exception); + Assert.Equal("protocol error: more data has been received", e.Exception.Message); + Assert.False(e.Connected); + }, + testCopyDataFromLeftToRight: SendAndReceiveHello + ); + } + + + void SimulateHttpConnectProxy( + Action simulateResponseFromProxyServer, + Action verifyCompletedEvent, + Action testCopyDataFromLeftToRight + ) + { + var server = NewTcpPeer(); + var proxyServer = NewTcpPeer(); + + var awaitAtProxyServer = proxyServer.AcceptAsync(); + + var proxy = new HttpConnectProxy(proxyServer.LocalEndPoint, 1024, null); + var eventArgs = (ProxyEventArgs)null; + var eventPulled = new ManualResetEvent(false); + proxy.Completed += (sender, e) => { eventArgs = e; - wait.Set(); + eventPulled.Set(); }; + proxy.Connect(server.LocalEndPoint); - Assert.True(wait.WaitOne(5000)); - Assert.Null(eventArgs.Exception); - Assert.True(eventArgs.Connected); + var proxyServerPeer = awaitAtProxyServer.GetAwaiter().GetResult(); + Assert.Equal($"CONNECT 127.0.0.1:{((IPEndPoint)server.LocalEndPoint).Port} HTTP/1.1\r\n", ReadLine(proxyServerPeer)); + Assert.Equal($"Host: 127.0.0.1:{((IPEndPoint)server.LocalEndPoint).Port}\r\n", ReadLine(proxyServerPeer)); + Assert.Equal($"Proxy-Connection: Keep-Alive\r\n", ReadLine(proxyServerPeer)); + Assert.Equal($"\r\n", ReadLine(proxyServerPeer)); + + simulateResponseFromProxyServer(proxyServerPeer); + + Assert.True(eventPulled.WaitOne(5000)); + + // This verification needs to be ran on xUnit thread. + // Otherwise xUnit cannot identify which test is failure. + verifyCompletedEvent(eventArgs); + + if (eventArgs.Connected) + { + testCopyDataFromLeftToRight?.Invoke(proxyServerPeer, eventArgs.Socket); + } + } + + void SendAndReceiveHello(Socket left, Socket right) + { + var echoMessage = $"HELLO, it is {DateTime.Now.Ticks} now!\r\n"; + + SendLine(left, echoMessage); + Assert.Equal(echoMessage, ReadLine(right)); + } + + void SendLine(Socket socket, string line) + { + Thread.Sleep(100); + socket.Send(Encoding.ASCII.GetBytes(line)); } - Socket CreateSimplyRespond(byte[] data) + string ReadLine(Socket socket) { - var socket = NewTcpLocalBound(); - Task.Run( - () => + byte[] lineBuff = new byte[1024]; + int at = 0; + while (true) + { + int received = socket.Receive(lineBuff, at, 1, SocketFlags.None); + if (received < 0) { - var stream = socket.Accept(); - stream.Send(data); - stream.Shutdown(SocketShutdown.Both); - socket.Dispose(); + break; } - ); - - return socket; + if (lineBuff[at] == 10) + { + at++; + break; + } + at++; + } + return Encoding.ASCII.GetString(lineBuff, 0, at); } - Socket NewTcp() => new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - - Socket NewTcpLocalBound() + Socket NewTcpPeer() { - var socket = NewTcp(); + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); socket.Listen(1); return socket; diff --git a/Test/README.md b/Test/README.md new file mode 100644 index 0000000..27996b5 --- /dev/null +++ b/Test/README.md @@ -0,0 +1,29 @@ +# Test + +Open `../global.json`, and remove `sdk`, and save like: + +```json +{ + "projects": [ + ".", "SuperSocket.ClientEngine", "Test" + ] +} +``` + +Launch test by `dotnet test` + +```bat +D:\Proj\SuperSocket.ClientEngine\Test>dotnet test +???????????????????????... +??????????? + +D:\Proj\SuperSocket.ClientEngine\Test\bin\Debug\netcoreapp1.0\Test.dll(.NETCoreApp,Version=v1.0) ?????? +Microsoft (R) Test Execution Command Line Tool Version 15.9.0 +Copyright (c) Microsoft Corporation. All rights reserved. + +Starting test execution, please wait... + +Total tests: 14. Passed: 14. Failed: 0. Skipped: 0. +Test Run Successful. +Test execution time: 4.7565 Seconds +``` diff --git a/Test/SearchMarkTest.cs b/Test/SearchMarkTest.cs index 593cec0..15eb4a9 100644 --- a/Test/SearchMarkTest.cs +++ b/Test/SearchMarkTest.cs @@ -74,5 +74,32 @@ public void TestMatchSecondTimeWithSearchMarkState() Assert.Equal(0, second.SearchMark(0, second.Length, searchState)); } } + + + [Fact] + public void Test() + { + byte[] first = Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n"); + byte[] second = Encoding.ASCII.GetBytes("Proxy-agent: Apache/2.2.29 (Win32)\r\n\r\n"); + + byte[] mark = Encoding.ASCII.GetBytes("\r\n\r\n"); + + var searchState = new SearchMarkState(mark); + + { + // -1 means: not matched, or partially matched. + Assert.Equal(-1, first.SearchMark(0, first.Length, searchState)); + + // Check if (1 <= searchState.Matched) in case of partial match. + } + { + var prevMatched = searchState.Matched; + Assert.Equal(prevMatched, 2); + + // "\r\n\r\n" is matched on second buffer at second[34] to [37]. + // So prevMatched should be ignored this time. + Assert.Equal(34, second.SearchMark(0, second.Length, searchState)); + } + } } }