diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln index c55af22354..7615861212 100644 --- a/KestrelHttpServer.sln +++ b/KestrelHttpServer.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Server.Kestrel", "src\Microsoft.AspNetCore.Server.Kestrel\Microsoft.AspNetCore.Server.Kestrel.xproj", "{F510611A-3BEE-4B88-A613-5F4A74ED82A1}" EndProject @@ -32,6 +32,11 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Server EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Server.Kestrel.FunctionalTests", "test\Microsoft.AspNetCore.Server.Kestrel.FunctionalTests\Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.xproj", "{9559A5F1-080C-4909-B6CF-7E4B3DC55748}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{0EF2ACDF-012F-4472-A13A-4272419E2903}" + ProjectSection(SolutionItems) = preProject + test\shared\HttpClientSlim.cs = test\shared\HttpClientSlim.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -78,5 +83,6 @@ Global {BD2D4D29-1BD9-40D0-BB31-337D5416B63C} = {327F7880-D9AF-46BD-B45C-3B7E34A01DFD} {5F64B3C3-0C2E-431A-B820-A81BBFC863DA} = {2D5D5227-4DBD-499A-96B1-76A36B03B750} {9559A5F1-080C-4909-B6CF-7E4B3DC55748} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} + {0EF2ACDF-012F-4472-A13A-4272419E2903} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} EndGlobalSection EndGlobal diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs index 24ed52ff81..a438309246 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; +using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Xunit; @@ -85,10 +86,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { host.Start(); - var client = new HttpClientSlim() { ValidateCertificate = false }; foreach (var testUrl in testUrls(host.ServerFeatures.Get())) { - var response = await client.GetStringAsync(testUrl); + var response = await HttpClientSlim.GetStringAsync(testUrl, validateCertificate: false); // Compare the response with Uri.ToString(), rather than testUrl directly. // Required to handle IPv6 addresses with zone index, like "fe80::3%1" diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlim.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlim.cs deleted file mode 100644 index 8775f93eff..0000000000 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlim.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests -{ - // Lightweight version of HttpClient implemented using Socket and SslStream - public class HttpClientSlim - { - public bool ValidateCertificate { get; set; } = true; - - public Task GetStringAsync(string requestUri) => GetStringAsync(new Uri(requestUri)); - - public async Task GetStringAsync(Uri requestUri) - { - using (var stream = await GetStream(requestUri)) - { - using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true)) - { - await writer.WriteAsync($"GET {requestUri.PathAndQuery} HTTP/1.0\r\n"); - await writer.WriteAsync($"Host: {requestUri.Authority}\r\n"); - await writer.WriteAsync("\r\n"); - } - - using (var reader = new StreamReader(stream, Encoding.ASCII)) - { - var response = await reader.ReadToEndAsync(); - var body = response.Substring(response.IndexOf("\r\n\r\n") + 4); - return body; - } - } - } - - private async Task GetStream(Uri requestUri) - { - var socket = await GetSocket(requestUri); - Stream stream = new NetworkStream(socket, ownsSocket: true); - - if (requestUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) - { - var sslStream = new SslStream(stream, leaveInnerStreamOpen: false, userCertificateValidationCallback: - ValidateCertificate ? null : (RemoteCertificateValidationCallback)((a, b, c, d) => true)); - await sslStream.AuthenticateAsClientAsync(requestUri.Host, clientCertificates: null, - enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12, - checkCertificateRevocation: ValidateCertificate); - return sslStream; - } - else - { - return stream; - } - } - - private static async Task GetSocket(Uri requestUri) - { - var tcs = new TaskCompletionSource(); - - var socketArgs = new SocketAsyncEventArgs(); - socketArgs.RemoteEndPoint = new DnsEndPoint(requestUri.DnsSafeHost, requestUri.Port); - socketArgs.Completed += (s, e) => tcs.TrySetResult(e.ConnectSocket); - - // Must use static ConnectAsync(), since instance Connect() does not support DNS names on OSX/Linux. - if (Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, socketArgs)) - { - await tcs.Task; - } - - var socket = socketArgs.ConnectSocket; - - if (socket == null) - { - throw new SocketException((int)socketArgs.SocketError); - } - else - { - return socket; - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlimTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlimTests.cs new file mode 100644 index 0000000000..913f160902 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/HttpClientSlimTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class HttpClientSlimTests + { + [Fact] + public async Task GetStringAsyncHttp() + { + using (var host = StartHost()) + { + Assert.Equal("test", await HttpClientSlim.GetStringAsync(host.GetUri())); + } + } + + [Fact] + public async Task GetStringAsyncHttps() + { + using (var host = StartHost(protocol: "https")) + { + Assert.Equal("test", await HttpClientSlim.GetStringAsync(host.GetUri(), validateCertificate: false)); + } + } + + [Fact] + public async Task GetStringAsyncThrowsForErrorResponse() + { + using (var host = StartHost(statusCode: 500)) + { + await Assert.ThrowsAnyAsync(() => HttpClientSlim.GetStringAsync(host.GetUri())); + } + } + + [Fact] + public async Task PostAsyncHttp() + { + using (var host = StartHost(handler: (context) => context.Request.Body.CopyToAsync(context.Response.Body))) + { + Assert.Equal("test post", await HttpClientSlim.PostAsync(host.GetUri(), new StringContent("test post"))); + } + } + + [Fact] + public async Task PostAsyncHttps() + { + using (var host = StartHost(protocol: "https", + handler: (context) => context.Request.Body.CopyToAsync(context.Response.Body))) + { + Assert.Equal("test post", await HttpClientSlim.PostAsync(host.GetUri(), + new StringContent("test post"), validateCertificate: false)); + } + } + + [Fact] + public async Task PostAsyncThrowsForErrorResponse() + { + using (var host = StartHost(statusCode: 500)) + { + await Assert.ThrowsAnyAsync( + () => HttpClientSlim.PostAsync(host.GetUri(), new StringContent(""))); + } + } + + private IWebHost StartHost(string protocol = "http", int statusCode = 200, Func handler = null) + { + var host = new WebHostBuilder() + .UseUrls($"{protocol}://127.0.0.1:0") + .UseKestrel(options => + { + options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + }) + .Configure((app) => + { + app.Run(context => + { + context.Response.StatusCode = statusCode; + if (handler == null) + { + return context.Response.WriteAsync("test"); + } + else + { + return handler(context); + } + }); + }) + .Build(); + + host.Start(); + return host; + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs index 625a10a7af..f2702be8f7 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Hosting { public static string GetHost(this IWebHost host) { - return host.GetUris().First().Host; + return host.GetUri().Host; } public static int GetPort(this IWebHost host) @@ -40,5 +40,10 @@ namespace Microsoft.AspNetCore.Hosting .Select(a => a.Replace("://+", "://localhost")) .Select(a => new Uri(a)); } + + public static Uri GetUri(this IWebHost host) + { + return host.GetUris().First(); + } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/project.json b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/project.json index c2d82b3f4c..07b88977b3 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/project.json +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/project.json @@ -31,6 +31,11 @@ }, "buildOptions": { "allowUnsafe": true, + "compile": { + "include": [ + "../shared/**/*.cs" + ] + }, "copyToOutput": { "include": "TestResources/testCert.pfx" } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionFilterTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionFilterTests.cs index 7946845290..a0ae26490b 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionFilterTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/HttpsConnectionFilterTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; using System.Net.Http; using System.Net.Security; using System.Net.Sockets; @@ -17,47 +16,19 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Filter; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; -using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.Testing; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests { - public class HttpsConnectionFilterTests : IDisposable + public class HttpsConnectionFilterTests { private static string _serverAddress = "https://127.0.0.1:0/"; - private static RemoteCertificateValidationCallback _alwaysValidCallback = - (sender, cert, chain, sslPolicyErrors) => true; private static X509Certificate2 _x509Certificate2 = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"); -#if NET451 - static HttpsConnectionFilterTests() - { - // SecurityProtocolType values below not available in Mono < 4.3 - const int SecurityProtocolTypeTls11 = 768; - const int SecurityProtocolTypeTls12 = 3072; - ServicePointManager.SecurityProtocol |= (SecurityProtocolType)(SecurityProtocolTypeTls12 | SecurityProtocolTypeTls11); - } -#endif - - public HttpsConnectionFilterTests() - { -#if NET451 - ServicePointManager.ServerCertificateValidationCallback += _alwaysValidCallback; -#endif - } - - public void Dispose() - { -#if NET451 - ServicePointManager.ServerCertificateValidationCallback -= _alwaysValidCallback; -#endif - } - // https://github.com/aspnet/KestrelHttpServer/issues/240 // This test currently fails on mono because of an issue with SslStream. - [ConditionalFact] - [OSSkipCondition(OperatingSystems.Linux, SkipReason = "WinHttpHandler not available on non-Windows.")] - [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "WinHttpHandler not available on non-Windows.")] + [Fact] public async Task CanReadAndWriteWithHttpsConnectionFilter() { var serviceContext = new TestServiceContext(new HttpsConnectionFilter( @@ -68,20 +39,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var server = new TestServer(App, serviceContext, _serverAddress)) { - using (var client = new HttpClient(GetHandler())) - { - var result = await client.PostAsync($"https://localhost:{server.Port}/", new FormUrlEncodedContent(new[] { - new KeyValuePair("content", "Hello World?") - })); + var result = await HttpClientSlim.PostAsync($"https://localhost:{server.Port}/", + new FormUrlEncodedContent(new[] { + new KeyValuePair("content", "Hello World?") + }), + validateCertificate: false); - Assert.Equal("content=Hello+World%3F", await result.Content.ReadAsStringAsync()); - } + Assert.Equal("content=Hello+World%3F", result); } } - [ConditionalFact] - [OSSkipCondition(OperatingSystems.Linux, SkipReason = "WinHttpHandler not available on non-Windows.")] - [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "WinHttpHandler not available on non-Windows.")] + [Fact] public async Task RequireCertificateFailsWhenNoCertificate() { var serviceContext = new TestServiceContext(new HttpsConnectionFilter( @@ -95,17 +63,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var server = new TestServer(App, serviceContext, _serverAddress)) { - using (var client = new HttpClient(GetHandler())) - { - await Assert.ThrowsAnyAsync( - () => client.GetAsync($"https://localhost:{server.Port}/")); - } + await Assert.ThrowsAnyAsync( + () => HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/")); } } - [ConditionalFact] - [OSSkipCondition(OperatingSystems.Linux, SkipReason = "WinHttpHandler not available on non-Windows.")] - [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "WinHttpHandler not available on non-Windows.")] + [Fact] public async Task AllowCertificateContinuesWhenNoCertificate() { var serviceContext = new TestServiceContext(new HttpsConnectionFilter( @@ -124,12 +87,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests }, serviceContext, _serverAddress)) { - using (var client = new HttpClient(GetHandler())) - { - var result = await client.GetAsync($"https://localhost:{server.Port}/"); - - Assert.Equal("hello world", await result.Content.ReadAsStringAsync()); - } + var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); + Assert.Equal("hello world", result); } } @@ -203,9 +162,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } } - [ConditionalFact] - [OSSkipCondition(OperatingSystems.Linux, SkipReason = "WinHttpHandler not available on non-Windows.")] - [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "WinHttpHandler not available on non-Windows.")] + [Fact] public async Task HttpsSchemePassedToRequestFeature() { var serviceContext = new TestServiceContext( @@ -219,12 +176,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var server = new TestServer(context => context.Response.WriteAsync(context.Request.Scheme), serviceContext, _serverAddress)) { - using (var client = new HttpClient(GetHandler())) - { - var result = await client.GetAsync($"https://localhost:{server.Port}/"); - - Assert.Equal("https", await result.Content.ReadAsStringAsync()); - } + var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); + Assert.Equal("https", result); } } @@ -423,16 +376,5 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Null(line); } } - - private HttpMessageHandler GetHandler() - { -#if NET451 - return new HttpClientHandler(); -#else - var handler = new WinHttpHandler(); - handler.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; - return handler; -#endif - } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/project.json b/test/Microsoft.AspNetCore.Server.KestrelTests/project.json index 9f10bec8ec..9354978969 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/project.json +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/project.json @@ -33,6 +33,11 @@ }, "buildOptions": { "allowUnsafe": true, + "compile": { + "include": [ + "../shared/**/*.cs" + ] + }, "keyFile": "../../tools/Key.snk", "copyToOutput": { "include": "TestResources/testCert.pfx" diff --git a/test/shared/HttpClientSlim.cs b/test/shared/HttpClientSlim.cs new file mode 100644 index 0000000000..a5d4d9caa2 --- /dev/null +++ b/test/shared/HttpClientSlim.cs @@ -0,0 +1,129 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Testing +{ + // Lightweight version of HttpClient implemented using Socket and SslStream + public static class HttpClientSlim + { + public static Task GetStringAsync(string requestUri, bool validateCertificate = true) + => GetStringAsync(new Uri(requestUri), validateCertificate); + + public static async Task GetStringAsync(Uri requestUri, bool validateCertificate = true) + { + using (var stream = await GetStream(requestUri, validateCertificate)) + { + using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true)) + { + await writer.WriteAsync($"GET {requestUri.PathAndQuery} HTTP/1.0\r\n"); + await writer.WriteAsync($"Host: {requestUri.Authority}\r\n"); + await writer.WriteAsync("\r\n"); + } + + return await ReadResponse(stream); + } + } + + public static Task PostAsync(string requestUri, HttpContent content, bool validateCertificate = true) + => PostAsync(new Uri(requestUri), content, validateCertificate); + + public static async Task PostAsync(Uri requestUri, HttpContent content, bool validateCertificate = true) + { + using (var stream = await GetStream(requestUri, validateCertificate)) + { + using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true)) + { + await writer.WriteAsync($"POST {requestUri.PathAndQuery} HTTP/1.0\r\n"); + await writer.WriteAsync($"Host: {requestUri.Authority}\r\n"); + await writer.WriteAsync($"Content-Type: {content.Headers.ContentType}\r\n"); + await writer.WriteAsync($"Content-Length: {content.Headers.ContentLength}\r\n"); + await writer.WriteAsync("\r\n"); + } + + await content.CopyToAsync(stream); + + return await ReadResponse(stream); + } + } + + private static async Task ReadResponse(Stream stream) + { + using (var reader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: true, + bufferSize: 1024, leaveOpen: true)) + { + var response = await reader.ReadToEndAsync(); + + var status = GetStatus(response); + new HttpResponseMessage(status).EnsureSuccessStatusCode(); + + var body = response.Substring(response.IndexOf("\r\n\r\n") + 4); + return body; + } + + } + + private static HttpStatusCode GetStatus(string response) + { + var statusStart = response.IndexOf(' ') + 1; + var statusEnd = response.IndexOf(' ', statusStart) - 1; + var statusLength = statusEnd - statusStart + 1; + return (HttpStatusCode)int.Parse(response.Substring(statusStart, statusLength)); + } + + private static async Task GetStream(Uri requestUri, bool validateCertificate) + { + var socket = await GetSocket(requestUri); + Stream stream = new NetworkStream(socket, ownsSocket: true); + + if (requestUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + var sslStream = new SslStream(stream, leaveInnerStreamOpen: false, userCertificateValidationCallback: + validateCertificate ? null : (RemoteCertificateValidationCallback)((a, b, c, d) => true)); + await sslStream.AuthenticateAsClientAsync(requestUri.Host, clientCertificates: null, + enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12, + checkCertificateRevocation: validateCertificate); + return sslStream; + } + else + { + return stream; + } + } + + private static async Task GetSocket(Uri requestUri) + { + var tcs = new TaskCompletionSource(); + + var socketArgs = new SocketAsyncEventArgs(); + socketArgs.RemoteEndPoint = new DnsEndPoint(requestUri.DnsSafeHost, requestUri.Port); + socketArgs.Completed += (s, e) => tcs.TrySetResult(e.ConnectSocket); + + // Must use static ConnectAsync(), since instance Connect() does not support DNS names on OSX/Linux. + if (Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, socketArgs)) + { + await tcs.Task; + } + + var socket = socketArgs.ConnectSocket; + + if (socket == null) + { + throw new SocketException((int)socketArgs.SocketError); + } + else + { + return socket; + } + } + } +}