diff --git a/src/Nancy.Demo.Hosting.Self/Models/Index.cs b/src/Nancy.Demo.Hosting.Self/Models/Index.cs new file mode 100644 index 0000000000..9c6b413480 --- /dev/null +++ b/src/Nancy.Demo.Hosting.Self/Models/Index.cs @@ -0,0 +1,14 @@ +namespace Nancy.Demo.Hosting.Self.Models +{ + public class Index + { + public string Name { get; set; } + + public string Posted { get; set; } + + public Index() + { + this.Posted = "Nothing :-("; + } + } +} \ No newline at end of file diff --git a/src/Nancy.Demo.Hosting.Self/Nancy.Demo.Hosting.Self.csproj b/src/Nancy.Demo.Hosting.Self/Nancy.Demo.Hosting.Self.csproj new file mode 100644 index 0000000000..01ff356077 --- /dev/null +++ b/src/Nancy.Demo.Hosting.Self/Nancy.Demo.Hosting.Self.csproj @@ -0,0 +1,177 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1} + Exe + Properties + Nancy.Demo.Hosting.Self + Nancy.Demo.Hosting.Self + v4.5 + 512 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + false + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + false + + + Nancy.Demo.Hosting.Self.Program + + + true + bin\x86\MonoDebug\ + DEBUG;TRACE + full + x86 + bin\Debug\Nancy.Demo.Hosting.Self.exe.CodeAnalysisLog.xml + true + GlobalSuppressions.cs + prompt + AllRules.ruleset + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets + false + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules + false + 4 + false + false + + + bin\x86\MonoRelease\ + TRACE + true + pdbonly + x86 + bin\Release\Nancy.Demo.Hosting.Self.exe.CodeAnalysisLog.xml + true + GlobalSuppressions.cs + prompt + AllRules.ruleset + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets + false + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules + false + 4 + false + + + + + + + + + + + + + + + + + + + + Properties\SharedAssemblyInfo.cs + + + + + + + + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134} + Nancy.Hosting.Self + + + {4b7e35df-1569-4346-b180-a09615723095} + Nancy.ViewEngines.Spark + + + {34576216-0DCA-4B0F-A0DC-9075E75A676F} + Nancy + + + + + Designer + + + Designer + Always + + + + + Always + + + + + False + Microsoft .NET Framework 4 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + + + \ No newline at end of file diff --git a/src/Nancy.Demo.Hosting.Self/Program.cs b/src/Nancy.Demo.Hosting.Self/Program.cs new file mode 100644 index 0000000000..92cabad650 --- /dev/null +++ b/src/Nancy.Demo.Hosting.Self/Program.cs @@ -0,0 +1,30 @@ +namespace Nancy.Demo.Hosting.Self +{ + using System; + using System.Diagnostics; + + using Nancy.Hosting.Self; + + class Program + { + static void Main() + { + using (var nancyHost = new NancyHost(new Uri("http://localhost:8888/nancy/"), new Uri("http://127.0.0.1:8898/nancy/"), new Uri("http://localhost:8889/nancytoo/"))) + { + nancyHost.Start(); + + Console.WriteLine("Nancy now listening - navigating to http://localhost:8888/nancy/. Press enter to stop"); + try + { + Process.Start("http://localhost:8888/nancy/"); + } + catch (Exception) + { + } + Console.ReadKey(); + } + + Console.WriteLine("Stopped. Good bye!"); + } + } +} diff --git a/src/Nancy.Demo.Hosting.Self/README.txt b/src/Nancy.Demo.Hosting.Self/README.txt new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/src/Nancy.Demo.Hosting.Self/README.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Nancy.Demo.Hosting.Self/TestModule.cs b/src/Nancy.Demo.Hosting.Self/TestModule.cs new file mode 100644 index 0000000000..236f732734 --- /dev/null +++ b/src/Nancy.Demo.Hosting.Self/TestModule.cs @@ -0,0 +1,45 @@ +namespace Nancy.Demo.Hosting.Self +{ + using System.Linq; + + using Nancy.Demo.Hosting.Self.Models; + + public class TestModule : NancyModule + { + public TestModule() + { + Get["/"] = parameters => { + return View["staticview", this.Request.Url]; + }; + + Get["/testing"] = parameters => + { + return View["staticview", this.Request.Url]; + }; + + Get["/fileupload"] = x => + { + var model = new Index() { Name = "Boss Hawg" }; + + return View["FileUpload", model]; + }; + + Post["/fileupload"] = x => + { + var model = new Index() { Name = "Boss Hawg" }; + + var file = this.Request.Files.FirstOrDefault(); + string fileDetails = "None"; + + if (file != null) + { + fileDetails = string.Format("{3} - {0} ({1}) {2}bytes", file.Name, file.ContentType, file.Value.Length, file.Key); + } + + model.Posted = fileDetails; + + return View["FileUpload", model]; + }; + } + } +} \ No newline at end of file diff --git a/src/Nancy.Demo.Hosting.Self/Views/FileUpload.spark b/src/Nancy.Demo.Hosting.Self/Views/FileUpload.spark new file mode 100644 index 0000000000..4fab92d003 --- /dev/null +++ b/src/Nancy.Demo.Hosting.Self/Views/FileUpload.spark @@ -0,0 +1,17 @@ + + + + Nancy Self Host Demo + + +

Hello ${Model.Name}!

+

This is a Spark view rendered via the self hosting.

+

You uploaded: ${Model.Posted}

+

+

+ + +
+

+ + \ No newline at end of file diff --git a/src/Nancy.Demo.Hosting.Self/Views/staticview.html b/src/Nancy.Demo.Hosting.Self/Views/staticview.html new file mode 100644 index 0000000000..f7a407d497 --- /dev/null +++ b/src/Nancy.Demo.Hosting.Self/Views/staticview.html @@ -0,0 +1,25 @@ + + + + Nancy - Static view served by self-host + + +

Static view served by self-host

+

+ This view was served by the Nancy self-host. +

+ + http://localhost:8888/nancy/
+ http://localhost:8888/nancy/testing
+ http://127.0.0.1:8898/nancy/
+ http://127.0.0.1:8898/nancy/testing
+ http://localhost:8889/nancytoo/
+ http://localhost:8889/nancytoo/testing
+ + diff --git a/src/Nancy.Demo.Hosting.Self/app.config b/src/Nancy.Demo.Hosting.Self/app.config new file mode 100644 index 0000000000..d757e6dc64 --- /dev/null +++ b/src/Nancy.Demo.Hosting.Self/app.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Nancy.Hosting.Self.Tests/IsCaseInstensitiveBaseOfFixture.cs b/src/Nancy.Hosting.Self.Tests/IsCaseInstensitiveBaseOfFixture.cs new file mode 100644 index 0000000000..95f16caa5a --- /dev/null +++ b/src/Nancy.Hosting.Self.Tests/IsCaseInstensitiveBaseOfFixture.cs @@ -0,0 +1,185 @@ +namespace Nancy.Hosting.Self.Tests +{ + using System; + + using Nancy.Tests; + + using Xunit; + + public class IsCaseInstensitiveBaseOfFixture + { + private readonly Uri baseUri = new Uri("http://host/path/path/file"); + private readonly Uri baseSlashUri = new Uri("http://host/path/path/"); + private readonly Uri baseLocalHostUri = new Uri("http://localhost/path/path/"); + + [Fact] + public void url_should_be_base_of_sub_directory() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path/file/")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_sub_path_with_fragment() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path/file#fragment")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_path_with_more_dirs() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path/file/MoreDir/")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_sub_path_with_file_and_query() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path/file/OtherFile?Query")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_path_with_extra_slash() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path/file/")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_sub_file() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path/file")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_not_be_base_of_other_scheme() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("https://host/path/path/file")); + + // Then + isBaseOf.ShouldBeFalse(); + } + + [Fact] + public void url_should_not_be_base_of_other_host() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://otherhost/path/path/file")); + + // Then + isBaseOf.ShouldBeFalse(); + } + + [Fact] + public void url_should_not_be_base_of_other_port() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://otherhost:8080/path/path/file")); + + // Then + isBaseOf.ShouldBeFalse(); + } + + [Fact] + public void url_should_be_base_of_host_with_different_casing() + { + // Given, When + var isBaseOf = baseUri.IsCaseInsensitiveBaseOf(new Uri("http://Host/path/path/file")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_exact_path_without_trailing_slash() + { + // Given, When + var isBaseOf = baseSlashUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_exact_path_without_trailing_slash_with_query() + { + // Given, When + var isBaseOf = baseSlashUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path?query")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_exact_path_without_trailing_slash_with_fragment() + { + // Given, When + var isBaseOf = baseSlashUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path#Fragment")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_not_be_base_of_other_path() + { + // Given, When + var isBaseOf = baseSlashUri.IsCaseInsensitiveBaseOf(new Uri("http://host/path/path2/")); + + // Then + isBaseOf.ShouldBeFalse(); + } + + [Fact] + public void url_should_be_base_of_same_path_with_different_host_casing() + { + // Given, When + var isBaseOf = baseSlashUri.IsCaseInsensitiveBaseOf(new Uri("http://Host/path/path/")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_same_path_with_different_path_casing() + { + // Given, When + var isBaseOf = baseSlashUri.IsCaseInsensitiveBaseOf(new Uri("http://host/Path/PATH/")); + + // Then + isBaseOf.ShouldBeTrue(); + } + + [Fact] + public void url_should_be_base_of_same_path_with_different_host_using_localhost_wildcard() + { + // Given, When + var isBaseOf = baseLocalHostUri.IsCaseInsensitiveBaseOf(new Uri("http://OtherHost/path/path/file")); + + // Then + isBaseOf.ShouldBeTrue(); + } + } +} diff --git a/src/Nancy.Hosting.Self.Tests/MakeAppLocalPathFixture.cs b/src/Nancy.Hosting.Self.Tests/MakeAppLocalPathFixture.cs new file mode 100644 index 0000000000..19cdc53fdc --- /dev/null +++ b/src/Nancy.Hosting.Self.Tests/MakeAppLocalPathFixture.cs @@ -0,0 +1,102 @@ +namespace Nancy.Hosting.Self.Tests +{ + using System; + + using Nancy.Tests; + + using Xunit; + + public class MakeAppLocalPathFixture + { + [Fact] + public void Should_return_path_as_local_path() + { + // Given + var uri = new Uri("http://host/base/"); + + // When + string result = uri.MakeAppLocalPath(new Uri("http://host/base/rel")); + + // Then + result.ShouldEqual("/rel"); + } + + [Fact] + public void Should_return_root_path_with_trailing_slash_as_slash() + { + // Given + var uri = new Uri("http://host/base/"); + + // When + string result = uri.MakeAppLocalPath(new Uri("http://host/base/")); + + // Then + result.ShouldEqual("/"); + } + + [Fact] + public void Should_return_root_path_without_trailing_slash_as_slash() + { + // Given + var uri = new Uri("http://host/base/"); + + // When + string result = uri.MakeAppLocalPath(new Uri("http://host/base")); + + // Then + result.ShouldEqual("/"); + } + + [Fact] + public void Should_return_path_with_same_casing_as_full_uri() + { + // Given + var uri = new Uri("http://host/base/"); + + // When + string result = uri.MakeAppLocalPath(new Uri("http://host/base/ReL")); + + // Then + result.ShouldEqual("/ReL"); + } + + [Fact] + public void Should_support_extended_site_root() + { + // Given + var uri = new Uri("http://host/"); + + // When + string result = uri.MakeAppLocalPath(new Uri("http://host/rel/file")); + + // Then + result.ShouldEqual("/rel/file"); + } + + [Fact] + public void Should_support_site_root_without_trailing_slash() + { + // Given + var uri = new Uri("http://host/"); + + // When + string result = uri.MakeAppLocalPath(new Uri("http://host")); + + // Then + result.ShouldEqual("/"); + } + + [Fact] + public void Should_return_path_with_case_insensitive_base_uri_comparison() + { + // Given + var uri = new Uri("http://host/base/"); + + // When + string result = uri.MakeAppLocalPath(new Uri("http://host/Base/rel")); + + // Then + result.ShouldEqual("/rel"); + } + } +} \ No newline at end of file diff --git a/src/Nancy.Hosting.Self.Tests/Nancy.Hosting.Self.Tests.csproj b/src/Nancy.Hosting.Self.Tests/Nancy.Hosting.Self.Tests.csproj new file mode 100644 index 0000000000..1adb443276 --- /dev/null +++ b/src/Nancy.Hosting.Self.Tests/Nancy.Hosting.Self.Tests.csproj @@ -0,0 +1,173 @@ + + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8} + Library + Properties + Nancy.Hosting.Self.Tests + Nancy.Hosting.Self.Tests + v4.5 + 512 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + false + + + true + bin\MonoDebug\ + DEBUG;TRACE + full + AnyCPU + bin\Debug\Nancy.Hosting.Self.Tests.dll.CodeAnalysisLog.xml + true + GlobalSuppressions.cs + prompt + AllRules.ruleset + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets + false + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules + true + false + + + bin\MonoRelease\ + TRACE + true + pdbonly + AnyCPU + bin\Release\Nancy.Hosting.Self.Tests.dll.CodeAnalysisLog.xml + true + GlobalSuppressions.cs + prompt + AllRules.ruleset + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets + true + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules + true + false + + + + False + ..\packages\FakeItEasy.1.19.0\lib\net40\FakeItEasy.dll + + + + + + + + + + False + ..\packages\xunit.1.9.1\lib\net20\xunit.dll + + + False + ..\packages\xunit.extensions.1.9.1\lib\net20\xunit.extensions.dll + + + + + ShouldExtensions.cs + + + SkipException.cs + + + SkippableFactAttribute.cs + + + Properties\SharedAssemblyInfo.cs + + + + + + + + + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134} + Nancy.Hosting.Self + + + {34576216-0DCA-4B0F-A0DC-9075E75A676F} + Nancy + + + + + False + Microsoft .NET Framework 4 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/Nancy.Hosting.Self.Tests/NancySelfHostFixture.cs b/src/Nancy.Hosting.Self.Tests/NancySelfHostFixture.cs new file mode 100644 index 0000000000..cc058076ef --- /dev/null +++ b/src/Nancy.Hosting.Self.Tests/NancySelfHostFixture.cs @@ -0,0 +1,268 @@ +#if !__MonoCS__ +namespace Nancy.Hosting.Self.Tests +{ + using System; + using System.IO; + using System.Linq; + using System.Net; + using System.Threading; + + using FakeItEasy; + + using Nancy.Bootstrapper; + using Nancy.Helpers; + using Nancy.Tests; + using Nancy.Tests.xUnitExtensions; + + using Xunit; + + /// + /// These tests attempt to listen on port 1234, and so require either administrative + /// privileges or that a command similar to the following has been run with + /// administrative privileges: + /// netsh http add urlacl url=http://+:1234/base user=DOMAIN\user + /// See http://msdn.microsoft.com/en-us/library/ms733768.aspx for more information. + /// + public class NancySelfHostFixture + { + private static readonly Uri BaseUri = new Uri("http://localhost:1234/base/"); + + [SkippableFact] + public void Should_be_get_an_exception_indicating_a_conflict_when_trying_to_listen_on_a_used_prefix() + { + Exception ex; + + // Given + using (CreateAndOpenSelfHost()) + { + // When + ex = Record.Exception(() => + { + using (var host = new NancyHost(BaseUri)) + { + host.Start(); + } + }); + } + + // Then + ex.Message.ShouldContain("conflict"); + } + + [SkippableFact] + public void Should_be_able_to_get_any_header_from_selfhost() + { + // Given + using (CreateAndOpenSelfHost()) + { + // When + var request = WebRequest.Create(new Uri(BaseUri, "rel/header/?query=value")); + request.Method = "GET"; + + // Then + request.GetResponse().Headers["X-Some-Header"].ShouldEqual("Some value"); + } + } + + [SkippableFact] + public void Should_set_query_string_and_uri_correctly() + { + // Given + Request nancyRequest = null; + var fakeEngine = A.Fake(); + A.CallTo(() => fakeEngine.HandleRequest(A.Ignored, A>.Ignored,A.Ignored)) + .Invokes(f => nancyRequest = (Request)f.Arguments[0]) + .ReturnsLazily(c => TaskHelpers.GetCompletedTask(new NancyContext { Request = (Request)c.Arguments[0], Response = new Response() })); + + var fakeBootstrapper = A.Fake(); + A.CallTo(() => fakeBootstrapper.GetEngine()).Returns(fakeEngine); + + // When + using (CreateAndOpenSelfHost(fakeBootstrapper)) + { + var request = WebRequest.Create(new Uri(BaseUri, "test/stuff?query=value&query2=value2")); + request.Method = "GET"; + + try + { + request.GetResponse(); + } + catch (WebException) + { + // Will throw because it returns 404 - don't care. + } + } + + // Then + nancyRequest.Path.ShouldEqual("/test/stuff"); + Assert.True(nancyRequest.Query.query.HasValue); + Assert.True(nancyRequest.Query.query2.HasValue); + } + + [SkippableFact] + public void Should_be_able_to_get_from_selfhost() + { + using (CreateAndOpenSelfHost()) + { + var reader = + new StreamReader(WebRequest.Create(new Uri(BaseUri, "rel")).GetResponse().GetResponseStream()); + + var response = reader.ReadToEnd(); + + response.ShouldEqual("This is the site route"); + } + } + + [SkippableFact] + public void Should_be_able_to_get_from_chunked_selfhost() + { + using (CreateAndOpenSelfHost()) + { + var response = WebRequest.Create(new Uri(BaseUri, "rel")).GetResponse(); + + Assert.Equal("chunked", response.Headers["Transfer-Encoding"]); + Assert.Equal(null, response.Headers["Content-Length"]); + + using (var reader = new StreamReader(response.GetResponseStream())) + { + var contents = reader.ReadToEnd(); + contents.ShouldEqual("This is the site route"); + } + } + } + + [SkippableFact] + public void Should_be_able_to_get_from_contentlength_selfhost() + { + HostConfiguration configuration = new HostConfiguration() + { + AllowChunkedEncoding = false + }; + using (CreateAndOpenSelfHost(null, configuration)) + { + var response = WebRequest.Create(new Uri(BaseUri, "rel")).GetResponse(); + + Assert.Equal(null, response.Headers["Transfer-Encoding"]); + Assert.Equal(22, Convert.ToInt32(response.Headers["Content-Length"])); + + using (var reader = new StreamReader(response.GetResponseStream())) + { + var contents = reader.ReadToEnd(); + contents.ShouldEqual("This is the site route"); + } + } + } + + [SkippableFact] + public void Should_be_able_to_post_body_to_selfhost() + { + using (CreateAndOpenSelfHost()) + { + const string testBody = "This is the body of the request"; + + var request = + WebRequest.Create(new Uri(BaseUri, "rel")); + request.Method = "POST"; + + var writer = + new StreamWriter(request.GetRequestStream()) { AutoFlush = true }; + writer.Write(testBody); + + var responseBody = + new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd(); + + responseBody.ShouldEqual(testBody); + } + } + + [SkippableFact] + public void Should_be_able_to_get_from_selfhost_with_slashless_uri() + { + using (CreateAndOpenSelfHost()) + { + var reader = + new StreamReader(WebRequest.Create(BaseUri.ToString().TrimEnd('/')).GetResponse().GetResponseStream()); + + var response = reader.ReadToEnd(); + + response.ShouldEqual("This is the site home"); + } + } + + private static NancyHostWrapper CreateAndOpenSelfHost(INancyBootstrapper nancyBootstrapper = null, HostConfiguration configuration = null) + { + if (nancyBootstrapper == null) + { + nancyBootstrapper = new DefaultNancyBootstrapper(); + } + + var host = new NancyHost( + nancyBootstrapper, + configuration, + BaseUri); + + try + { + host.Start(); + } + catch + { + throw new SkipException("Skipped due to no Administrator access - please see test fixture for more information."); + } + + return new NancyHostWrapper(host); + } + + + [SkippableFact] + public void Should_be_able_to_recover_from_rendering_exception() + { + using (CreateAndOpenSelfHost()) + { + + var reader = + new StreamReader(WebRequest.Create(new Uri(BaseUri, "exception")).GetResponse().GetResponseStream()); + + var response = reader.ReadToEnd(); + + response.ShouldEqual("Content"); + } + } + + [SkippableFact] + public void Should_be_serializable() + { + var type = typeof(NancyHost); + Assert.True(type.Attributes.ToString().Contains("Serializable")); + } + + [Fact] + public void Should_include_default_port_in_uri_prefixes() + { + // Given + var host = new NancyHost(new Uri("http://localhost/")); + + // When + var prefix = host.GetPrefixes().Single(); + + // Then + prefix.ShouldEqual("http://+:80/"); + } + + private class NancyHostWrapper : IDisposable + { + private readonly NancyHost host; + + public NancyHostWrapper(NancyHost host) + { + this.host = host; + } + + public void Dispose() + { + host.Stop(); + } + } + } +} +#endif diff --git a/src/Nancy.Hosting.Self.Tests/TestModule.cs b/src/Nancy.Hosting.Self.Tests/TestModule.cs new file mode 100644 index 0000000000..463fb0eb32 --- /dev/null +++ b/src/Nancy.Hosting.Self.Tests/TestModule.cs @@ -0,0 +1,33 @@ +namespace Nancy.Hosting.Self.Tests +{ + using System; + using System.IO; + + public class TestModule : NancyModule + { + public TestModule() + { + Get["/"] = parameters => "This is the site home"; + + Get["/rel"] = parameters => "This is the site route"; + + Get["/rel/header"] = parameters => + { + var response = new Response(); + response.Headers["X-Some-Header"] = "Some value"; + + return response; + }; + + Post["/rel"] = parameters => new StreamReader(this.Request.Body).ReadToEnd(); + + Get["/exception"] = parameters => new Response() {Contents = s => + { + var writer = new StreamWriter(s); + writer.Write("Content"); + writer.Flush(); + throw new Exception("An error occured during content rendering"); + }}; + } + } +} \ No newline at end of file diff --git a/src/Nancy.Hosting.Self.Tests/packages.config b/src/Nancy.Hosting.Self.Tests/packages.config new file mode 100644 index 0000000000..18f13949c6 --- /dev/null +++ b/src/Nancy.Hosting.Self.Tests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Nancy.Hosting.Self/AutomaticUrlReservationCreationFailureException.cs b/src/Nancy.Hosting.Self/AutomaticUrlReservationCreationFailureException.cs new file mode 100644 index 0000000000..f54c1f59f5 --- /dev/null +++ b/src/Nancy.Hosting.Self/AutomaticUrlReservationCreationFailureException.cs @@ -0,0 +1,52 @@ +namespace Nancy.Hosting.Self +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// Exception for when automatic address reservation creation fails. + /// Provides the user with manual instructions. + /// + public class AutomaticUrlReservationCreationFailureException : Exception + { + private readonly IEnumerable prefixes; + private readonly string user; + + public AutomaticUrlReservationCreationFailureException(IEnumerable prefixes, string user) + { + this.prefixes = prefixes; + this.user = user; + } + + /// + /// Gets a message that describes the current exception. + /// + /// + /// The error message that explains the reason for the exception, or an empty string(""). + /// + /// 1 + public override string Message + { + get + { + var stringBuilder = new StringBuilder(); + + stringBuilder.AppendLine("The Nancy self host was unable to start, as no namespace reservation existed for the provided url(s)."); + stringBuilder.AppendLine(); + + stringBuilder.AppendLine("Please either enable UrlReservations.CreateAutomatically on the HostConfiguration provided to "); + stringBuilder.AppendLine("the NancyHost, or create the reservations manually with the (elevated) command(s):"); + stringBuilder.AppendLine(); + + foreach (var prefix in prefixes) + { + var command = NetSh.GetParameters(prefix, user); + stringBuilder.AppendLine(string.Format("netsh {0}", command)); + } + + return stringBuilder.ToString(); + } + } + } +} diff --git a/src/Nancy.Hosting.Self/FileSystemRootPathProvider.cs b/src/Nancy.Hosting.Self/FileSystemRootPathProvider.cs new file mode 100644 index 0000000000..9fbf119cdb --- /dev/null +++ b/src/Nancy.Hosting.Self/FileSystemRootPathProvider.cs @@ -0,0 +1,17 @@ +namespace Nancy.Hosting.Self +{ + using System.IO; + using System.Reflection; + + public class FileSystemRootPathProvider : IRootPathProvider + { + public string GetRootPath() + { + var assembly = Assembly.GetEntryAssembly(); + + return assembly != null ? + Path.GetDirectoryName(assembly.Location) : + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + } + } +} diff --git a/src/Nancy.Hosting.Self/HostConfiguration.cs b/src/Nancy.Hosting.Self/HostConfiguration.cs new file mode 100644 index 0000000000..1007cf193a --- /dev/null +++ b/src/Nancy.Hosting.Self/HostConfiguration.cs @@ -0,0 +1,70 @@ +namespace Nancy.Hosting.Self +{ + using System; + using System.Diagnostics; + + /// + /// Host configuration for the self host + /// + public sealed class HostConfiguration + { + /// + /// Gets or sets a property that determines if localhost uris are + /// rewritten to htp://+:port/ style uris to allow for listening on + /// all ports, but requiring either a url reservation, or admin + /// access + /// Defaults to true. + /// + public bool RewriteLocalhost { get; set; } + + /// + /// Configuration around automatically creating url reservations + /// + public UrlReservations UrlReservations { get; set; } + + /// + /// Gets or sets a property that determines if Transfer-Encoding: Chunked is allowed + /// for the response instead of Content-Length (default: true). + /// + public bool AllowChunkedEncoding { get; set; } + + /// + /// Gets or sets a property that provides a callback to be called + /// if there's an unhandled exception in the self host. + /// Note: this will *not* be called for normal nancy Exceptions + /// that are handled by the Nancy handlers. + /// Defaults to writing to debug out. + /// + public Action UnhandledExceptionCallback { get; set; } + + /// + /// Gets or sets a property that determines whether client certificates + /// are enabled or not. + /// When set to true the self host will request a client certificate if the + /// request is running over SSL. + /// Defaults to false. + /// + public bool EnableClientCertificates { get; set; } + + /// + /// Gets or sets a property determining if base uri matching can fall back to just + /// using Authority (Schema + Host + Port) as base uri if it cannot match anything in + /// the known list. This should only be set to True if you have issues with port forwarding + /// (e.g. on Azure). + /// + public bool AllowAuthorityFallback { get; set; } + + public HostConfiguration() + { + this.RewriteLocalhost = true; + this.UrlReservations = new UrlReservations(); + this.AllowChunkedEncoding = true; + this.UnhandledExceptionCallback = e => + { + var message = string.Format("---\n{0}\n---\n", e); + Debug.Write(message); + }; + this.EnableClientCertificates = false; + } + } +} diff --git a/src/Nancy.Hosting.Self/IgnoredHeaders.cs b/src/Nancy.Hosting.Self/IgnoredHeaders.cs new file mode 100644 index 0000000000..4a6ea2da69 --- /dev/null +++ b/src/Nancy.Hosting.Self/IgnoredHeaders.cs @@ -0,0 +1,34 @@ +namespace Nancy.Hosting.Self +{ + using System; + using System.Collections.Generic; + + /// + /// A helper class that checks for a header against a list of headers that should be ignored + /// when populating the headers of an object. + /// + public static class IgnoredHeaders + { + + private static readonly HashSet knownHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "content-length", + "content-type", + "transfer-encoding", + "keep-alive" + }; + + /// + /// Determines if a header is ignored when populating the headers of an + /// object. + /// + /// The name of the header. + /// true if the header is ignored; otherwise, false. + public static bool IsIgnored(string headerName) + { + return knownHeaders.Contains(headerName); + } + + } + +} diff --git a/src/Nancy.Hosting.Self/Nancy.Hosting.Self.csproj b/src/Nancy.Hosting.Self/Nancy.Hosting.Self.csproj new file mode 100644 index 0000000000..8ded63f0c9 --- /dev/null +++ b/src/Nancy.Hosting.Self/Nancy.Hosting.Self.csproj @@ -0,0 +1,161 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134} + Library + Properties + Nancy.Hosting.Self + Nancy.Hosting.Self + v4.5 + 512 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + bin\Debug\Nancy.Hosting.Self.XML + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + bin\Release\Nancy.Hosting.Self.XML + false + + + true + bin\MonoDebug\ + DEBUG;TRACE + full + AnyCPU + bin\Debug\Nancy.Hosting.Self.dll.CodeAnalysisLog.xml + true + GlobalSuppressions.cs + prompt + AllRules.ruleset + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules + false + false + 4 + false + bin\MonoDebug\Nancy.Hosting.Self.XML + false + + + bin\MonoRelease\ + TRACE + true + pdbonly + AnyCPU + bin\Release\Nancy.Hosting.Self.dll.CodeAnalysisLog.xml + true + GlobalSuppressions.cs + prompt + AllRules.ruleset + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules + 4 + bin\MonoRelease\Nancy.Hosting.Self.XML + false + + + + + + + + + + + + + + + + + + + + Properties\SharedAssemblyInfo.cs + + + + + + + + + + + Code + + + + + {34576216-0DCA-4B0F-A0DC-9075E75A676F} + Nancy + + + + + False + Microsoft .NET Framework 4 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + + + \ No newline at end of file diff --git a/src/Nancy.Hosting.Self/NancyHost.cs b/src/Nancy.Hosting.Self/NancyHost.cs new file mode 100644 index 0000000000..314c5aed4d --- /dev/null +++ b/src/Nancy.Hosting.Self/NancyHost.cs @@ -0,0 +1,451 @@ +namespace Nancy.Hosting.Self +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Security.Principal; + + using Nancy.Bootstrapper; + using Nancy.Extensions; + using Nancy.Helpers; + using Nancy.IO; + + /// + /// Allows to host Nancy server inside any application - console or windows service. + /// + /// + /// NancyHost uses internally. Therefore, it requires full .net 4.0 profile (not client profile) + /// to run. will launch a thread that will listen for requests and then process them. Each request is processed in + /// its own execution thread. NancyHost needs in order to be used from another appdomain under + /// mono. Working with AppDomains is necessary if you want to unload the dependencies that come with NancyHost. + /// + [Serializable] + public class NancyHost : IDisposable + { + private const int ACCESS_DENIED = 5; + + private readonly IList baseUriList; + private HttpListener listener; + private readonly INancyEngine engine; + private readonly HostConfiguration configuration; + private readonly INancyBootstrapper bootstrapper; + + /// + /// Initializes a new instance of the class for the specified . + /// Uses the default configuration + /// + /// The s that the host will listen to. + public NancyHost(params Uri[] baseUris) + : this(NancyBootstrapperLocator.Bootstrapper, new HostConfiguration(), baseUris) { } + + /// + /// Initializes a new instance of the class for the specified . + /// Uses the specified configuration. + /// + /// The s that the host will listen to. + /// Configuration to use + public NancyHost(HostConfiguration configuration, params Uri[] baseUris) + : this(NancyBootstrapperLocator.Bootstrapper, configuration, baseUris){} + + /// + /// Initializes a new instance of the class for the specified , using + /// the provided . + /// Uses the default configuration + /// + /// The bootstrapper that should be used to handle the request. + /// The s that the host will listen to. + public NancyHost(INancyBootstrapper bootstrapper, params Uri[] baseUris) + : this(bootstrapper, new HostConfiguration(), baseUris) + { + } + + /// + /// Initializes a new instance of the class for the specified , using + /// the provided . + /// Uses the specified configuration. + /// + /// The bootstrapper that should be used to handle the request. + /// Configuration to use + /// The s that the host will listen to. + public NancyHost(INancyBootstrapper bootstrapper, HostConfiguration configuration, params Uri[] baseUris) + { + this.bootstrapper = bootstrapper; + this.configuration = configuration ?? new HostConfiguration(); + this.baseUriList = baseUris; + + bootstrapper.Initialise(); + this.engine = bootstrapper.GetEngine(); + } + + /// + /// Initializes a new instance of the class for the specified , using + /// the provided . + /// Uses the default configuration + /// + /// The that the host will listen to. + /// The bootstrapper that should be used to handle the request. + public NancyHost(Uri baseUri, INancyBootstrapper bootstrapper) + : this(bootstrapper, new HostConfiguration(), baseUri) + { + } + + /// + /// Initializes a new instance of the class for the specified , using + /// the provided . + /// Uses the specified configuration. + /// + /// The that the host will listen to. + /// The bootstrapper that should be used to handle the request. + /// Configuration to use + public NancyHost(Uri baseUri, INancyBootstrapper bootstrapper, HostConfiguration configuration) + : this (bootstrapper, configuration, baseUri) + { + } + + /// + /// Stops the host if it is running. + /// + public void Dispose() + { + this.Stop(); + + this.bootstrapper.Dispose(); + } + + /// + /// Start listening for incoming requests with the given configuration + /// + public void Start() + { + this.StartListener(); + + try + { + this.listener.BeginGetContext(this.GotCallback, null); + } + catch (Exception e) + { + this.configuration.UnhandledExceptionCallback.Invoke(e); + + throw; + } + } + + private void StartListener() + { + if (this.TryStartListener()) + { + return; + } + + if (!this.configuration.UrlReservations.CreateAutomatically) + { + throw new AutomaticUrlReservationCreationFailureException(this.GetPrefixes(), this.GetUser()); + } + + if (!this.TryAddUrlReservations()) + { + throw new InvalidOperationException("Unable to configure namespace reservation"); + } + + if (!TryStartListener()) + { + throw new InvalidOperationException("Unable to start listener"); + } + } + + private bool TryStartListener() + { + try + { + // if the listener fails to start, it gets disposed; + // so we need a new one, each time. + this.listener = new HttpListener(); + foreach (var prefix in this.GetPrefixes()) + { + this.listener.Prefixes.Add(prefix); + } + + this.listener.Start(); + + return true; + } + catch (HttpListenerException e) + { + if (e.ErrorCode == ACCESS_DENIED) + { + return false; + } + + throw; + } + } + + private bool TryAddUrlReservations() + { + var user = this.GetUser(); + + foreach (var prefix in this.GetPrefixes()) + { + if (!NetSh.AddUrlAcl(prefix, user)) + { + return false; + } + } + + return true; + } + + private string GetUser() + { + if (!string.IsNullOrWhiteSpace(this.configuration.UrlReservations.User)) + { + return this.configuration.UrlReservations.User; + } + + return WindowsIdentity.GetCurrent().Name; + } + + /// + /// Stop listening for incoming requests. + /// + public void Stop() + { + if (this.listener.IsListening) + { + listener.Stop(); + } + } + + internal IEnumerable GetPrefixes() + { + foreach (var baseUri in this.baseUriList) + { + var prefix = new UriBuilder(baseUri).ToString(); + + if (this.configuration.RewriteLocalhost && !baseUri.Host.Contains(".")) + { + prefix = prefix.Replace("localhost", "+"); + } + + yield return prefix; + } + } + + private Request ConvertRequestToNancyRequest(HttpListenerRequest request) + { + var baseUri = this.GetBaseUri(request); + + if (baseUri == null) + { + throw new InvalidOperationException(String.Format("Unable to locate base URI for request: {0}",request.Url)); + } + + var expectedRequestLength = + GetExpectedRequestLength(request.Headers.ToDictionary()); + + var relativeUrl = baseUri.MakeAppLocalPath(request.Url); + + var nancyUrl = new Url + { + Scheme = request.Url.Scheme, + HostName = request.Url.Host, + Port = request.Url.IsDefaultPort ? null : (int?)request.Url.Port, + BasePath = baseUri.AbsolutePath.TrimEnd('/'), + Path = HttpUtility.UrlDecode(relativeUrl), + Query = request.Url.Query, + }; + + byte[] certificate = null; + + if (this.configuration.EnableClientCertificates) + { + var x509Certificate = request.GetClientCertificate(); + + if (x509Certificate != null) + { + certificate = x509Certificate.RawData; + } + } + + // NOTE: For HTTP/2 we want fieldCount = 1, + // otherwise (HTTP/1.0 and HTTP/1.1) we want fieldCount = 2 + var fieldCount = request.ProtocolVersion.Major == 2 ? 1 : 2; + + var protocolVersion = string.Format("HTTP/{0}", request.ProtocolVersion.ToString(fieldCount)); + + return new Request( + request.HttpMethod, + nancyUrl, + RequestStream.FromStream(request.InputStream, expectedRequestLength, StaticConfiguration.DisableRequestStreamSwitching ?? false), + request.Headers.ToDictionary(), + (request.RemoteEndPoint != null) ? request.RemoteEndPoint.Address.ToString() : null, + certificate, + protocolVersion); + } + + private Uri GetBaseUri(HttpListenerRequest request) + { + var result = this.baseUriList.FirstOrDefault(uri => uri.IsCaseInsensitiveBaseOf(request.Url)); + + if (result != null) + { + return result; + } + + if (!this.configuration.AllowAuthorityFallback) + { + return null; + } + + return new Uri(request.Url.GetLeftPart(UriPartial.Authority)); + } + + private void ConvertNancyResponseToResponse(Response nancyResponse, HttpListenerResponse response) + { + foreach (var header in nancyResponse.Headers) + { + if (!IgnoredHeaders.IsIgnored(header.Key)) + { + response.AddHeader(header.Key, header.Value); + } + } + + foreach (var nancyCookie in nancyResponse.Cookies) + { + response.Headers.Add(HttpResponseHeader.SetCookie, nancyCookie.ToString()); + } + + if (nancyResponse.ReasonPhrase != null) + { + response.StatusDescription = nancyResponse.ReasonPhrase; + } + + if (nancyResponse.ContentType != null) + { + response.ContentType = nancyResponse.ContentType; + } + + response.StatusCode = (int)nancyResponse.StatusCode; + + if (configuration.AllowChunkedEncoding) + { + OutputWithDefaultTransferEncoding(nancyResponse, response); + } + else + { + OutputWithContentLength(nancyResponse, response); + } + } + + private static void OutputWithDefaultTransferEncoding(Response nancyResponse, HttpListenerResponse response) + { + using (var output = response.OutputStream) + { + nancyResponse.Contents.Invoke(output); + } + } + + private static void OutputWithContentLength(Response nancyResponse, HttpListenerResponse response) + { + byte[] buffer; + using (var memoryStream = new MemoryStream()) + { + nancyResponse.Contents.Invoke(memoryStream); + buffer = memoryStream.ToArray(); + } + + var contentLength = (nancyResponse.Headers.ContainsKey("Content-Length")) ? + Convert.ToInt64(nancyResponse.Headers["Content-Length"]) : + buffer.Length; + + response.SendChunked = false; + response.ContentLength64 = contentLength; + + using (var output = response.OutputStream) + { + using (var writer = new BinaryWriter(output)) + { + writer.Write(buffer); + writer.Flush(); + } + } + } + + private static long GetExpectedRequestLength(IDictionary> incomingHeaders) + { + if (incomingHeaders == null) + { + return 0; + } + + if (!incomingHeaders.ContainsKey("Content-Length")) + { + return 0; + } + + var headerValue = + incomingHeaders["Content-Length"].SingleOrDefault(); + + if (headerValue == null) + { + return 0; + } + + long contentLength; + + return !long.TryParse(headerValue, NumberStyles.Any, CultureInfo.InvariantCulture, out contentLength) ? + 0 : + contentLength; + } + + private void GotCallback(IAsyncResult ar) + { + try + { + var ctx = this.listener.EndGetContext(ar); + this.listener.BeginGetContext(this.GotCallback, null); + this.Process(ctx); + } + catch (Exception e) + { + this.configuration.UnhandledExceptionCallback.Invoke(e); + + try + { + this.listener.BeginGetContext(this.GotCallback, null); + } + catch + { + this.configuration.UnhandledExceptionCallback.Invoke(e); + } + } + } + + private void Process(HttpListenerContext ctx) + { + try + { + var nancyRequest = this.ConvertRequestToNancyRequest(ctx.Request); + using (var nancyContext = this.engine.HandleRequest(nancyRequest)) + { + try + { + ConvertNancyResponseToResponse(nancyContext.Response, ctx.Response); + } + catch (Exception e) + { + this.configuration.UnhandledExceptionCallback.Invoke(e); + } + } + } + catch (Exception e) + { + this.configuration.UnhandledExceptionCallback.Invoke(e); + } + } + } +} diff --git a/src/Nancy.Hosting.Self/NetSh.cs b/src/Nancy.Hosting.Self/NetSh.cs new file mode 100644 index 0000000000..b86d45ce1c --- /dev/null +++ b/src/Nancy.Hosting.Self/NetSh.cs @@ -0,0 +1,37 @@ +namespace Nancy.Hosting.Self +{ + using System; + + /// + /// Executes NetSh commands + /// + public static class NetSh + { + private const string NetshCommand = "netsh"; + + /// + /// Add a url reservation + /// + /// Url to add + /// User to add the reservation for + /// True if successful, false otherwise. + public static bool AddUrlAcl(string url, string user) + { + try + { + var arguments = GetParameters(url, user); + + return UacHelper.RunElevated(NetshCommand, arguments); + } + catch (Exception) + { + return false; + } + } + + internal static string GetParameters(string url, string user) + { + return string.Format("http add urlacl url=\"{0}\" user=\"{1}\"", url, user); + } + } +} \ No newline at end of file diff --git a/src/Nancy.Hosting.Self/UacHelper.cs b/src/Nancy.Hosting.Self/UacHelper.cs new file mode 100644 index 0000000000..62c1c296d2 --- /dev/null +++ b/src/Nancy.Hosting.Self/UacHelper.cs @@ -0,0 +1,39 @@ +namespace Nancy.Hosting.Self +{ + using System.Diagnostics; + + /// + /// Helpers for UAC + /// + public static class UacHelper + { + /// + /// Run an executable elevated + /// + /// File to execute + /// Arguments to pass to the executable + /// True if successful, false otherwise + public static bool RunElevated(string file, string args) + { + var process = CreateProcess(args, file); + + process.Start(); + process.WaitForExit(); + + return process.ExitCode == 0; + } + + private static Process CreateProcess(string args, string file) + { + return new Process + { + StartInfo = new ProcessStartInfo + { + Verb = "runas", + Arguments = args, + FileName = file, + } + }; + } + } +} \ No newline at end of file diff --git a/src/Nancy.Hosting.Self/UriExtensions.cs b/src/Nancy.Hosting.Self/UriExtensions.cs new file mode 100644 index 0000000000..a7e0ca7fdf --- /dev/null +++ b/src/Nancy.Hosting.Self/UriExtensions.cs @@ -0,0 +1,129 @@ +namespace Nancy.Hosting.Self +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// Extension methods for working with instances. + /// + public static class UriExtensions + { + public static bool IsCaseInsensitiveBaseOf(this Uri source, Uri value) + { + var uriComponents = source.Host == "localhost" ? (UriComponents.Port | UriComponents.Scheme) : (UriComponents.HostAndPort | UriComponents.Scheme); + if (Uri.Compare(source, value, uriComponents, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) != 0) + { + return false; + } + + var sourceSegments = source.Segments; + var valueSegments = value.Segments; + + return sourceSegments.ZipCompare(valueSegments, (s1, s2) => s1.Length == 0 || SegmentEquals(s1, s2)); + } + + public static string MakeAppLocalPath(this Uri appBaseUri, Uri fullUri) + { + return string.Concat("/", appBaseUri.Segments.ZipFill(fullUri.Segments, (x, y) => x != null && SegmentEquals(x, y) ? null : y).Join()); + } + + private static string AppendSlashIfNeeded(string segment) + { + if (!segment.EndsWith("/")) + { + segment = string.Concat(segment, "/"); + } + + return segment; + } + + private static bool SegmentEquals(string segment1, string segment2) + { + return String.Equals(AppendSlashIfNeeded(segment1), AppendSlashIfNeeded(segment2), StringComparison.OrdinalIgnoreCase); + } + + private static bool ZipCompare(this IEnumerable source1, IEnumerable source2, Func comparison) + { + using (var enumerator1 = source1.GetEnumerator()) + { + using (var enumerator2 = source2.GetEnumerator()) + { + var has1 = enumerator1.MoveNext(); + var has2 = enumerator2.MoveNext(); + + while (has1 || has2) + { + var current1 = has1 ? enumerator1.Current : ""; + var current2 = has2 ? enumerator2.Current : ""; + + if (!comparison(current1, current2)) + { + return false; + } + + if (has1) + { + has1 = enumerator1.MoveNext(); + } + + if (has2) + { + has2 = enumerator2.MoveNext(); + } + } + + } + } + + return true; + } + + private static IEnumerable ZipFill(this IEnumerable source1, IEnumerable source2, Func selector) + { + using (var enumerator1 = source1.GetEnumerator()) + { + using (var enumerator2 = source2.GetEnumerator()) + { + var has1 = enumerator1.MoveNext(); + var has2 = enumerator2.MoveNext(); + + while (has1 || has2) + { + var value1 = has1 ? enumerator1.Current : null; + var value2 = has2 ? enumerator2.Current : null; + var value = selector(value1, value2); + + if (value != null) + { + yield return value; + } + + if (has1) + { + has1 = enumerator1.MoveNext(); + } + + if (has2) + { + has2 = enumerator2.MoveNext(); + } + } + + } + } + } + + private static string Join(this IEnumerable source) + { + var builder = new StringBuilder(); + + foreach (var value in source) + { + builder.Append(value); + } + + return builder.ToString(); + } + } +} diff --git a/src/Nancy.Hosting.Self/UrlReservations.cs b/src/Nancy.Hosting.Self/UrlReservations.cs new file mode 100644 index 0000000000..5ed49734f7 --- /dev/null +++ b/src/Nancy.Hosting.Self/UrlReservations.cs @@ -0,0 +1,53 @@ +namespace Nancy.Hosting.Self +{ + using System; + using System.Security.Principal; + + /// + /// Configuration for automatic url reservation creation + /// + public class UrlReservations + { + private const string EveryoneAccountName = "Everyone"; + + private static readonly IdentityReference EveryoneReference = + new SecurityIdentifier(WellKnownSidType.WorldSid, null); + + public UrlReservations() + { + this.CreateAutomatically = false; + this.User = GetEveryoneAccountName(); + } + + /// + /// Gets or sets a value indicating whether url reservations + /// are automatically created when necessary. + /// Defaults to false. + /// + public bool CreateAutomatically { get; set; } + + /// + /// Gets or sets a value for the user to use to create the url reservations for. + /// Defaults to the "Everyone" group. + /// + public string User { get; set; } + + private static string GetEveryoneAccountName() + { + try + { + var account = EveryoneReference.Translate(typeof(NTAccount)) as NTAccount; + if (account != null) + { + return account.Value; + } + + return EveryoneAccountName; + } + catch (Exception) + { + return EveryoneAccountName; + } + } + } +} \ No newline at end of file diff --git a/src/Nancy.Hosting.Self/nancy.hosting.self.nuspec b/src/Nancy.Hosting.Self/nancy.hosting.self.nuspec new file mode 100644 index 0000000000..e6de7acd6b --- /dev/null +++ b/src/Nancy.Hosting.Self/nancy.hosting.self.nuspec @@ -0,0 +1,26 @@ + + + + Nancy.Hosting.Self + 0.0.0 + Andreas Håkansson, Steven Robbins and contributors + false + Enables hosting Nancy in any application. + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + en-US + Andreas Håkansson, Steven Robbins and contributors + http://nancyfx.org/nancy-nuget.png + https://github.com/NancyFx/Nancy/blob/master/license.txt + http://nancyfx.org + + + + Nancy + + + + + + + + \ No newline at end of file diff --git a/src/Nancy.sln b/src/Nancy.sln index a10fe748de..c207c992ff 100644 --- a/src/Nancy.sln +++ b/src/Nancy.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Hosting.Aspnet", "Nan EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Authentication.Forms", "Nancy.Authentication.Forms\Nancy.Authentication.Forms.csproj", "{E8B18958-7C8A-4FBA-AF00-3041C34A20CE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Hosting.Self", "Nancy.Hosting.Self\Nancy.Hosting.Self.csproj", "{AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Testing", "Nancy.Testing\Nancy.Testing.csproj", "{D79203C0-B672-4751-9C95-C3AB7D3FEFBE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.ViewEngines.DotLiquid", "Nancy.ViewEngines.DotLiquid\Nancy.ViewEngines.DotLiquid.csproj", "{B795886D-9D70-45B1-BFB5-AD54CBC9A447}" @@ -53,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Bootstrapping.As EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Caching", "Nancy.Demo.Caching\Nancy.Demo.Caching.csproj", "{28F9EA8B-90F7-4974-BB40-0B7FA9309D02}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Hosting.Self", "Nancy.Demo.Hosting.Self\Nancy.Demo.Hosting.Self.csproj", "{0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Hosting.Aspnet", "Nancy.Demo.Hosting.Aspnet\Nancy.Demo.Hosting.Aspnet.csproj", "{E127FED3-01C0-41BA-BF83-D8DCDD827D6A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Authentication.Forms.TestingDemo", "Nancy.Demo.Authentication.Forms.TestingDemo\Nancy.Demo.Authentication.Forms.TestingDemo.csproj", "{948A8EF6-D50C-45EA-9AFD-7A4723ADAB0B}" @@ -81,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.ViewEngines.Razor.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.ViewEngines.Razor.BuildProviders", "Nancy.ViewEngines.Razor.BuildProviders\Nancy.ViewEngines.Razor.BuildProviders.csproj", "{EDF3E264-2D0F-4440-99FF-45D279A598A9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Hosting.Self.Tests", "Nancy.Hosting.Self.Tests\Nancy.Hosting.Self.Tests.csproj", "{CA24ED85-DD68-4C10-B80A-D81C6745FCB8}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Authentication.Stateless", "Nancy.Authentication.Stateless\Nancy.Authentication.Stateless.csproj", "{211560C3-FDDF-46D6-AB0C-F3BC04B173B5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Authentication.Stateless.Website", "Nancy.Demo.Authentication.Stateless.Website\Nancy.Demo.Authentication.Stateless.Website.csproj", "{B5E3586D-81DE-49C3-83BC-062684795127}" @@ -189,6 +195,18 @@ Global {E8B18958-7C8A-4FBA-AF00-3041C34A20CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8B18958-7C8A-4FBA-AF00-3041C34A20CE}.Release|Any CPU.Build.0 = Release|Any CPU {E8B18958-7C8A-4FBA-AF00-3041C34A20CE}.Release|x86.ActiveCfg = Release|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.MonoDebug|Any CPU.ActiveCfg = MonoDebug|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.MonoDebug|Any CPU.Build.0 = MonoDebug|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.MonoDebug|x86.ActiveCfg = MonoDebug|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.MonoRelease|Any CPU.ActiveCfg = MonoRelease|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.MonoRelease|Any CPU.Build.0 = MonoRelease|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.MonoRelease|x86.ActiveCfg = MonoRelease|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.Release|Any CPU.Build.0 = Release|Any CPU + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134}.Release|x86.ActiveCfg = Release|Any CPU {D79203C0-B672-4751-9C95-C3AB7D3FEFBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D79203C0-B672-4751-9C95-C3AB7D3FEFBE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D79203C0-B672-4751-9C95-C3AB7D3FEFBE}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -379,6 +397,22 @@ Global {28F9EA8B-90F7-4974-BB40-0B7FA9309D02}.Release|Any CPU.ActiveCfg = Release|Any CPU {28F9EA8B-90F7-4974-BB40-0B7FA9309D02}.Release|Any CPU.Build.0 = Release|Any CPU {28F9EA8B-90F7-4974-BB40-0B7FA9309D02}.Release|x86.ActiveCfg = Release|Any CPU + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.Debug|Any CPU.ActiveCfg = Debug|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.Debug|Any CPU.Build.0 = Debug|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.Debug|x86.ActiveCfg = Debug|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.Debug|x86.Build.0 = Debug|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.MonoDebug|Any CPU.ActiveCfg = MonoDebug|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.MonoDebug|Any CPU.Build.0 = MonoDebug|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.MonoDebug|x86.ActiveCfg = MonoDebug|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.MonoDebug|x86.Build.0 = MonoDebug|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.MonoRelease|Any CPU.ActiveCfg = MonoRelease|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.MonoRelease|Any CPU.Build.0 = MonoRelease|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.MonoRelease|x86.ActiveCfg = MonoRelease|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.MonoRelease|x86.Build.0 = MonoRelease|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.Release|Any CPU.ActiveCfg = Release|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.Release|Any CPU.Build.0 = Release|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.Release|x86.ActiveCfg = Release|x86 + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1}.Release|x86.Build.0 = Release|x86 {E127FED3-01C0-41BA-BF83-D8DCDD827D6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E127FED3-01C0-41BA-BF83-D8DCDD827D6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {E127FED3-01C0-41BA-BF83-D8DCDD827D6A}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -547,6 +581,18 @@ Global {EDF3E264-2D0F-4440-99FF-45D279A598A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDF3E264-2D0F-4440-99FF-45D279A598A9}.Release|Any CPU.Build.0 = Release|Any CPU {EDF3E264-2D0F-4440-99FF-45D279A598A9}.Release|x86.ActiveCfg = Release|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.MonoDebug|Any CPU.ActiveCfg = MonoDebug|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.MonoDebug|Any CPU.Build.0 = MonoDebug|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.MonoDebug|x86.ActiveCfg = Debug|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.MonoRelease|Any CPU.ActiveCfg = MonoRelease|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.MonoRelease|Any CPU.Build.0 = MonoRelease|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.MonoRelease|x86.ActiveCfg = Release|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.Release|Any CPU.Build.0 = Release|Any CPU + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8}.Release|x86.ActiveCfg = Release|Any CPU {211560C3-FDDF-46D6-AB0C-F3BC04B173B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {211560C3-FDDF-46D6-AB0C-F3BC04B173B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {211560C3-FDDF-46D6-AB0C-F3BC04B173B5}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -773,6 +819,7 @@ Global {4B7E35DF-1569-4346-B180-A09615723095} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} {15B7F794-0BB2-4B66-AD78-4A951F1209B2} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} {E8B18958-7C8A-4FBA-AF00-3041C34A20CE} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} + {AA7F66EB-EC2C-47DE-855F-30B3E6EF2134} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} {D79203C0-B672-4751-9C95-C3AB7D3FEFBE} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} {B795886D-9D70-45B1-BFB5-AD54CBC9A447} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} {BD72B98D-C81A-4013-B606-94B4BA2273E5} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} @@ -789,6 +836,7 @@ Global {98940A30-1B48-4F71-A6BA-85F0AAF31A2F} = {4A24657F-9695-437B-9702-2541ED280628} {EF660223-4DFD-4E36-BF36-9DD6AFB3F837} = {4A24657F-9695-437B-9702-2541ED280628} {28F9EA8B-90F7-4974-BB40-0B7FA9309D02} = {4A24657F-9695-437B-9702-2541ED280628} + {0B3EA40E-F7D8-4E14-A30F-1536F41B62D1} = {4A24657F-9695-437B-9702-2541ED280628} {E127FED3-01C0-41BA-BF83-D8DCDD827D6A} = {4A24657F-9695-437B-9702-2541ED280628} {948A8EF6-D50C-45EA-9AFD-7A4723ADAB0B} = {4A24657F-9695-437B-9702-2541ED280628} {1258BFCD-3BAD-4373-B786-4D698EC3C157} = {4A24657F-9695-437B-9702-2541ED280628} @@ -803,6 +851,7 @@ Global {FBC35268-377B-4DBE-87E3-B22D1314BEC6} = {A427F9F8-0A6F-4EEA-837F-FCDAB6E7D4B3} {3F18F5DA-93E0-4513-8BF4-BC8EE5C4117C} = {A427F9F8-0A6F-4EEA-837F-FCDAB6E7D4B3} {EDF3E264-2D0F-4440-99FF-45D279A598A9} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} + {CA24ED85-DD68-4C10-B80A-D81C6745FCB8} = {A427F9F8-0A6F-4EEA-837F-FCDAB6E7D4B3} {211560C3-FDDF-46D6-AB0C-F3BC04B173B5} = {E944109B-0B7A-4ADE-8602-004CEFA5897D} {B5E3586D-81DE-49C3-83BC-062684795127} = {4A24657F-9695-437B-9702-2541ED280628} {BAE74CD5-57C2-40E3-8F7A-EDE5721C2ACC} = {4A24657F-9695-437B-9702-2541ED280628}