diff --git a/WebListener.sln b/WebListener.sln index e2a7a67d0b..c3c21e8fe7 100644 --- a/WebListener.sln +++ b/WebListener.sln @@ -25,6 +25,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Server.Web EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Server.WebListener", "src\Microsoft.AspNet.Server.WebListener\Microsoft.AspNet.Server.WebListener.kproj", "{B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Net.Server.FunctionalTests", "test\Microsoft.Net.Server.FunctionalTests\Microsoft.Net.Server.FunctionalTests.kproj", "{DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -115,6 +117,16 @@ Global {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Release|Mixed Platforms.Build.0 = Release|x86 {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Release|x86.ActiveCfg = Release|x86 {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Release|x86.Build.0 = Release|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Debug|Any CPU.ActiveCfg = Debug|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Debug|x86.ActiveCfg = Debug|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Debug|x86.Build.0 = Debug|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Release|Any CPU.ActiveCfg = Release|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Release|Mixed Platforms.Build.0 = Release|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Release|x86.ActiveCfg = Release|x86 + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -128,5 +140,6 @@ Global {E788AEAE-2CB4-4BFA-8746-D0BB7E93A1BB} = {99D5E5F3-88F5-4CCF-8D8C-717C8925DF09} {4492FF4C-9032-411D-853F-46B01755E504} = {E183C826-1360-4DFF-9994-F33CED5C8525} {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92} = {99D5E5F3-88F5-4CCF-8D8C-717C8925DF09} + {DCB6E0B1-223D-44E6-8696-4767E5B6E6A1} = {E183C826-1360-4DFF-9994-F33CED5C8525} EndGlobalSection EndGlobal diff --git a/src/Microsoft.Net.Server/Resources.Designer.cs b/src/Microsoft.Net.Server/Resources.Designer.cs index b0cf11573e..9fdfa80837 100644 --- a/src/Microsoft.Net.Server/Resources.Designer.cs +++ b/src/Microsoft.Net.Server/Resources.Designer.cs @@ -56,7 +56,7 @@ namespace Microsoft.Net.Server { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Server.WebListener.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Net.Server.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); resourceMan = temp; } return resourceMan; diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs index 8f69f36cc7..3aa245dddb 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs @@ -120,7 +120,7 @@ namespace Microsoft.AspNet.Server.WebListener } */ [Fact] - public void ResponseBody_WriteContentLengthNoneWritten_Throws() + public async Task ResponseBody_WriteContentLengthNoneWritten_Throws() { using (Utilities.CreateHttpServer(env => { @@ -129,7 +129,7 @@ namespace Microsoft.AspNet.Server.WebListener return Task.FromResult(0); })) { - Assert.Throws(() => SendRequestAsync(Address).Result); + await Assert.ThrowsAsync(() => SendRequestAsync(Address)); } } diff --git a/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs b/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs new file mode 100644 index 0000000000..593575806c --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class AuthenticationTests + { + private const string Address = "http://localhost:8080/"; + + [Theory] + [InlineData(AuthenticationType.Kerberos)] + [InlineData(AuthenticationType.Negotiate)] + [InlineData(AuthenticationType.Ntlm)] + [InlineData(AuthenticationType.Digest)] + [InlineData(AuthenticationType.Basic)] + [InlineData(AuthenticationType.Kerberos | AuthenticationType.Negotiate | AuthenticationType.Ntlm | AuthenticationType.Digest | AuthenticationType.Basic)] + public async Task AuthTypes_EnabledButNotChalleneged_PassThrough(AuthenticationType authType) + { + using (var server = Utilities.CreateAuthServer(authType)) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Dispose(); + + var response = await responseTask; + response.EnsureSuccessStatusCode(); + } + } + + [Theory] + [InlineData(AuthenticationType.Kerberos)] + [InlineData(AuthenticationType.Negotiate)] + [InlineData(AuthenticationType.Ntlm)] + // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented + [InlineData(AuthenticationType.Basic)] + public async Task AuthType_Specify401_ChallengesAdded(AuthenticationType authType) + { + using (var server = Utilities.CreateAuthServer(authType)) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.StatusCode = 401; + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); + } + } + + [Fact] + public async Task MultipleAuthTypes_Specify401_ChallengesAdded() + { + using (var server = Utilities.CreateAuthServer( + AuthenticationType.Kerberos + | AuthenticationType.Negotiate + | AuthenticationType.Ntlm + /* | AuthenticationType.Digest TODO: Not implemented */ + | AuthenticationType.Basic)) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.StatusCode = 401; + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal("Kerberos, Negotiate, NTLM, basic", response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); + } + } + /* TODO: User + [Theory] + [InlineData(AuthenticationType.Kerberos)] + [InlineData(AuthenticationType.Negotiate)] + [InlineData(AuthenticationType.Ntlm)] + // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented + // [InlineData(AuthenticationType.Basic)] // Doesn't work with default creds + [InlineData(AuthenticationType.Kerberos | AuthenticationType.Negotiate | AuthenticationType.Ntlm | / *AuthenticationType.Digest |* / AuthenticationType.Basic)] + public async Task AuthTypes_Login_Success(AuthenticationType authType) + { + int requestCount = 0; + using (Utilities.CreateAuthServer(authType, env => + { + requestCount++; + / * // TODO: Expose user as feature. + object obj; + if (env.TryGetValue("server.User", out obj) && obj != null) + { + return Task.FromResult(0); + }* / + new DefaultHttpContext((IFeatureCollection)env).Response.StatusCode = 401; + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address, useDefaultCredentials: true); + response.EnsureSuccessStatusCode(); + } + } + */ + + private async Task SendRequestAsync(string uri, bool useDefaultCredentials = false) + { + HttpClientHandler handler = new HttpClientHandler(); + handler.UseDefaultCredentials = useDefaultCredentials; + using (HttpClient client = new HttpClient(handler)) + { + return await client.GetAsync(uri); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/HttpsTests.cs b/test/Microsoft.Net.Server.FunctionalTests/HttpsTests.cs new file mode 100644 index 0000000000..e4c13293eb --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/HttpsTests.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.IO; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class HttpsTests + { + private const string Address = "https://localhost:9090/"; + + [Fact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + public async Task Https_200OK_Success() + { + using (var server = Utilities.CreateHttpsServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Fact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + public async Task Https_SendHelloWorld_Success() + { + using (var server = Utilities.CreateHttpsServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + byte[] body = Encoding.UTF8.GetBytes("Hello World"); + context.Response.ContentLength = body.Length; + await context.Response.Body.WriteAsync(body, 0, body.Length); + context.Dispose(); + + string response = await responseTask; + Assert.Equal("Hello World", response); + } + } + + [Fact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + public async Task Https_EchoHelloWorld_Success() + { + using (var server = Utilities.CreateHttpsServer()) + { + Task responseTask = SendRequestAsync(Address, "Hello World"); + + var context = await server.GetContextAsync(); + string input = new StreamReader(context.Request.Body).ReadToEnd(); + Assert.Equal("Hello World", input); + context.Response.ContentLength = 11; + using (var writer = new StreamWriter(context.Response.Body)) + { + writer.Write("Hello World"); + } + + string response = await responseTask; + Assert.Equal("Hello World", response); + } + } + + [Fact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + public async Task Https_ClientCertNotSent_ClientCertNotPresent() + { + using (var server = Utilities.CreateHttpsServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + var cert = await context.Request.GetClientCertificateAsync(); + Assert.Null(cert); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Fact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")] + public async Task Https_ClientCertRequested_ClientCertPresent() + { + using (var server = Utilities.CreateHttpsServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + var cert = await context.Request.GetClientCertificateAsync(); + Assert.NotNull(cert); + context.Dispose(); + + X509Certificate2 clientCert = FindClientCert(); + Assert.NotNull(clientCert); + string response = await SendRequestAsync(Address, clientCert); + Assert.Equal(string.Empty, response); + } + } + + private async Task SendRequestAsync(string uri, + X509Certificate cert = null) + { + WebRequestHandler handler = new WebRequestHandler(); + handler.ServerCertificateValidationCallback = (a, b, c, d) => true; + if (cert != null) + { + handler.ClientCertificates.Add(cert); + } + using (HttpClient client = new HttpClient(handler)) + { + return await client.GetStringAsync(uri); + } + } + + private async Task SendRequestAsync(string uri, string upload) + { + WebRequestHandler handler = new WebRequestHandler(); + handler.ServerCertificateValidationCallback = (a, b, c, d) => true; + using (HttpClient client = new HttpClient(handler)) + { + HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload)); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + } + + private X509Certificate2 FindClientCert() + { + var store = new X509Store(); + store.Open(OpenFlags.ReadOnly); + + foreach (var cert in store.Certificates) + { + bool isClientAuth = false; + bool isSmartCard = false; + foreach (var extension in cert.Extensions) + { + var eku = extension as X509EnhancedKeyUsageExtension; + if (eku != null) + { + foreach (var oid in eku.EnhancedKeyUsages) + { + if (oid.FriendlyName == "Client Authentication") + { + isClientAuth = true; + } + else if (oid.FriendlyName == "Smart Card Logon") + { + isSmartCard = true; + break; + } + } + } + } + + if (isClientAuth && !isSmartCard) + { + return cert; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/Microsoft.Net.Server.FunctionalTests.kproj b/test/Microsoft.Net.Server.FunctionalTests/Microsoft.Net.Server.FunctionalTests.kproj new file mode 100644 index 0000000000..93b8944285 --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/Microsoft.Net.Server.FunctionalTests.kproj @@ -0,0 +1,39 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + dcb6e0b1-223d-44e6-8696-4767e5b6e6a1 + Library + net45 + + + + + + + 2.0 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/OpaqueUpgradeTests.cs b/test/Microsoft.Net.Server.FunctionalTests/OpaqueUpgradeTests.cs new file mode 100644 index 0000000000..031aeeca3b --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/OpaqueUpgradeTests.cs @@ -0,0 +1,322 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +/* TODO: Opaque +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.Net.Server +{ + using AppFunc = Func; + using OpaqueUpgrade = Action, Func, Task>>; + + public class OpaqueUpgradeTests + { + private const string Address = "http://localhost:8080/"; + + [Fact] + public async Task OpaqueUpgrade_SupportKeys_Present() + { + using (CreateServer(env => + { + try + { + IDictionary capabilities = env.Get>("server.Capabilities"); + Assert.NotNull(capabilities); + + Assert.Equal("1.0", capabilities.Get("opaque.Version")); + + OpaqueUpgrade opaqueUpgrade = env.Get("opaque.Upgrade"); + Assert.NotNull(opaqueUpgrade); + } + catch (Exception ex) + { + byte[] body = Encoding.UTF8.GetBytes(ex.ToString()); + env.Get("owin.ResponseBody").Write(body, 0, body.Length); + } + return Task.FromResult(0); + })) + { + HttpResponseMessage response = await SendRequestAsync(Address); + Assert.Equal(200, (int)response.StatusCode); + Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); + Assert.Equal(0, response.Content.Headers.ContentLength); + Assert.Equal(string.Empty, response.Content.ReadAsStringAsync().Result); + } + } + + [Fact] + public async Task OpaqueUpgrade_NullCallback_Throws() + { + using (CreateServer(env => + { + try + { + OpaqueUpgrade opaqueUpgrade = env.Get("opaque.Upgrade"); + opaqueUpgrade(new Dictionary(), null); + } + catch (Exception ex) + { + byte[] body = Encoding.UTF8.GetBytes(ex.ToString()); + env.Get("owin.ResponseBody").Write(body, 0, body.Length); + } + return Task.FromResult(0); + })) + { + HttpResponseMessage response = await SendRequestAsync(Address); + Assert.Equal(200, (int)response.StatusCode); + Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); + Assert.Contains("callback", response.Content.ReadAsStringAsync().Result); + } + } + + [Fact] + public async Task OpaqueUpgrade_AfterHeadersSent_Throws() + { + bool? upgradeThrew = null; + using (CreateServer(env => + { + byte[] body = Encoding.UTF8.GetBytes("Hello World"); + env.Get("owin.ResponseBody").Write(body, 0, body.Length); + OpaqueUpgrade opaqueUpgrade = env.Get("opaque.Upgrade"); + try + { + opaqueUpgrade(null, _ => Task.FromResult(0)); + upgradeThrew = false; + } + catch (InvalidOperationException) + { + upgradeThrew = true; + } + return Task.FromResult(0); + })) + { + HttpResponseMessage response = await SendRequestAsync(Address); + Assert.Equal(200, (int)response.StatusCode); + Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); + Assert.True(upgradeThrew.Value); + } + } + + [Fact] + public async Task OpaqueUpgrade_GetUpgrade_Success() + { + ManualResetEvent waitHandle = new ManualResetEvent(false); + bool? callbackInvoked = null; + using (CreateServer(env => + { + var responseHeaders = env.Get>("owin.ResponseHeaders"); + responseHeaders["Upgrade"] = new string[] { "websocket" }; // Win8.1 blocks anything but WebSockets + OpaqueUpgrade opaqueUpgrade = env.Get("opaque.Upgrade"); + opaqueUpgrade(null, opqEnv => + { + callbackInvoked = true; + waitHandle.Set(); + return Task.FromResult(0); + }); + return Task.FromResult(0); + })) + { + using (Stream stream = await SendOpaqueRequestAsync("GET", Address)) + { + Assert.True(waitHandle.WaitOne(TimeSpan.FromSeconds(1)), "Timed out"); + Assert.True(callbackInvoked.HasValue, "CallbackInvoked not set"); + Assert.True(callbackInvoked.Value, "Callback not invoked"); + } + } + } + + [Theory] + // See HTTP_VERB for known verbs + [InlineData("UNKNOWN", null)] + [InlineData("INVALID", null)] + [InlineData("OPTIONS", null)] + [InlineData("GET", null)] + [InlineData("HEAD", null)] + [InlineData("DELETE", null)] + [InlineData("TRACE", null)] + [InlineData("CONNECT", null)] + [InlineData("TRACK", null)] + [InlineData("MOVE", null)] + [InlineData("COPY", null)] + [InlineData("PROPFIND", null)] + [InlineData("PROPPATCH", null)] + [InlineData("MKCOL", null)] + [InlineData("LOCK", null)] + [InlineData("UNLOCK", null)] + [InlineData("SEARCH", null)] + [InlineData("CUSTOMVERB", null)] + [InlineData("PATCH", null)] + [InlineData("POST", "Content-Length: 0")] + [InlineData("PUT", "Content-Length: 0")] + public async Task OpaqueUpgrade_VariousMethodsUpgradeSendAndReceive_Success(string method, string extraHeader) + { + using (CreateServer(env => + { + var responseHeaders = env.Get>("owin.ResponseHeaders"); + responseHeaders["Upgrade"] = new string[] { "WebSocket" }; // Win8.1 blocks anything but WebSockets + OpaqueUpgrade opaqueUpgrade = env.Get("opaque.Upgrade"); + opaqueUpgrade(null, async opqEnv => + { + Stream opaqueStream = opqEnv.Get("opaque.Stream"); + + byte[] buffer = new byte[100]; + int read = await opaqueStream.ReadAsync(buffer, 0, buffer.Length); + + await opaqueStream.WriteAsync(buffer, 0, read); + }); + return Task.FromResult(0); + })) + { + using (Stream stream = await SendOpaqueRequestAsync(method, Address, extraHeader)) + { + byte[] data = new byte[100]; + stream.WriteAsync(data, 0, 49).Wait(); + int read = stream.ReadAsync(data, 0, data.Length).Result; + Assert.Equal(49, read); + } + } + } + + [Theory] + // Http.Sys returns a 411 Length Required if PUT or POST does not specify content-length or chunked. + [InlineData("POST", "Content-Length: 10")] + [InlineData("POST", "Transfer-Encoding: chunked")] + [InlineData("PUT", "Content-Length: 10")] + [InlineData("PUT", "Transfer-Encoding: chunked")] + [InlineData("CUSTOMVERB", "Content-Length: 10")] + [InlineData("CUSTOMVERB", "Transfer-Encoding: chunked")] + public void OpaqueUpgrade_InvalidMethodUpgrade_Disconnected(string method, string extraHeader) + { + OpaqueUpgrade opaqueUpgrade = null; + using (CreateServer(env => + { + opaqueUpgrade = env.Get("opaque.Upgrade"); + if (opaqueUpgrade == null) + { + throw new NotImplementedException(); + } + opaqueUpgrade(null, opqEnv => Task.FromResult(0)); + return Task.FromResult(0); + })) + { + Assert.Throws(() => + { + try + { + return SendOpaqueRequestAsync(method, Address, extraHeader).Result; + } + catch (AggregateException ag) + { + throw ag.GetBaseException(); + } + }); + Assert.Null(opaqueUpgrade); + } + } + + private IDisposable CreateServer(AppFunc app) + { + IDictionary properties = new Dictionary(); + IList> addresses = new List>(); + properties["host.Addresses"] = addresses; + + IDictionary address = new Dictionary(); + addresses.Add(address); + + address["scheme"] = "http"; + address["host"] = "localhost"; + address["port"] = "8080"; + address["path"] = string.Empty; + + OwinServerFactory.Initialize(properties); + + return OwinServerFactory.Create(app, properties); + } + + private async Task SendRequestAsync(string uri) + { + using (HttpClient client = new HttpClient()) + { + return await client.GetAsync(uri); + } + } + + // Returns a bidirectional opaque stream or throws if the upgrade fails + private async Task SendOpaqueRequestAsync(string method, string address, string extraHeader = null) + { + // Connect with a socket + Uri uri = new Uri(address); + TcpClient client = new TcpClient(); + try + { + await client.ConnectAsync(uri.Host, uri.Port); + NetworkStream stream = client.GetStream(); + + // Send an HTTP GET request + byte[] requestBytes = BuildGetRequest(method, uri, extraHeader); + await stream.WriteAsync(requestBytes, 0, requestBytes.Length); + + // Read the response headers, fail if it's not a 101 + await ParseResponseAsync(stream); + + // Return the opaque network stream + return stream; + } + catch (Exception) + { + client.Close(); + throw; + } + } + + private byte[] BuildGetRequest(string method, Uri uri, string extraHeader) + { + StringBuilder builder = new StringBuilder(); + builder.Append(method); + builder.Append(" "); + builder.Append(uri.PathAndQuery); + builder.Append(" HTTP/1.1"); + builder.AppendLine(); + + builder.Append("Host: "); + builder.Append(uri.Host); + builder.Append(':'); + builder.Append(uri.Port); + builder.AppendLine(); + + if (!string.IsNullOrEmpty(extraHeader)) + { + builder.AppendLine(extraHeader); + } + + builder.AppendLine(); + return Encoding.ASCII.GetBytes(builder.ToString()); + } + + // Read the response headers, fail if it's not a 101 + private async Task ParseResponseAsync(NetworkStream stream) + { + StreamReader reader = new StreamReader(stream); + string statusLine = await reader.ReadLineAsync(); + string[] parts = statusLine.Split(' '); + if (int.Parse(parts[1]) != 101) + { + throw new InvalidOperationException("The response status code was incorrect: " + statusLine); + } + + // Scan to the end of the headers + while (!string.IsNullOrEmpty(reader.ReadLine())) + { + } + } + } +} +*/ \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/Project.json b/test/Microsoft.Net.Server.FunctionalTests/Project.json new file mode 100644 index 0000000000..286adb463e --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/Project.json @@ -0,0 +1,24 @@ +{ + "version" : "0.1-alpha-*", + "commands": { + "test": "Xunit.KRunner" + }, + "dependencies": { + "Xunit.KRunner": "0.1-alpha-*", + "xunit.abstractions": "2.0.0-aspnet-*", + "xunit.assert": "2.0.0-aspnet-*", + "xunit.core": "2.0.0-aspnet-*", + "xunit.execution": "2.0.0-aspnet-*", + "Microsoft.Net.Server" : "", + "Microsoft.AspNet.Logging": "0.1-alpha-*" + }, + "configurations": { + "net45": { + "dependencies": { + "System.Runtime": "", + "System.Net.Http": "", + "System.Net.Http.WebRequest": "" + } + } + } +} diff --git a/test/Microsoft.Net.Server.FunctionalTests/Properties/AssemblyInfo.cs b/test/Microsoft.Net.Server.FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1bc3b800fb --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- + +// These tests can't run in parallel because they all use the same port. +[assembly: Xunit.CollectionBehaviorAttribute(Xunit.CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] diff --git a/test/Microsoft.Net.Server.FunctionalTests/RequestBodyTests.cs b/test/Microsoft.Net.Server.FunctionalTests/RequestBodyTests.cs new file mode 100644 index 0000000000..89f6d14d32 --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/RequestBodyTests.cs @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 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.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class RequestBodyTests + { + private const string Address = "http://localhost:8080/"; + + [Fact] + public async Task RequestBody_ReadSync_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address, "Hello World"); + + var context = await server.GetContextAsync(); + byte[] input = new byte[100]; + int read = context.Request.Body.Read(input, 0, input.Length); + context.Response.ContentLength = read; + context.Response.Body.Write(input, 0, read); + + string response = await responseTask; + Assert.Equal("Hello World", response); + } + } + + [Fact] + public async Task RequestBody_ReadAync_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address, "Hello World"); + + var context = await server.GetContextAsync(); + byte[] input = new byte[100]; + int read = await context.Request.Body.ReadAsync(input, 0, input.Length); + context.Response.ContentLength = read; + await context.Response.Body.WriteAsync(input, 0, read); + + string response = await responseTask; + Assert.Equal("Hello World", response); + } + } +#if NET45 + [Fact] + public async Task RequestBody_ReadBeginEnd_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address, "Hello World"); + + var context = await server.GetContextAsync(); + byte[] input = new byte[100]; + int read = context.Request.Body.EndRead(context.Request.Body.BeginRead(input, 0, input.Length, null, null)); + context.Response.ContentLength = read; + context.Response.Body.EndWrite(context.Response.Body.BeginWrite(input, 0, read, null, null)); + + string response = await responseTask; + Assert.Equal("Hello World", response); + } + } +#endif + + [Fact] + public async Task RequestBody_InvalidBuffer_ArgumentException() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address, "Hello World"); + + var context = await server.GetContextAsync(); + byte[] input = new byte[100]; + Assert.Throws("buffer", () => context.Request.Body.Read(null, 0, 1)); + Assert.Throws("offset", () => context.Request.Body.Read(input, -1, 1)); + Assert.Throws("offset", () => context.Request.Body.Read(input, input.Length + 1, 1)); + Assert.Throws("size", () => context.Request.Body.Read(input, 10, -1)); + Assert.Throws("size", () => context.Request.Body.Read(input, 0, 0)); + Assert.Throws("size", () => context.Request.Body.Read(input, 1, input.Length)); + Assert.Throws("size", () => context.Request.Body.Read(input, 0, input.Length + 1)); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Fact] + public async Task RequestBody_ReadSyncPartialBody_Success() + { + StaggardContent content = new StaggardContent(); + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address, content); + + var context = await server.GetContextAsync(); + byte[] input = new byte[10]; + int read = context.Request.Body.Read(input, 0, input.Length); + Assert.Equal(5, read); + content.Block.Release(); + read = context.Request.Body.Read(input, 0, input.Length); + Assert.Equal(5, read); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Fact] + public async Task RequestBody_ReadAsyncPartialBody_Success() + { + StaggardContent content = new StaggardContent(); + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address, content); + + var context = await server.GetContextAsync(); + byte[] input = new byte[10]; + int read = await context.Request.Body.ReadAsync(input, 0, input.Length); + Assert.Equal(5, read); + content.Block.Release(); + read = await context.Request.Body.ReadAsync(input, 0, input.Length); + Assert.Equal(5, read); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Fact] + public async Task RequestBody_PostWithImidateBody_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendSocketRequestAsync(Address); + + var context = await server.GetContextAsync(); + byte[] input = new byte[11]; + int read = await context.Request.Body.ReadAsync(input, 0, input.Length); + Assert.Equal(10, read); + read = await context.Request.Body.ReadAsync(input, 0, input.Length); + Assert.Equal(0, read); + context.Response.ContentLength = 10; + await context.Response.Body.WriteAsync(input, 0, 10); + context.Dispose(); + + string response = await responseTask; + string[] lines = response.Split('\r', '\n'); + Assert.Equal(13, lines.Length); + Assert.Equal("HTTP/1.1 200 OK", lines[0]); + Assert.Equal("0123456789", lines[12]); + } + } + + private Task SendRequestAsync(string uri, string upload) + { + return SendRequestAsync(uri, new StringContent(upload)); + } + + private async Task SendRequestAsync(string uri, HttpContent content) + { + using (HttpClient client = new HttpClient()) + { + HttpResponseMessage response = await client.PostAsync(uri, content); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + } + + private async Task SendSocketRequestAsync(string address) + { + // Connect with a socket + Uri uri = new Uri(address); + TcpClient client = new TcpClient(); + try + { + await client.ConnectAsync(uri.Host, uri.Port); + NetworkStream stream = client.GetStream(); + + // Send an HTTP GET request + byte[] requestBytes = BuildPostRequest(uri); + await stream.WriteAsync(requestBytes, 0, requestBytes.Length); + StreamReader reader = new StreamReader(stream); + return await reader.ReadToEndAsync(); + } + catch (Exception) + { + client.Close(); + throw; + } + } + + private byte[] BuildPostRequest(Uri uri) + { + StringBuilder builder = new StringBuilder(); + builder.Append("POST"); + builder.Append(" "); + builder.Append(uri.PathAndQuery); + builder.Append(" HTTP/1.1"); + builder.AppendLine(); + + builder.Append("Host: "); + builder.Append(uri.Host); + builder.Append(':'); + builder.Append(uri.Port); + builder.AppendLine(); + + builder.AppendLine("Connection: close"); + builder.AppendLine("Content-Length: 10"); + builder.AppendLine(); + builder.Append("0123456789"); + return Encoding.ASCII.GetBytes(builder.ToString()); + } + + private class StaggardContent : HttpContent + { + public StaggardContent() + { + Block = new SemaphoreSlim(0, 1); + } + + public SemaphoreSlim Block { get; private set; } + + protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + await stream.WriteAsync(new byte[5], 0, 5); + await Block.WaitAsync(); + await stream.WriteAsync(new byte[5], 0, 5); + } + + protected override bool TryComputeLength(out long length) + { + length = 10; + return true; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/RequestHeaderTests.cs b/test/Microsoft.Net.Server.FunctionalTests/RequestHeaderTests.cs new file mode 100644 index 0000000000..7a4122db53 --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/RequestHeaderTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Linq; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class RequestHeaderTests + { + private const string Address = "http://localhost:8080/"; + + [Fact] + public async Task RequestHeaders_ClientSendsDefaultHeaders_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + var requestHeaders = context.Request.Headers; + // NOTE: The System.Net client only sends the Connection: keep-alive header on the first connection per service-point. + // Assert.Equal(2, requestHeaders.Count); + // Assert.Equal("Keep-Alive", requestHeaders.Get("Connection")); + Assert.Equal("localhost:8080", requestHeaders["Host"].First()); + string[] values; + Assert.False(requestHeaders.TryGetValue("Accept", out values)); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Fact] + public async Task RequestHeaders_ClientSendsCustomHeaders_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + string[] customValues = new string[] { "custom1, and custom2", "custom3" }; + Task responseTask = SendRequestAsync("localhost", 8080, "Custom-Header", customValues); + + var context = await server.GetContextAsync(); + var requestHeaders = context.Request.Headers; + Assert.Equal(4, requestHeaders.Count); + Assert.Equal("localhost:8080", requestHeaders["Host"].First()); + Assert.Equal("close", requestHeaders["Connection"].First()); + Assert.Equal(1, requestHeaders["Custom-Header"].Length); + // Apparently Http.Sys squashes request headers together. + Assert.Equal("custom1, and custom2, custom3", requestHeaders["Custom-Header"].First()); + Assert.Equal(1, requestHeaders["Spacer-Header"].Length); + Assert.Equal("spacervalue, spacervalue", requestHeaders["Spacer-Header"].First()); + context.Dispose(); + + await responseTask; + } + } + + private async Task SendRequestAsync(string uri) + { + using (HttpClient client = new HttpClient()) + { + return await client.GetStringAsync(uri); + } + } + + private async Task SendRequestAsync(string host, int port, string customHeader, string[] customValues) + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine("GET / HTTP/1.1"); + builder.AppendLine("Connection: close"); + builder.Append("HOST: "); + builder.Append(host); + builder.Append(':'); + builder.AppendLine(port.ToString()); + foreach (string value in customValues) + { + builder.Append(customHeader); + builder.Append(": "); + builder.AppendLine(value); + builder.AppendLine("Spacer-Header: spacervalue"); + } + builder.AppendLine(); + + byte[] request = Encoding.ASCII.GetBytes(builder.ToString()); + + Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + socket.Connect(host, port); + + socket.Send(request); + + byte[] response = new byte[1024 * 5]; + await Task.Run(() => socket.Receive(response)); + socket.Close(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/RequestTests.cs b/test/Microsoft.Net.Server.FunctionalTests/RequestTests.cs new file mode 100644 index 0000000000..54ef27e237 --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/RequestTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class RequestTests + { + private const string Address = "http://localhost:8080"; + + [Fact] + public async Task Request_SimpleGet_Success() + { + using (var server = Utilities.CreateServer("http", "localhost", "8080", "/basepath")) + { + Task responseTask = SendRequestAsync(Address + "/basepath/SomePath?SomeQuery"); + + var context = await server.GetContextAsync(); + + // General fields + var request = context.Request; + + // Request Keys + Assert.Equal("GET", request.Method); + Assert.Equal(Stream.Null, request.Body); + Assert.NotNull(request.Headers); + Assert.Equal("http", request.Scheme); + Assert.Equal("/basepath", request.PathBase); + Assert.Equal("/SomePath", request.Path); + Assert.Equal("?SomeQuery", request.QueryString); + Assert.Equal(new Version(1, 1), request.ProtocolVersion); + + Assert.Equal("::1", request.RemoteIpAddress.ToString()); + Assert.NotEqual(0, request.RemotePort); + Assert.Equal("::1", request.LocalIpAddress.ToString()); + Assert.NotEqual(0, request.LocalPort); + Assert.True(request.IsLocal); + + // Note: Response keys are validated in the ResponseTests + + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Theory] + [InlineData("/", "http://localhost:8080/", "", "/")] + [InlineData("/basepath/", "http://localhost:8080/basepath", "/basepath", "")] + [InlineData("/basepath/", "http://localhost:8080/basepath/", "/basepath", "/")] + [InlineData("/basepath/", "http://localhost:8080/basepath/subpath", "/basepath", "/subpath")] + [InlineData("/base path/", "http://localhost:8080/base%20path/sub path", "/base path", "/sub path")] + [InlineData("/base葉path/", "http://localhost:8080/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")] + public async Task Request_PathSplitting(string pathBase, string requestUri, string expectedPathBase, string expectedPath) + { + using (var server = Utilities.CreateServer("http", "localhost", "8080", pathBase)) + { + Task responseTask = SendRequestAsync(requestUri); + + var context = await server.GetContextAsync(); + + // General fields + var request = context.Request; + + // Request Keys + Assert.Equal("http", request.Scheme); + Assert.Equal(expectedPath, request.Path); + Assert.Equal(expectedPathBase, request.PathBase); + Assert.Equal(string.Empty, request.QueryString); + Assert.Equal(8080, request.LocalPort); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Theory] + // The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2" + [InlineData("/", "", "/")] + [InlineData("/random", "", "/random")] + [InlineData("/11", "/11", "")] + [InlineData("/11/", "/11", "/")] + [InlineData("/11/random", "/11", "/random")] + [InlineData("/2", "/2", "")] + [InlineData("/2/", "/2", "/")] + [InlineData("/2/random", "/2", "/random")] + [InlineData("/2/3", "/2/3", "")] + [InlineData("/2/3/", "/2/3", "/")] + [InlineData("/2/3/random", "/2/3", "/random")] + public async Task Request_MultiplePrefixes(string requestUri, string expectedPathBase, string expectedPath) + { + using (var server = new WebListener()) + { + foreach (string path in new[] { "/", "/11", "/2/3", "/2", "/11/2" }) + { + server.UrlPrefixes.Add(UrlPrefix.Create("http", "localhost", "8080", path)); + } + server.Start(); + + Task responseTask = SendRequestAsync(Address + requestUri); + + var context = await server.GetContextAsync(); + var request = context.Request; + + Assert.Equal(expectedPath, request.Path); + Assert.Equal(expectedPathBase, request.PathBase); + + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + private async Task SendRequestAsync(string uri) + { + using (HttpClient client = new HttpClient()) + { + return await client.GetStringAsync(uri); + } + } + } +} diff --git a/test/Microsoft.Net.Server.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.Net.Server.FunctionalTests/ResponseBodyTests.cs new file mode 100644 index 0000000000..25a125372f --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/ResponseBodyTests.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class ResponseBodyTests + { + private const string Address = "http://localhost:8080/"; + + [Fact] + public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Body.Write(new byte[10], 0, 10); + await context.Response.Body.WriteAsync(new byte[10], 0, 10); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); + Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); + } + } + + [Fact] + public async Task ResponseBody_WriteChunked_Chunked() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Request.Headers["transfeR-Encoding"] = new[] { " CHunked " }; + Stream stream = context.Response.Body; + stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null)); + stream.Write(new byte[10], 0, 10); + await stream.WriteAsync(new byte[10], 0, 10); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); + Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync()); + } + } + + [Fact] + public async Task ResponseBody_WriteContentLength_PassedThrough() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Content-lenGth"] = new[] { " 30 " }; + Stream stream = context.Response.Body; + stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null)); + stream.Write(new byte[10], 0, 10); + await stream.WriteAsync(new byte[10], 0, 10); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); + IEnumerable contentLength; + Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.Equal("30", contentLength.First()); + Assert.Null(response.Headers.TransferEncodingChunked); + Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync()); + } + } + /* TODO: response protocol + [Fact] + public async Task ResponseBody_Http10WriteNoHeaders_DefaultsConnectionClose() + { + using (Utilities.CreateHttpServer(env => + { + env["owin.ResponseProtocol"] = "HTTP/1.0"; + env.Get("owin.ResponseBody").Write(new byte[10], 0, 10); + return env.Get("owin.ResponseBody").WriteAsync(new byte[10], 0, 10); + })) + { + HttpResponseMessage response = await SendRequestAsync(Address); + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); // Http.Sys won't transmit 1.0 + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.Null(response.Headers.TransferEncodingChunked); + Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); + } + } + */ + /* TODO: Why does this test time out? + [Fact] + public async Task ResponseBody_WriteContentLengthNoneWritten_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Content-lenGth"] = new[] { " 20 " }; + context.Dispose(); + + await Assert.ThrowsAsync(() => responseTask); + } + } + */ + [Fact] + public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Content-lenGth"] = new[] { " 20 " }; + context.Response.Body.Write(new byte[5], 0, 5); + context.Dispose(); + + await Assert.ThrowsAsync(() => responseTask); + } + } + + [Fact] + public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Content-lenGth"] = new[] { " 10 " }; + context.Response.Body.Write(new byte[5], 0, 5); + Assert.Throws(() => context.Response.Body.Write(new byte[6], 0, 6)); + context.Dispose(); + + await Assert.ThrowsAsync(() => responseTask); + } + } + + [Fact] + public async Task ResponseBody_WriteContentLengthExtraWritten_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Content-lenGth"] = new[] { " 10 " }; + context.Response.Body.Write(new byte[10], 0, 10); + Assert.Throws(() => context.Response.Body.Write(new byte[6], 0, 6)); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); + IEnumerable contentLength; + Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.Equal("10", contentLength.First()); + Assert.Null(response.Headers.TransferEncodingChunked); + Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync()); + } + } + + private async Task SendRequestAsync(string uri) + { + using (HttpClient client = new HttpClient()) + { + return await client.GetAsync(uri); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/ResponseHeaderTests.cs b/test/Microsoft.Net.Server.FunctionalTests/ResponseHeaderTests.cs new file mode 100644 index 0000000000..4411ba1d74 --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/ResponseHeaderTests.cs @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class ResponseHeaderTests + { + private const string Address = "http://localhost:8080/"; + + [Fact] + public async Task ResponseHeaders_ServerSendsDefaultHeaders_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + response.EnsureSuccessStatusCode(); + Assert.Equal(2, response.Headers.Count()); + Assert.False(response.Headers.TransferEncodingChunked.HasValue); + Assert.True(response.Headers.Date.HasValue); + Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString()); + Assert.Equal(1, response.Content.Headers.Count()); + Assert.Equal(0, response.Content.Headers.ContentLength); + } + } + + [Fact] + public async Task ResponseHeaders_ServerSendsSingleValueKnownHeaders_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + WebRequest request = WebRequest.Create(Address); + Task responseTask = request.GetResponseAsync(); + + var context = await server.GetContextAsync(); + var responseHeaders = context.Response.Headers; + responseHeaders["WWW-Authenticate"] = new string[] { "custom1" }; + context.Dispose(); + + // HttpClient would merge the headers no matter what + HttpWebResponse response = (HttpWebResponse)await responseTask; + Assert.Equal(4, response.Headers.Count); + Assert.Null(response.Headers["Transfer-Encoding"]); + Assert.Equal(0, response.ContentLength); + Assert.NotNull(response.Headers["Date"]); + Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]); + Assert.Equal(new string[] { "custom1" }, response.Headers.GetValues("WWW-Authenticate")); + } + } + + [Fact] + public async Task ResponseHeaders_ServerSendsMultiValueKnownHeaders_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + WebRequest request = WebRequest.Create(Address); + Task responseTask = request.GetResponseAsync(); + + var context = await server.GetContextAsync(); + var responseHeaders = context.Response.Headers; + responseHeaders["WWW-Authenticate"] = new string[] { "custom1, and custom2", "custom3" }; + context.Dispose(); + + // HttpClient would merge the headers no matter what + HttpWebResponse response = (HttpWebResponse)await responseTask; + Assert.Equal(4, response.Headers.Count); + Assert.Null(response.Headers["Transfer-Encoding"]); + Assert.Equal(0, response.ContentLength); + Assert.NotNull(response.Headers["Date"]); + Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]); + Assert.Equal(new string[] { "custom1, and custom2", "custom3" }, response.Headers.GetValues("WWW-Authenticate")); + } + } + + [Fact] + public async Task ResponseHeaders_ServerSendsCustomHeaders_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + WebRequest request = WebRequest.Create(Address); + Task responseTask = request.GetResponseAsync(); + + var context = await server.GetContextAsync(); + var responseHeaders = context.Response.Headers; + responseHeaders["Custom-Header1"] = new string[] { "custom1, and custom2", "custom3" }; + context.Dispose(); + + // HttpClient would merge the headers no matter what + HttpWebResponse response = (HttpWebResponse)await responseTask; + Assert.Equal(4, response.Headers.Count); + Assert.Null(response.Headers["Transfer-Encoding"]); + Assert.Equal(0, response.ContentLength); + Assert.NotNull(response.Headers["Date"]); + Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]); + Assert.Equal(new string[] { "custom1, and custom2", "custom3" }, response.Headers.GetValues("Custom-Header1")); + } + } + + [Fact] + public async Task ResponseHeaders_ServerSendsConnectionClose_Closed() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + var responseHeaders = context.Response.Headers; + responseHeaders["Connection"] = new string[] { "Close" }; + context.Dispose(); + + HttpResponseMessage response = await responseTask; + response.EnsureSuccessStatusCode(); + Assert.True(response.Headers.ConnectionClose.Value); + Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); + } + } + /* TODO: + [Fact] + public async Task ResponseHeaders_SendsHttp10_Gets11Close() + { + using (Utilities.CreateHttpServer(env => + { + env["owin.ResponseProtocol"] = "HTTP/1.0"; + return Task.FromResult(0); + })) + { + HttpResponseMessage response = await SendRequestAsync(Address); + response.EnsureSuccessStatusCode(); + Assert.Equal(new Version(1, 1), response.Version); + Assert.True(response.Headers.ConnectionClose.Value); + Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); + } + } + + [Fact] + public async Task ResponseHeaders_SendsHttp10WithBody_Gets11Close() + { + using (Utilities.CreateHttpServer(env => + { + env["owin.ResponseProtocol"] = "HTTP/1.0"; + return env.Get("owin.ResponseBody").WriteAsync(new byte[10], 0, 10); + })) + { + HttpResponseMessage response = await SendRequestAsync(Address); + response.EnsureSuccessStatusCode(); + Assert.Equal(new Version(1, 1), response.Version); + Assert.False(response.Headers.TransferEncodingChunked.HasValue); + Assert.False(response.Content.Headers.Contains("Content-Length")); + Assert.True(response.Headers.ConnectionClose.Value); + Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); + } + } + */ + + [Fact] + public async Task ResponseHeaders_HTTP10Request_Gets11Close() + { + using (var server = Utilities.CreateHttpServer()) + { + using (HttpClient client = new HttpClient()) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Address); + request.Version = new Version(1, 0); + Task responseTask = client.SendAsync(request); + + var context = await server.GetContextAsync(); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + response.EnsureSuccessStatusCode(); + Assert.Equal(new Version(1, 1), response.Version); + Assert.True(response.Headers.ConnectionClose.Value); + Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); + } + } + } + + [Fact] + public async Task ResponseHeaders_HTTP10Request_RemovesChunkedHeader() + { + using (var server = Utilities.CreateHttpServer()) + { + using (HttpClient client = new HttpClient()) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Address); + request.Version = new Version(1, 0); + Task responseTask = client.SendAsync(request); + + var context = await server.GetContextAsync(); + var responseHeaders = context.Response.Headers; + responseHeaders["Transfer-Encoding"] = new string[] { "chunked" }; + await context.Response.Body.WriteAsync(new byte[10], 0, 10); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + response.EnsureSuccessStatusCode(); + Assert.Equal(new Version(1, 1), response.Version); + Assert.False(response.Headers.TransferEncodingChunked.HasValue); + Assert.False(response.Content.Headers.Contains("Content-Length")); + Assert.True(response.Headers.ConnectionClose.Value); + Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); + } + } + } + + [Fact] + public async Task Headers_FlushSendsHeaders_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + var responseHeaders = context.Response.Headers; + + responseHeaders.Add("Custom1", new string[] { "value1a", "value1b" }); + responseHeaders.Add("Custom2", new string[] { "value2a, value2b" }); + var body = context.Response.Body; + body.Flush(); + Assert.Throws(() => context.Response.StatusCode = 404); + responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }); // Ignored + + context.Dispose(); + + HttpResponseMessage response = await responseTask; + response.EnsureSuccessStatusCode(); + Assert.Equal(5, response.Headers.Count()); // Date, Server, Chunked + + Assert.Equal(2, response.Headers.GetValues("Custom1").Count()); + Assert.Equal("value1a", response.Headers.GetValues("Custom1").First()); + Assert.Equal("value1b", response.Headers.GetValues("Custom1").Skip(1).First()); + Assert.Equal(1, response.Headers.GetValues("Custom2").Count()); + Assert.Equal("value2a, value2b", response.Headers.GetValues("Custom2").First()); + } + } + + [Fact] + public async Task Headers_FlushAsyncSendsHeaders_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + var responseHeaders = context.Response.Headers; + + responseHeaders.Add("Custom1", new string[] { "value1a", "value1b" }); + responseHeaders.Add("Custom2", new string[] { "value2a, value2b" }); + var body = context.Response.Body; + await body.FlushAsync(); + Assert.Throws(() => context.Response.StatusCode = 404); + responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }); // Ignored + + context.Dispose(); + + HttpResponseMessage response = await responseTask; + response.EnsureSuccessStatusCode(); + Assert.Equal(5, response.Headers.Count()); // Date, Server, Chunked + + Assert.Equal(2, response.Headers.GetValues("Custom1").Count()); + Assert.Equal("value1a", response.Headers.GetValues("Custom1").First()); + Assert.Equal("value1b", response.Headers.GetValues("Custom1").Skip(1).First()); + Assert.Equal(1, response.Headers.GetValues("Custom2").Count()); + Assert.Equal("value2a, value2b", response.Headers.GetValues("Custom2").First()); + } + } + + private async Task SendRequestAsync(string uri) + { + using (HttpClient client = new HttpClient()) + { + return await client.GetAsync(uri); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/ResponseSendFileTests.cs b/test/Microsoft.Net.Server.FunctionalTests/ResponseSendFileTests.cs new file mode 100644 index 0000000000..ff9fb85c9c --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/ResponseSendFileTests.cs @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class ResponseSendFileTests + { + private const string Address = "http://localhost:8080/"; + private readonly string AbsoluteFilePath; + private readonly string RelativeFilePath; + private readonly long FileLength; + + public ResponseSendFileTests() + { + AbsoluteFilePath = Directory.GetFiles(Environment.CurrentDirectory).First(); + RelativeFilePath = Path.GetFileName(AbsoluteFilePath); + FileLength = new FileInfo(AbsoluteFilePath).Length; + } + + [Fact] + public async Task ResponseSendFile_MissingFile_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + await Assert.ThrowsAsync(() => + context.Response.SendFileAsync("Missing.txt", 0, null, CancellationToken.None)); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + } + } + + [Fact] + public async Task ResponseSendFile_NoHeaders_DefaultsToChunked() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); + Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFile_RelativeFile_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + await context.Response.SendFileAsync(RelativeFilePath, 0, null, CancellationToken.None); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); + Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFile_Chunked_Chunked() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Transfer-EncodinG"] = new[] { "CHUNKED" }; + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable contentLength; + Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value); + Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFile_MultipleChunks_Chunked() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Transfer-EncodinG"] = new[] { "CHUNKED" }; + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable contentLength; + Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value); + Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFile_ChunkedHalfOfFile_Chunked() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + await context.Response.SendFileAsync(AbsoluteFilePath, 0, FileLength / 2, CancellationToken.None); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable contentLength; + Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value); + Assert.Equal(FileLength / 2, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFile_ChunkedOffsetOutOfRange_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + await Assert.ThrowsAsync( + () => context.Response.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None)); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + } + } + + [Fact] + public async Task ResponseSendFile_ChunkedCountOutOfRange_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + await Assert.ThrowsAsync( + () => context.Response.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None)); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + } + } + + [Fact] + public async Task ResponseSendFile_ChunkedCount0_Chunked() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + await context.Response.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable contentLength; + Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value); + Assert.Equal(0, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFile_ContentLength_PassedThrough() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Content-lenGth"] = new[] { FileLength.ToString() }; + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable contentLength; + Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.Equal(FileLength.ToString(), contentLength.First()); + Assert.Null(response.Headers.TransferEncodingChunked); + Assert.Equal(FileLength, response.Content.ReadAsByteArrayAsync().Result.Length); + } + } + + [Fact] + public async Task ResponseSendFile_ContentLengthSpecific_PassedThrough() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Content-lenGth"] = new[] { "10" }; + await context.Response.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable contentLength; + Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.Equal("10", contentLength.First()); + Assert.Null(response.Headers.TransferEncodingChunked); + Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFile_ContentLength0_PassedThrough() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.Headers["Content-lenGth"] = new[] { "0" }; + await context.Response.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + IEnumerable contentLength; + Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); + Assert.Equal("0", contentLength.First()); + Assert.Null(response.Headers.TransferEncodingChunked); + Assert.Equal(0, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + private async Task SendRequestAsync(string uri) + { + using (HttpClient client = new HttpClient()) + { + return await client.GetAsync(uri); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/ResponseTests.cs b/test/Microsoft.Net.Server.FunctionalTests/ResponseTests.cs new file mode 100644 index 0000000000..6aaceeff89 --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/ResponseTests.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class ResponseTests + { + private const string Address = "http://localhost:8080/"; + + [Fact] + public async Task Response_ServerSendsDefaultResponse_ServerProvidesStatusCodeAndReasonPhrase() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + Assert.Equal(200, context.Response.StatusCode); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("OK", response.ReasonPhrase); + Assert.Equal(new Version(1, 1), response.Version); + Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task Response_ServerSendsSpecificStatus_ServerProvidesReasonPhrase() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.StatusCode = 201; + // TODO: env["owin.ResponseProtocol"] = "HTTP/1.0"; // Http.Sys ignores this value + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(201, (int)response.StatusCode); + Assert.Equal("Created", response.ReasonPhrase); + Assert.Equal(new Version(1, 1), response.Version); + Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task Response_ServerSendsSpecificStatusAndReasonPhrase_PassedThrough() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.StatusCode = 201; + context.Response.ReasonPhrase = "CustomReasonPhrase"; + // TODO: env["owin.ResponseProtocol"] = "HTTP/1.0"; // Http.Sys ignores this value + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(201, (int)response.StatusCode); + Assert.Equal("CustomReasonPhrase", response.ReasonPhrase); + Assert.Equal(new Version(1, 1), response.Version); + Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task Response_ServerSendsCustomStatus_NoReasonPhrase() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.StatusCode = 901; + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(901, (int)response.StatusCode); + Assert.Equal(string.Empty, response.ReasonPhrase); + Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task Response_100_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + Assert.Throws(() => { context.Response.StatusCode = 100; }); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + } + } + + [Fact] + public async Task Response_0_Throws() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + Assert.Throws(() => { context.Response.StatusCode = 0; }); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + } + } + + private async Task SendRequestAsync(string uri) + { + using (HttpClient client = new HttpClient()) + { + return await client.GetAsync(uri); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Net.Server.FunctionalTests/ServerTests.cs b/test/Microsoft.Net.Server.FunctionalTests/ServerTests.cs new file mode 100644 index 0000000000..5778917050 --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/ServerTests.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.Server +{ + public class ServerTests + { + private const string Address = "http://localhost:8080/"; + + [Fact] + public async Task Server_200OK_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + [Fact] + public async Task Server_SendHelloWorld_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Response.ContentLength = 11; + using (var writer = new StreamWriter(context.Response.Body)) + { + writer.Write("Hello World"); + } + + string response = await responseTask; + Assert.Equal("Hello World", response); + } + } + + [Fact] + public async Task Server_EchoHelloWorld_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + Task responseTask = SendRequestAsync(Address, "Hello World"); + + var context = await server.GetContextAsync(); + string input = new StreamReader(context.Request.Body).ReadToEnd(); + Assert.Equal("Hello World", input); + context.Response.ContentLength = 11; + using (var writer = new StreamWriter(context.Response.Body)) + { + writer.Write("Hello World"); + } + + string response = await responseTask; + Assert.Equal("Hello World", response); + } + } + + [Fact] + public async Task Server_ClientDisconnects_CallCancelled() + { + TimeSpan interval = TimeSpan.FromSeconds(1); + ManualResetEvent canceled = new ManualResetEvent(false); + + using (var server = Utilities.CreateHttpServer()) + { + // Note: System.Net.Sockets does not RST the connection by default, it just FINs. + // Http.Sys's disconnect notice requires a RST. + Task responseTask = SendHungRequestAsync("GET", Address); + + var context = await server.GetContextAsync(); + CancellationToken ct = context.DisconnectToken; + Assert.True(ct.CanBeCanceled, "CanBeCanceled"); + Assert.False(ct.IsCancellationRequested, "IsCancellationRequested"); + ct.Register(() => canceled.Set()); + + using (Socket socket = await responseTask) + { + socket.Close(0); // Force a RST + } + Assert.True(canceled.WaitOne(interval), "canceled"); + Assert.True(ct.IsCancellationRequested, "IsCancellationRequested"); + + context.Dispose(); + } + } + + [Fact] + public async Task Server_Abort_CallCancelled() + { + TimeSpan interval = TimeSpan.FromSeconds(1); + ManualResetEvent canceled = new ManualResetEvent(false); + + using (var server = Utilities.CreateHttpServer()) + { + // Note: System.Net.Sockets does not RST the connection by default, it just FINs. + // Http.Sys's disconnect notice requires a RST. + Task responseTask = SendHungRequestAsync("GET", Address); + + var context = await server.GetContextAsync(); + CancellationToken ct = context.DisconnectToken; + Assert.True(ct.CanBeCanceled, "CanBeCanceled"); + Assert.False(ct.IsCancellationRequested, "IsCancellationRequested"); + ct.Register(() => canceled.Set()); + context.Abort(); + Assert.True(canceled.WaitOne(interval), "Aborted"); + Assert.True(ct.IsCancellationRequested, "IsCancellationRequested"); + + using (Socket socket = await responseTask) + { + Assert.Throws(() => socket.Receive(new byte[10])); + } + } + } + + [Fact] + public async Task Server_SetQueueLimit_Success() + { + using (var server = Utilities.CreateHttpServer()) + { + server.SetRequestQueueLimit(1001); + Task responseTask = SendRequestAsync(Address); + + var context = await server.GetContextAsync(); + context.Dispose(); + + string response = await responseTask; + Assert.Equal(string.Empty, response); + } + } + + private async Task SendRequestAsync(string uri) + { + ServicePointManager.DefaultConnectionLimit = 100; + using (HttpClient client = new HttpClient()) + { + return await client.GetStringAsync(uri); + } + } + + private async Task SendRequestAsync(string uri, string upload) + { + using (HttpClient client = new HttpClient()) + { + HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload)); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + } + + private async Task SendHungRequestAsync(string method, string address) + { + // Connect with a socket + Uri uri = new Uri(address); + TcpClient client = new TcpClient(); + try + { + await client.ConnectAsync(uri.Host, uri.Port); + NetworkStream stream = client.GetStream(); + + // Send an HTTP GET request + byte[] requestBytes = BuildGetRequest(method, uri); + await stream.WriteAsync(requestBytes, 0, requestBytes.Length); + + // Return the opaque network stream + return client.Client; + } + catch (Exception) + { + client.Close(); + throw; + } + } + + private byte[] BuildGetRequest(string method, Uri uri) + { + StringBuilder builder = new StringBuilder(); + builder.Append(method); + builder.Append(" "); + builder.Append(uri.PathAndQuery); + builder.Append(" HTTP/1.1"); + builder.AppendLine(); + + builder.Append("Host: "); + builder.Append(uri.Host); + builder.Append(':'); + builder.Append(uri.Port); + builder.AppendLine(); + + builder.AppendLine(); + return Encoding.ASCII.GetBytes(builder.ToString()); + } + } +} diff --git a/test/Microsoft.Net.Server.FunctionalTests/Utilities.cs b/test/Microsoft.Net.Server.FunctionalTests/Utilities.cs new file mode 100644 index 0000000000..c11b7596f1 --- /dev/null +++ b/test/Microsoft.Net.Server.FunctionalTests/Utilities.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Net.Server +{ + internal static class Utilities + { + internal static WebListener CreateHttpServer() + { + return CreateServer("http", "localhost", "8080", string.Empty); + } + + internal static WebListener CreateHttpsServer() + { + return CreateServer("https", "localhost", "9090", string.Empty); + } + + internal static WebListener CreateAuthServer(AuthenticationType authType) + { + return CreateServer("http", "localhost", "8080", string.Empty, authType); + } + + internal static WebListener CreateServer(string scheme, string host, string port, string path) + { + return CreateServer(scheme, host, port, path, AuthenticationType.None); + } + + internal static WebListener CreateServer(string scheme, string host, string port, string path, AuthenticationType authType) + { + WebListener listener = new WebListener(); + listener.UrlPrefixes.Add(UrlPrefix.Create(scheme, host, port, path)); + listener.AuthenticationManager.AuthenticationTypes = authType; + listener.Start(); + return listener; + } + } +}