diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs index 10fb4158ed8..a109d60bb04 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs @@ -4,6 +4,8 @@ using NUnit.Framework; +using Xamarin.Android.NetTests; + namespace System.NetTests { [TestFixture, Category ("InetAccess")] @@ -13,8 +15,9 @@ public class ProxyTest { [Test] public void QuoteInvalidQuoteUrlsShouldWork () { + using var server = LocalHttpServer.Start (); try { - string url = "http://www.msftconnecttest.com/connecttest.txt?query&foo|bar"; + string url = $"{server.Url}ok?query&foo|bar"; var request = (HttpWebRequest) WebRequest.Create (url); request.Method = "GET"; var response = (HttpWebResponse) request.GetResponse (); @@ -34,6 +37,8 @@ public void QuoteInvalidQuoteUrlsShouldWork () ex.Status == WebExceptionStatus.Timeout) { Assert.Ignore ($"Ignoring network failure: {ex.Message}"); } + + server.AssertNoUnhandledExceptions (); } } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs index 5a9e23f1c14..3e54aa21819 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs @@ -8,6 +8,8 @@ using NUnit.Framework; +using Xamarin.Android.NetTests; + namespace System.NetTests { // TODO: https://github.com/dotnet/android/issues/10069 [TestFixture, Category ("InetAccess"), Category ("SSL")] @@ -29,6 +31,7 @@ bool ShouldIgnoreException (WebException wex) [Test] public void SslWithinTasksShouldWork () { + using var server = LocalHttpsServer.Start (); var cb = ServicePointManager.ServerCertificateValidationCallback; ServicePointManager.ServerCertificateValidationCallback = (s, cert, chain, policy) => { Console.WriteLine ("# ServerCertificateValidationCallback"); @@ -39,9 +42,7 @@ public void SslWithinTasksShouldWork () Exception exception = null; var thread = new Thread (() => { - string url = "https://dotnet.microsoft.com/"; - - var downloadTask = new WebClient ().DownloadDataTaskAsync (url); + var downloadTask = new WebClient ().DownloadDataTaskAsync (server.OkUri); var completeTask = downloadTask.ContinueWith (t => { Console.WriteLine ("# DownloadDataTaskAsync complete; status={0}; exception={1}", t.Status, t.Exception); status = t.Status; @@ -66,6 +67,7 @@ public void SslWithinTasksShouldWork () throw exception; Assert.AreEqual (TaskStatus.RanToCompletion, status); + server.AssertNoUnhandledExceptions (); } [Test] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/WebSocketTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/WebSocketTests.cs index 7dd880bdf9a..e72f5ec91a7 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/WebSocketTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/WebSocketTests.cs @@ -4,20 +4,23 @@ using System.Threading; using System.Threading.Tasks; +using Xamarin.Android.NetTests; + namespace System.NetTests { [TestFixture] public class WebSocketTests { [Test, Category ("InetAccess")] - [Ignore ("echo.websocket.org is not available anymore")] public void TestSocketConnection() { string testMessage = "This is a test!"; var messageBytes = CustomWebSocket.GetBytes (testMessage); CustomWebSocket.BytesSize = messageBytes.Length; - var result = CustomWebSocket.Connect ("ws://echo.websocket.org", messageBytes).Result; + using var server = LocalWebSocketServer.Start (); + var result = CustomWebSocket.Connect (server.Url, messageBytes).Result; Assert.AreEqual (result, testMessage, $"Socket test failed. Expected: {testMessage}, Received: {result}"); + server.AssertNoUnhandledExceptions (); } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs index 4b1878643e9..154a1fec207 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs @@ -73,11 +73,12 @@ public bool IsBypassed (Uri host) [Test] public void Disposed () { + using var server = LocalHttpServer.Start (); var h = CreateHandler (); h.Dispose (); var c = new HttpClient (h); try { - var t = ConnectIgnoreFailure (() => c.GetAsync ("http://google.com"), out bool connectionFailed); + var t = ConnectIgnoreFailure (() => c.GetAsync (server.OkUri), out bool connectionFailed); if (connectionFailed) return; diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerIntegrationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerIntegrationTests.cs index 8daa6bf191b..b82d486c5a2 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerIntegrationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerIntegrationTests.cs @@ -147,7 +147,7 @@ public void CancelRequestViaProxy () handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; var httpClient = new HttpClient (handler) { - BaseAddress = new Uri ("https://google.com"), + BaseAddress = new Uri ("https://localhost/"), Timeout = TimeSpan.FromMilliseconds (1) }; @@ -273,10 +273,12 @@ public void Send_Invalid () [Test] public void GetString_Many () { + using var server = LocalHttpServer.Start (); var client = new HttpClient (new Xamarin.Android.Net.AndroidMessageHandler ()); - var t1 = client.GetStringAsync ("https://google.com"); - var t2 = client.GetStringAsync ("https://google.com"); + var t1 = client.GetStringAsync (server.OkUri); + var t2 = client.GetStringAsync (server.OkUri); Assert.IsTrue (Task.WaitAll (new [] { t1, t2 }, WaitTimeout)); + server.AssertNoUnhandledExceptions (); } [Test] @@ -285,7 +287,7 @@ public void DisallowAutoRedirect () var listener = CreateListener (l => { using (var response = l.Response) { - response.Redirect("http://xamarin.com/"); + response.Redirect("http://localhost/"); } }); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs index 1b1e7303705..84fc6749e13 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs @@ -111,16 +111,15 @@ public async Task DoesNotDisposeContentStream() public async Task ServerCertificateCustomValidationCallback_ApproveRequest () { bool callbackHasBeenCalled = false; + using var server = LocalHttpsServer.Start (); var handler = new AndroidMessageHandler { ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => { Assert.NotNull (request, "request"); - Assert.AreEqual ("www.microsoft.com", request.RequestUri.Host); + Assert.AreEqual ("localhost", request.RequestUri.Host); Assert.NotNull (cert, "cert"); - Assert.True (cert!.Subject.Contains ("www.microsoft.com"), $"Unexpected certificate subject {cert!.Subject}"); - Assert.True (cert!.Issuer.Contains ("Microsoft"), $"Unexpected certificate issuer {cert!.Issuer}"); + Assert.True (cert.Subject.Contains ("localhost"), $"Unexpected certificate subject {cert.Subject}"); Assert.NotNull (chain, "chain"); - Assert.AreEqual (SslPolicyErrors.None, errors); callbackHasBeenCalled = true; return true; @@ -128,15 +127,17 @@ public async Task ServerCertificateCustomValidationCallback_ApproveRequest () }; var client = new HttpClient (handler); - await client.GetStringAsync ("https://www.microsoft.com/"); + Assert.AreEqual ("OK", await client.GetStringAsync (server.OkUri)); Assert.IsTrue (callbackHasBeenCalled, "custom validation callback hasn't been called"); + server.AssertNoUnhandledExceptions (); } [Test] public async Task ServerCertificateCustomValidationCallback_RejectRequest () { bool callbackHasBeenCalled = false; + using var server = LocalHttpsServer.Start (); var handler = new AndroidMessageHandler { ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => { @@ -146,7 +147,7 @@ public async Task ServerCertificateCustomValidationCallback_RejectRequest () }; var client = new HttpClient (handler); - await AssertRejectsRemoteCertificate (() => client.GetStringAsync ("https://www.microsoft.com/")); + await AssertRejectsRemoteCertificate (() => client.GetStringAsync (server.OkUri)); Assert.IsTrue (callbackHasBeenCalled, "custom validation callback hasn't been called"); } @@ -259,19 +260,23 @@ public async Task AndroidMessageHandlerFollows308PermanentRedirect () public async Task AndroidMessageHandlerSendsClientCertificate ([Values(true, false)] bool setClientCertificateOptionsExplicitly) { using X509Certificate2 certificate = BuildClientCertificate (); + using var server = LocalHttpsServer.Start (requestClientCertificate: true); - using var handler = new AndroidMessageHandler (); + using var handler = new AndroidMessageHandler { + ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => true, + }; if (setClientCertificateOptionsExplicitly) { handler.ClientCertificateOptions = ClientCertificateOption.Manual; } handler.ClientCertificates.Add (certificate); using var client = new HttpClient (handler); - var response = await client.GetAsync ("https://corefx-net-tls.azurewebsites.net/EchoClientCertificate.ashx"); + var response = await client.GetAsync (server.GetUri ("echo-client-certificate")); var content = await response.EnsureSuccessStatusCode ().Content.ReadAsStringAsync (); X509Certificate2 certificate2 = new X509Certificate2 (global::System.Convert.FromBase64String (content)); Assert.AreEqual (certificate.Thumbprint, certificate2.Thumbprint); + server.AssertNoUnhandledExceptions (); } [Test] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs index bd29a60ef4e..55d7b67760a 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs @@ -5,9 +5,11 @@ using System.Net; using System.Net.Sockets; using System.Text; +using System.Threading; using System.Threading.Tasks; #if !NETSTANDARD2_0 using System.Net.Security; +using System.Net.WebSockets; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -238,7 +240,7 @@ static int GetContentLength (string[] headers) return 0; } - static Task HandleRequest (Stream stream, LocalHttpRequest request) + protected virtual Task HandleRequest (Stream stream, LocalHttpRequest request) { switch (request.Path) { #if !NETSTANDARD2_0 @@ -350,7 +352,7 @@ static string GetReasonPhrase (HttpStatusCode statusCode) } } - sealed class LocalHttpRequest + protected sealed class LocalHttpRequest { public LocalHttpRequest (string method, string target, string body) { @@ -402,12 +404,14 @@ sealed class LocalHttpsServer : LocalHttpServer { readonly RSA certificateKey; readonly X509Certificate2 certificate; + readonly bool requestClientCertificate; - LocalHttpsServer (string certificateHost) + LocalHttpsServer (string certificateHost, bool requestClientCertificate) : base ("Local HTTPS server") { certificateKey = RSA.Create (keySizeInBits: 2048); certificate = CreateCertificate (certificateKey, certificateHost); + this.requestClientCertificate = requestClientCertificate; } public byte [] CertificateData { @@ -422,14 +426,14 @@ protected override string Host { get { return "localhost"; } } - public static LocalHttpsServer Start () + public static LocalHttpsServer Start (bool requestClientCertificate = false) { - return Start ("localhost"); + return Start ("localhost", requestClientCertificate); } - public static LocalHttpsServer Start (string certificateHost) + public static LocalHttpsServer Start (string certificateHost, bool requestClientCertificate = false) { - var server = new LocalHttpsServer (certificateHost); + var server = new LocalHttpsServer (certificateHost, requestClientCertificate); server.StartListening (); return server; } @@ -443,11 +447,30 @@ public override void Dispose () protected override async Task GetRequestStream (TcpClient client) { - var sslStream = new SslStream (client.GetStream (), leaveInnerStreamOpen: false); - await sslStream.AuthenticateAsServerAsync (certificate, clientCertificateRequired: false, enabledSslProtocols: SslProtocols.None, checkCertificateRevocation: false).ConfigureAwait (false); + var sslStream = new SslStream (client.GetStream (), leaveInnerStreamOpen: false, userCertificateValidationCallback: (sender, clientCertificate, chain, sslPolicyErrors) => true); + await sslStream.AuthenticateAsServerAsync (certificate, clientCertificateRequired: requestClientCertificate, enabledSslProtocols: SslProtocols.None, checkCertificateRevocation: false).ConfigureAwait (false); return sslStream; } + protected override Task HandleRequest (Stream stream, LocalHttpRequest request) + { + if (request.Path == "/echo-client-certificate") { + return WriteClientCertificateAsync (stream); + } + + return base.HandleRequest (stream, request); + } + + static Task WriteClientCertificateAsync (Stream stream) + { + string clientCertificateData = ""; + if (stream is SslStream sslStream && sslStream.RemoteCertificate != null) { + clientCertificateData = Convert.ToBase64String (sslStream.RemoteCertificate.Export (X509ContentType.Cert)); + } + + return WriteStringAsync (stream, clientCertificateData, "text/plain"); + } + static X509Certificate2 CreateCertificate (RSA key, string certificateHost) { DateTimeOffset start = DateTimeOffset.UtcNow.AddDays (-30); @@ -469,5 +492,166 @@ static X509Certificate2 CreateCertificate (RSA key, string certificateHost) return request.CreateSelfSigned (start, end); } } + + sealed class LocalWebSocketServer : LocalTestServer + { + const string WebSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + readonly TcpListener listener; + Task acceptLoop = Task.CompletedTask; + bool disposed; + + LocalWebSocketServer () + : base ("Local WebSocket server") + { + listener = new TcpListener (IPAddress.Loopback, 0); + } + + public int Port { get; private set; } + + public Uri Uri { + get { return new Uri ($"ws://{LoopbackHost}:{Port}/"); } + } + + public string Url { + get { return Uri.ToString (); } + } + + public static LocalWebSocketServer Start () + { + var server = new LocalWebSocketServer (); + server.StartListening (); + return server; + } + + public override void Dispose () + { + disposed = true; + listener.Stop (); + WaitForShutdown (acceptLoop, inner => inner is ObjectDisposedException || inner is SocketException); + } + + void StartListening () + { + listener.Start (); + Port = ((IPEndPoint) listener.LocalEndpoint).Port; + acceptLoop = Task.Run (AcceptLoop); + } + + async Task AcceptLoop () + { + while (!disposed) { + TcpClient client; + try { + client = await listener.AcceptTcpClientAsync ().ConfigureAwait (false); + } catch (ObjectDisposedException) { + return; + } catch (SocketException) when (disposed) { + return; + } + + _ = Task.Run (() => HandleClient (client)); + } + } + + async Task HandleClient (TcpClient client) + { + using (client) { + bool handshakeCompleted = false; + try { + Stream stream = client.GetStream (); + string key = await ReadWebSocketKeyAsync (stream).ConfigureAwait (false); + if (key == null) { + return; + } + + await WriteHandshakeResponseAsync (stream, key).ConfigureAwait (false); + handshakeCompleted = true; + + using var webSocket = WebSocket.CreateFromStream (stream, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromSeconds (30)); + await EchoLoop (webSocket).ConfigureAwait (false); + } catch (Exception ex) { + if (!(handshakeCompleted && (ex is IOException || ex is ObjectDisposedException || ex is WebSocketException))) { + AddHandlerException (ex); + } + } + } + } + + static async Task ReadWebSocketKeyAsync (Stream stream) + { + byte[] endOfHeaders = Encoding.ASCII.GetBytes ("\r\n\r\n"); + byte[] buffer = new byte [1]; + int matched = 0; + + using var headersStream = new MemoryStream (); + while (headersStream.Length < 64 * 1024) { + int read = await stream.ReadAsync (buffer, 0, buffer.Length).ConfigureAwait (false); + if (read == 0) { + break; + } + + headersStream.WriteByte (buffer [0]); + if (buffer [0] == endOfHeaders [matched]) { + matched++; + if (matched == endOfHeaders.Length) { + break; + } + } else { + matched = buffer [0] == endOfHeaders [0] ? 1 : 0; + } + } + + string headers = Encoding.ASCII.GetString (headersStream.ToArray ()); + foreach (string line in headers.Split (new [] { "\r\n" }, StringSplitOptions.None)) { + if (line.StartsWith ("Sec-WebSocket-Key:", StringComparison.OrdinalIgnoreCase)) { + return line.Substring ("Sec-WebSocket-Key:".Length).Trim (); + } + } + + return null; + } + + static Task WriteHandshakeResponseAsync (Stream stream, string key) + { + string accept; +#pragma warning disable CA5350 // SHA-1 is mandated by the WebSocket handshake (RFC 6455 ยง1.3) + using (var sha1 = SHA1.Create ()) { + byte[] hash = sha1.ComputeHash (Encoding.ASCII.GetBytes (key + WebSocketGuid)); + accept = Convert.ToBase64String (hash); + } +#pragma warning restore CA5350 + + var response = new StringBuilder (); + response.Append ("HTTP/1.1 101 Switching Protocols\r\n"); + response.Append ("Upgrade: websocket\r\n"); + response.Append ("Connection: Upgrade\r\n"); + response.Append ("Sec-WebSocket-Accept: ").Append (accept).Append ("\r\n\r\n"); + + byte[] bytes = Encoding.ASCII.GetBytes (response.ToString ()); + return stream.WriteAsync (bytes, 0, bytes.Length); + } + + static async Task EchoLoop (WebSocket webSocket) + { + byte[] buffer = new byte [4096]; + while (webSocket.State == WebSocketState.Open) { + WebSocketReceiveResult result; + try { + result = await webSocket.ReceiveAsync (new ArraySegment (buffer), CancellationToken.None).ConfigureAwait (false); + } catch (Exception ex) when (ex is WebSocketException || ex is IOException || ex is ObjectDisposedException) { + // The client closed the connection without a WebSocket close handshake. + return; + } + + if (result.MessageType == WebSocketMessageType.Close) { + await webSocket.CloseAsync (WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait (false); + return; + } + + await webSocket.SendAsync (new ArraySegment (buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None).ConfigureAwait (false); + } + } + } #endif }