diff --git a/Kudu.SiteManagement.Test/Certificates/CertificateLookupFacts.cs b/Kudu.SiteManagement.Test/Certificates/CertificateLookupFacts.cs new file mode 100644 index 000000000..e803a657c --- /dev/null +++ b/Kudu.SiteManagement.Test/Certificates/CertificateLookupFacts.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using Kudu.SiteManagement.Certificates; +using Kudu.SiteManagement.Certificates.Wrappers; +using Kudu.SiteManagement.Test.Certificates.Fakes; +using Moq; +using Xunit; + +namespace Kudu.SiteManagement.Test.Certificates +{ + public class CertificateLookupFacts + { + [Fact] + public void ByThumbprint_OneMatching_ReturnsCertificate() + { + Mock storeMock = new Mock(); + IX509Certificate2Collection collectionFake = new X509Certificate2CollectionFake + { + new X509Certificate2Fake(), + new X509Certificate2Fake(), + new X509Certificate2Fake(friendlyName: "FindMe", thumbprint: "FindMe") + }; + storeMock.Setup(mock => mock.Certificates).Returns(collectionFake); + + Certificate result = new CertificateLookup("FindMe", new[] { StoreName.My }, name => storeMock.Object) + .ByThumbprint(); + + Assert.Equal(result.FriendlyName, "FindMe"); + Assert.Equal(result.Thumbprint, "FindMe"); + } + + [Fact] + public void ByFriendlyName_OneMatching_ReturnsCertificate() + { + Mock storeMock = new Mock(); + storeMock.Setup(mock => mock.Certificates).Returns(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(), + new X509Certificate2Fake(), + new X509Certificate2Fake(friendlyName: "FindMe", thumbprint: "FindMe") + }); + + Certificate result = new CertificateLookup("FindMe", new[] { StoreName.My }, name => storeMock.Object) + .ByFriendlyName(); + + Assert.Equal(result.FriendlyName, "FindMe"); + Assert.Equal(result.Thumbprint, "FindMe"); + } + + [Fact] + public void ByThumbprint_NoneMatching_ReturnsNull() + { + Mock storeMock = new Mock(); + IX509Certificate2Collection collectionFake = new X509Certificate2CollectionFake { new X509Certificate2Fake() }; + storeMock.Setup(mock => mock.Certificates).Returns(collectionFake); + + Certificate result = new CertificateLookup("FindMe", new[] { StoreName.My }, name => storeMock.Object) + .ByThumbprint(); + + Assert.Null(result); + } + + [Fact] + public void ByFriendlyName_NoneMatching_ReturnsNull() + { + Mock storeMock = new Mock(); + storeMock.Setup(mock => mock.Certificates).Returns(new X509Certificate2CollectionFake { new X509Certificate2Fake() }); + + Certificate result = new CertificateLookup("FindMe", new[] { StoreName.My }, name => storeMock.Object) + .ByFriendlyName(); + + Assert.Null(result); + } + + [Fact] + public void ByThumbprint_MultipleMatching_ReturnsFirstMatchingCertificate() + { + Mock storeMock = new Mock(); + storeMock.Setup(mock => mock.Certificates).Returns(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(), + new X509Certificate2Fake(friendlyName: "FindMe", thumbprint: "FindMe"), + new X509Certificate2Fake(friendlyName: "NotMe", thumbprint: "FindMe") + }); + + Certificate result = new CertificateLookup("FindMe", new[] { StoreName.My }, name => storeMock.Object) + .ByThumbprint(); + + Assert.Equal(result.FriendlyName, "FindMe"); + Assert.Equal(result.Thumbprint, "FindMe"); + } + + [Fact] + public void ByFriendlyName_MultipleMatching_ReturnsFirstMatchingCertificate() + { + Mock storeMock = new Mock(); + storeMock.Setup(mock => mock.Certificates).Returns(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(), + new X509Certificate2Fake(friendlyName: "FindMe", thumbprint: "FindMe"), + new X509Certificate2Fake(friendlyName: "FindMe", thumbprint: "NotMe") + }); + + Certificate result = new CertificateLookup("FindMe", new[] { StoreName.My }, name => storeMock.Object) + .ByFriendlyName(); + + Assert.Equal(result.FriendlyName, "FindMe"); + Assert.Equal(result.Thumbprint, "FindMe"); + } + + [Fact] + public void ByThumbprint_OneMatchingInSecondaryStore_ReturnsCertificate() + { + Dictionary> storeMocks = new Dictionary>(); + storeMocks[StoreName.My] = CreateX509StoreMock(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(), + new X509Certificate2Fake() + }); + storeMocks[StoreName.Root] = CreateX509StoreMock(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(), + new X509Certificate2Fake(friendlyName: "FindMe", thumbprint: "FindMe") + }); + + Certificate result = new CertificateLookup("FindMe", new[] { StoreName.My, StoreName.Root }, name => storeMocks[name].Object) + .ByThumbprint(); + + Assert.Equal(result.FriendlyName, "FindMe"); + Assert.Equal(result.Thumbprint, "FindMe"); + } + + [Fact] + public void ByFriendlyName_OneMatchingInSecondaryStore_ReturnsCertificate() + { + Dictionary> storeMocks = new Dictionary>(); + storeMocks[StoreName.My] = CreateX509StoreMock(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(), + new X509Certificate2Fake() + }); + storeMocks[StoreName.Root] = CreateX509StoreMock(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(), + new X509Certificate2Fake(friendlyName: "FindMe", thumbprint: "FindMe") + }); + + Certificate result = new CertificateLookup("FindMe", new[] { StoreName.My, StoreName.Root }, name => storeMocks[name].Object) + .ByFriendlyName(); + + Assert.Equal(result.FriendlyName, "FindMe"); + Assert.Equal(result.Thumbprint, "FindMe"); + } + + private static Mock CreateX509StoreMock(IX509Certificate2Collection fake) + { + Mock mock = new Mock(); + mock.Setup(m => m.Certificates).Returns(fake); + return mock; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Certificates/CertificateSearcherFacts.cs b/Kudu.SiteManagement.Test/Certificates/CertificateSearcherFacts.cs new file mode 100644 index 000000000..53731d765 --- /dev/null +++ b/Kudu.SiteManagement.Test/Certificates/CertificateSearcherFacts.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Kudu.SiteManagement.Certificates; +using Kudu.SiteManagement.Certificates.Wrappers; +using Kudu.SiteManagement.Configuration; +using Kudu.SiteManagement.Test.Certificates.Fakes; +using Moq; +using Xunit; + +namespace Kudu.SiteManagement.Test.Certificates +{ + public class CertificateSearcherFacts + { + [Fact] + public void FindAll_MultipleStores_ReturnsFromAllStores() + { + Mock configMock = new Mock(); + configMock.Setup(mock => mock.CertificateStores) + .Returns(new[] { new CertificateStoreConfiguration(StoreName.My), new CertificateStoreConfiguration(StoreName.Root) }); + + Dictionary> storeMocks = new Dictionary>(); + storeMocks[StoreName.My] = CreateX509StoreMock(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(friendlyName: "My_CertA"), + new X509Certificate2Fake(friendlyName: "My_CertB") + }); + storeMocks[StoreName.Root] = CreateX509StoreMock(new X509Certificate2CollectionFake + { + new X509Certificate2Fake(friendlyName: "Root_CertA"), + new X509Certificate2Fake(friendlyName: "Root_CertB") + }); + + ICertificateSearcher searcher = new CertificateSearcher(configMock.Object, name => storeMocks[name].Object); + Dictionary all = searcher.FindAll().ToDictionary(c => c.FriendlyName); + + Assert.Equal(all.Count, 4); + Assert.NotNull(all["My_CertA"]); + Assert.NotNull(all["My_CertB"]); + Assert.NotNull(all["Root_CertA"]); + Assert.NotNull(all["Root_CertB"]); + } + + [Fact] + public void Lookup_ReturnsCertificateLookupObject() + { + Mock configMock = new Mock(); + configMock.Setup(mock => mock.CertificateStores).Returns(new[] { new CertificateStoreConfiguration(StoreName.My) }); + + ICertificateSearcher searcher = new CertificateSearcher(configMock.Object, null); + ICertificateLookup result = searcher.Lookup("FindMe"); + + Assert.IsType(result); + } + + private static Mock CreateX509StoreMock(IX509Certificate2Collection fake) + { + Mock mock = new Mock(); + mock.Setup(m => m.Certificates).Returns(fake); + return mock; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Certificates/Fakes/X509Certificate2CollectionFake.cs b/Kudu.SiteManagement.Test/Certificates/Fakes/X509Certificate2CollectionFake.cs new file mode 100644 index 000000000..0793bd605 --- /dev/null +++ b/Kudu.SiteManagement.Test/Certificates/Fakes/X509Certificate2CollectionFake.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Kudu.SiteManagement.Certificates.Wrappers; + +namespace Kudu.SiteManagement.Test.Certificates.Fakes +{ + //Note: Simple Mock class instead of having to mock the interface, since IX509Certificate2 essentially is + // a "data collection" and not a Service, this can sometimes be easier this way. + public class X509Certificate2CollectionFake : List, IX509Certificate2Collection + { + public X509Certificate2CollectionFake() + { + } + + public X509Certificate2CollectionFake(IEnumerable collection) + : base(collection) + { + } + + public IX509Certificate2Collection Find(X509FindType findType, object findValue, bool validOnly) + { + switch (findType) + { + case X509FindType.FindByThumbprint: + return new X509Certificate2CollectionFake(this.Where(c => c.Thumbprint == (string)findValue)); + case X509FindType.FindBySubjectName: + case X509FindType.FindBySubjectDistinguishedName: + case X509FindType.FindByIssuerName: + case X509FindType.FindByIssuerDistinguishedName: + case X509FindType.FindBySerialNumber: + case X509FindType.FindByTimeValid: + case X509FindType.FindByTimeNotYetValid: + case X509FindType.FindByTimeExpired: + case X509FindType.FindByTemplateName: + case X509FindType.FindByApplicationPolicy: + case X509FindType.FindByCertificatePolicy: + case X509FindType.FindByExtension: + case X509FindType.FindByKeyUsage: + case X509FindType.FindBySubjectKeyIdentifier: + throw new NotImplementedException(); + default: + throw new ArgumentOutOfRangeException("findType"); + } + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Certificates/Fakes/X509Certificate2Fake.cs b/Kudu.SiteManagement.Test/Certificates/Fakes/X509Certificate2Fake.cs new file mode 100644 index 000000000..d18ef7f89 --- /dev/null +++ b/Kudu.SiteManagement.Test/Certificates/Fakes/X509Certificate2Fake.cs @@ -0,0 +1,26 @@ +using Kudu.SiteManagement.Certificates.Wrappers; + +namespace Kudu.SiteManagement.Test.Certificates.Fakes +{ + //Note: Simple Mock class instead of having to mock the interface, since IX509Certificate2 essentially is + // a "data object" and not a Service, this can sometimes be easier this way. + public class X509Certificate2Fake : IX509Certificate2 + { + private byte[] hash; + + public string Thumbprint { get; private set; } + public string FriendlyName { get; private set; } + + public X509Certificate2Fake(string friendlyName = "DummyFriendlyName", string thumbprint = "DummyThumbprint", byte[] hash = null) + { + Thumbprint = thumbprint; + FriendlyName = friendlyName; + this.hash = hash ?? new byte[0]; + } + + public byte[] GetCertHash() + { + return hash; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Configuration/Fakes/ApplicationBindingConfigurationElementFake.cs b/Kudu.SiteManagement.Test/Configuration/Fakes/ApplicationBindingConfigurationElementFake.cs new file mode 100644 index 000000000..2d2ed4605 --- /dev/null +++ b/Kudu.SiteManagement.Test/Configuration/Fakes/ApplicationBindingConfigurationElementFake.cs @@ -0,0 +1,13 @@ +using Kudu.SiteManagement.Configuration.Section.Bindings; + +namespace Kudu.SiteManagement.Test.Configuration.Fakes +{ + public class ApplicationBindingConfigurationElementFake : ApplicationBindingConfigurationElement + { + public ApplicationBindingConfigurationElementFake SetFake(string key, object value) + { + this[key] = value; + return this; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Configuration/Fakes/BindingsConfigurationElementCollectionFake.cs b/Kudu.SiteManagement.Test/Configuration/Fakes/BindingsConfigurationElementCollectionFake.cs new file mode 100644 index 000000000..d1f7f453f --- /dev/null +++ b/Kudu.SiteManagement.Test/Configuration/Fakes/BindingsConfigurationElementCollectionFake.cs @@ -0,0 +1,13 @@ +using Kudu.SiteManagement.Configuration.Section.Bindings; + +namespace Kudu.SiteManagement.Test.Configuration.Fakes +{ + public class BindingsConfigurationElementCollectionFake : BindingsConfigurationElementCollection + { + public BindingsConfigurationElementCollectionFake AddFake(BindingConfigurationElement element) + { + base.Add(element); + return this; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Configuration/Fakes/CertificateStoreConfigurationElementFake.cs b/Kudu.SiteManagement.Test/Configuration/Fakes/CertificateStoreConfigurationElementFake.cs new file mode 100644 index 000000000..88a8b8fe2 --- /dev/null +++ b/Kudu.SiteManagement.Test/Configuration/Fakes/CertificateStoreConfigurationElementFake.cs @@ -0,0 +1,13 @@ +using Kudu.SiteManagement.Configuration.Section.Cert; + +namespace Kudu.SiteManagement.Test.Configuration.Fakes +{ + public class CertificateStoreConfigurationElementFake : CertificateStoreConfigurationElement + { + public CertificateStoreConfigurationElementFake SetFake(string key, object value) + { + this[key] = value; + return this; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Configuration/Fakes/CertificateStoresConfigurationElementCollectionFake.cs b/Kudu.SiteManagement.Test/Configuration/Fakes/CertificateStoresConfigurationElementCollectionFake.cs new file mode 100644 index 000000000..b2dadfee8 --- /dev/null +++ b/Kudu.SiteManagement.Test/Configuration/Fakes/CertificateStoresConfigurationElementCollectionFake.cs @@ -0,0 +1,14 @@ +using Kudu.SiteManagement.Configuration.Section.Cert; + +namespace Kudu.SiteManagement.Test.Configuration.Fakes +{ + public class CertificateStoresConfigurationElementCollectionFake : CertificateStoresConfigurationElementCollection + { + public CertificateStoresConfigurationElementCollectionFake AddFake(CertificateStoreConfigurationElement element) + { + base.Add(element); + return this; + } + + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Configuration/Fakes/KuduConfigurationSectionFake.cs b/Kudu.SiteManagement.Test/Configuration/Fakes/KuduConfigurationSectionFake.cs new file mode 100644 index 000000000..c595c02c7 --- /dev/null +++ b/Kudu.SiteManagement.Test/Configuration/Fakes/KuduConfigurationSectionFake.cs @@ -0,0 +1,13 @@ +using Kudu.SiteManagement.Configuration.Section; + +namespace Kudu.SiteManagement.Test.Configuration.Fakes +{ + public class KuduConfigurationSectionFake : KuduConfigurationSection + { + public KuduConfigurationSectionFake SetFake(string key, object value) + { + this[key] = value; + return this; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Configuration/Fakes/PathConfigurationElementFake.cs b/Kudu.SiteManagement.Test/Configuration/Fakes/PathConfigurationElementFake.cs new file mode 100644 index 000000000..579ea8ce6 --- /dev/null +++ b/Kudu.SiteManagement.Test/Configuration/Fakes/PathConfigurationElementFake.cs @@ -0,0 +1,18 @@ +using Kudu.SiteManagement.Configuration.Section; + +namespace Kudu.SiteManagement.Test.Configuration.Fakes +{ + public class PathConfigurationElementFake : PathConfigurationElement + { + public static PathConfigurationElementFake Fake(string key, object value) + { + return new PathConfigurationElementFake().SetFake(key, value); + } + + public PathConfigurationElementFake SetFake(string key, object value) + { + this[key] = value; + return this; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Configuration/Fakes/ServiceBindingConfigurationElementFake.cs b/Kudu.SiteManagement.Test/Configuration/Fakes/ServiceBindingConfigurationElementFake.cs new file mode 100644 index 000000000..2bc298624 --- /dev/null +++ b/Kudu.SiteManagement.Test/Configuration/Fakes/ServiceBindingConfigurationElementFake.cs @@ -0,0 +1,13 @@ +using Kudu.SiteManagement.Configuration.Section.Bindings; + +namespace Kudu.SiteManagement.Test.Configuration.Fakes +{ + public class ServiceBindingConfigurationElementFake : ServiceBindingConfigurationElement + { + public ServiceBindingConfigurationElementFake SetFake(string key, object value) + { + this[key] = value; + return this; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Configuration/KuduConfigurationFacts.cs b/Kudu.SiteManagement.Test/Configuration/KuduConfigurationFacts.cs new file mode 100644 index 000000000..d2615179e --- /dev/null +++ b/Kudu.SiteManagement.Test/Configuration/KuduConfigurationFacts.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using Kudu.SiteManagement.Configuration; +using Kudu.SiteManagement.Configuration.Section; +using Kudu.SiteManagement.Configuration.Section.Cert; +using Kudu.SiteManagement.Test.Configuration.Fakes; +using Xunit; + +namespace Kudu.SiteManagement.Test.Configuration +{ + public class KuduConfigurationFacts + { + [Fact] + public void CustomHostNamesEnabled_NoConfiguration_DefaultsToFalse() + { + var appSettingsFake = new NameValueCollection(); + + IKuduConfiguration config = CreateConfiguration(null, appSettingsFake); + Assert.Equal(false, config.CustomHostNamesEnabled); + } + + [Fact] + public void CustomHostNamesEnabled_WithoutConfigurationSection_ReturnsLegacySetting() + { + var appSettingsFake = new NameValueCollection(); + appSettingsFake.Add("enableCustomHostNames", "true"); + + IKuduConfiguration config = CreateConfiguration(null, appSettingsFake); + Assert.Equal(true, config.CustomHostNamesEnabled); + } + + [Fact] + public void CustomHostNamesEnabled_WithoutConfigurationSectionInvalid_ReturnsFalse() + { + var appSettingsFake = new NameValueCollection(); + appSettingsFake.Add("enableCustomHostNames", "fubar"); + + IKuduConfiguration config = CreateConfiguration(null, appSettingsFake); + Assert.Equal(false, config.CustomHostNamesEnabled); + } + + [Fact] + public void CustomHostNamesEnabled_WithConfigurationSectionTrue_ReturnsTrue() + { + var configFake = new KuduConfigurationSectionFake(); + configFake.SetFake("enableCustomHostNames", true); + + IKuduConfiguration config = CreateConfiguration(configFake, new NameValueCollection()); + Assert.Equal(true, config.CustomHostNamesEnabled); + } + + [Fact] + public void ApplicationsPath_WithoutConfigurationSection_ReturnsCombinedPathUsingLegacySeeting() + { + var appSettingsFake = new NameValueCollection(); + appSettingsFake.Add("sitesPath", ".\\sitespath"); + + IKuduConfiguration config = CreateConfiguration(null, appSettingsFake); + string expected = Path.GetFullPath(".\\root_path\\sitespath"); + Assert.Equal(expected, config.ApplicationsPath); + } + + [Fact] + public void ApplicationsPath_WithConfigurationSection_ReturnsCombinedPath() + { + var configFake = new KuduConfigurationSectionFake(); + configFake.SetFake("applications", PathConfigurationElementFake.Fake("path", ".\\sitespath")); + + IKuduConfiguration config = CreateConfiguration(configFake, new NameValueCollection()); + string expected = Path.GetFullPath(".\\root_path\\sitespath"); + Assert.Equal(expected, config.ApplicationsPath); + } + + [Fact] + public void ServiceSitePath_WithoutConfigurationSection_ReturnsCombinedPathUsingLegacySeeting() + { + var appSettingsFake = new NameValueCollection(); + appSettingsFake.Add("serviceSitePath", ".\\servicepath"); + + IKuduConfiguration config = CreateConfiguration(null, appSettingsFake); + string expected = Path.GetFullPath(".\\root_path\\servicepath"); + Assert.Equal(expected, config.ServiceSitePath); + } + + [Fact] + public void ServiceSitePath_WithConfigurationSection_ReturnsSetting() + { + var configFake = new KuduConfigurationSectionFake(); + configFake.SetFake("serviceSite", PathConfigurationElementFake.Fake("path", ".\\servicepath")); + + IKuduConfiguration config = CreateConfiguration(configFake, new NameValueCollection()); + string expected = Path.GetFullPath(".\\root_path\\servicepath"); + Assert.Equal(expected, config.ServiceSitePath); + } + + [Fact] + public void Bindings_LegacyApplicationBinding_MapsToBindingConfiguration() + { + var appSettingsFake = new NameValueCollection(); + appSettingsFake.Add("urlBaseValue", "kudu.domain.com"); + + IKuduConfiguration config = CreateConfiguration(null, appSettingsFake); + Assert.Equal(1, config.Bindings.Count()); + Assert.Equal(UriScheme.Http, config.Bindings.First().Scheme); + Assert.Equal("kudu.domain.com", config.Bindings.First().Url); + Assert.Equal(SiteType.Live, config.Bindings.First().SiteType); + } + + [Fact] + public void Bindings_LegacyServiceBinding_MapsToBindingConfiguration() + { + var appSettingsFake = new NameValueCollection(); + appSettingsFake.Add("serviceUrlBaseValue", "kudu.svc.domain.com"); + + IKuduConfiguration config = CreateConfiguration(null, appSettingsFake); + Assert.Equal(1, config.Bindings.Count()); + Assert.Equal(UriScheme.Http, config.Bindings.First().Scheme); + Assert.Equal("kudu.svc.domain.com", config.Bindings.First().Url); + Assert.Equal(SiteType.Service, config.Bindings.First().SiteType); + } + + [Fact] + public void Bindings_SingleApplicationBinding_MapsToBindingConfiguration() + { + var configFake = new KuduConfigurationSectionFake(); + var bindingsFake = new BindingsConfigurationElementCollectionFake(); + bindingsFake.AddFake(new ApplicationBindingConfigurationElementFake() + .SetFake("scheme", UriScheme.Http) + .SetFake("url", "kudu.domain.com")); + + configFake.SetFake("bindings", bindingsFake); + + IKuduConfiguration config = CreateConfiguration(configFake, new NameValueCollection()); + Assert.Equal(1, config.Bindings.Count()); + Assert.Equal(UriScheme.Http, config.Bindings.First().Scheme); + Assert.Equal("kudu.domain.com", config.Bindings.First().Url); + Assert.Equal(SiteType.Live, config.Bindings.First().SiteType); + } + + [Fact] + public void Bindings_SingleServiceBinding_MapsToBindingConfiguration() + { + var configFake = new KuduConfigurationSectionFake(); + var bindingsFake = new BindingsConfigurationElementCollectionFake(); + bindingsFake.AddFake(new ServiceBindingConfigurationElementFake() + .SetFake("scheme", UriScheme.Http) + .SetFake("url", "kudu.svc.domain.com")); + + configFake.SetFake("bindings", bindingsFake); + + IKuduConfiguration config = CreateConfiguration(configFake, new NameValueCollection()); + Assert.Equal(1, config.Bindings.Count()); + Assert.Equal(UriScheme.Http, config.Bindings.First().Scheme); + Assert.Equal("kudu.svc.domain.com", config.Bindings.First().Url); + Assert.Equal(SiteType.Service, config.Bindings.First().SiteType); + } + + + [Fact] + public void CertificateStores_WithoutStoresConfiguration_DefaultsToSingleStoreMy() + { + IKuduConfiguration config = CreateConfiguration(null, new NameValueCollection()); + Assert.Equal(StoreName.My, config.CertificateStores.Single().Name); + } + + [Fact] + public void CertificateStores_WithEmptyConfigurationSection_DefaultsToSingleStoreMy() + { + var configFake = new KuduConfigurationSectionFake(); + var storesFake = new CertificateStoresConfigurationElementCollectionFake(); + configFake.SetFake("certificateStores", storesFake); + + IKuduConfiguration config = CreateConfiguration(configFake, new NameValueCollection()); + Assert.Equal(StoreName.My, config.CertificateStores.Single().Name); + } + + [Fact] + public void CertificateStores_WithSingleElement_ConstainsSingleItem() + { + var configFake = new KuduConfigurationSectionFake(); + var storesFake = new CertificateStoresConfigurationElementCollectionFake(); + storesFake.Add(new CertificateStoreConfigurationElementFake() + .SetFake("name", StoreName.Root)); + + configFake.SetFake("certificateStores", storesFake); + + IKuduConfiguration config = CreateConfiguration(configFake, new NameValueCollection()); + Assert.Equal(StoreName.Root, config.CertificateStores.Single().Name); + } + + [Fact] + public void CertificateStores_WithMultipleElements_ConstainsSingleItem() + { + var configFake = new KuduConfigurationSectionFake(); + var storesFake = new CertificateStoresConfigurationElementCollectionFake(); + storesFake.Add(new CertificateStoreConfigurationElementFake() + .SetFake("name", StoreName.Root)); + storesFake.Add(new CertificateStoreConfigurationElementFake() + .SetFake("name", StoreName.My)); + + configFake.SetFake("certificateStores", storesFake); + + IKuduConfiguration config = CreateConfiguration(configFake, new NameValueCollection()); + Assert.Equal(2, config.CertificateStores.Count()); + Assert.Equal(StoreName.Root, config.CertificateStores.ElementAt(0).Name); + Assert.Equal(StoreName.My, config.CertificateStores.ElementAt(1).Name); + } + + private IKuduConfiguration CreateConfiguration(KuduConfigurationSectionFake configFake, NameValueCollection appSettingsFake) + { + Type type = typeof(KuduConfiguration); + ConstructorInfo ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, + new[] { typeof(string), typeof(KuduConfigurationSection), typeof(NameValueCollection) }, null); + return (IKuduConfiguration)ctor.Invoke(new object[] { "root_path", configFake, appSettingsFake }); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/Kudu.SiteManagement.Test.csproj b/Kudu.SiteManagement.Test/Kudu.SiteManagement.Test.csproj new file mode 100644 index 000000000..5a8282466 --- /dev/null +++ b/Kudu.SiteManagement.Test/Kudu.SiteManagement.Test.csproj @@ -0,0 +1,99 @@ + + + + + Debug + AnyCPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6} + Library + Properties + Kudu.SiteManagement.Test + Kudu.SiteManagement.Test + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Moq.4.2.1312.1622\lib\net40\Moq.dll + + + + + + + ..\packages\Microsoft.Net.Http.2.2.18\lib\net45\System.Net.Http.Extensions.dll + + + ..\packages\Microsoft.Net.Http.2.2.18\lib\net45\System.Net.Http.Primitives.dll + + + + + + + + + ..\packages\xunit.1.9.2\lib\net20\xunit.dll + + + ..\packages\xunit.extensions.1.9.2\lib\net20\xunit.extensions.dll + + + + + + + + + + + + + + + + + + + + + + + + + {D5669C1D-3408-4CEE-8C1B-D86D03D27EE2} + Kudu.SiteManagement + + + + + + + + + + + \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/PathResolverFacts.cs b/Kudu.SiteManagement.Test/PathResolverFacts.cs new file mode 100644 index 000000000..11c9333e0 --- /dev/null +++ b/Kudu.SiteManagement.Test/PathResolverFacts.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Kudu.SiteManagement.Configuration; +using Moq; +using Xunit; + +namespace Kudu.SiteManagement.Test +{ + public class PathResolverFacts + { + [Fact] + public void GetApplicationPath_WithApplicationsPath_CombinesNameAndPath() + { + Mock mock = new Mock(); + mock.Setup(x => x.ApplicationsPath).Returns("C:\\Dummy\\Path"); + + IPathResolver resolver = new PathResolver(mock.Object); + + Assert.Equal(resolver.GetApplicationPath("Foo"), "C:\\Dummy\\Path\\Foo"); + } + + [Fact] + public void GetLiveSitePath_WithApplicationsPath_CombinesNameAndPathWithSiteAdded() + { + Mock mock = new Mock(); + mock.Setup(x => x.ApplicationsPath).Returns("C:\\Dummy\\Path"); + + IPathResolver resolver = new PathResolver(mock.Object); + + Assert.Equal(resolver.GetLiveSitePath("Foo"), "C:\\Dummy\\Path\\Foo\\site"); + } + } +} diff --git a/Kudu.SiteManagement.Test/Properties/AssemblyInfo.cs b/Kudu.SiteManagement.Test/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..a8d1abf22 --- /dev/null +++ b/Kudu.SiteManagement.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Kudu.SiteManagement.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Kudu.SiteManagement.Test")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f3845f07-6bf6-4e81-89c9-36517a8d07fe")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Kudu.SiteManagement.Test/app.config b/Kudu.SiteManagement.Test/app.config new file mode 100644 index 000000000..5c36deece --- /dev/null +++ b/Kudu.SiteManagement.Test/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Kudu.SiteManagement.Test/packages.config b/Kudu.SiteManagement.Test/packages.config new file mode 100644 index 000000000..83cefe2ca --- /dev/null +++ b/Kudu.SiteManagement.Test/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/Certificate.cs b/Kudu.SiteManagement/Certificates/Certificate.cs new file mode 100644 index 000000000..e7ab0bbeb --- /dev/null +++ b/Kudu.SiteManagement/Certificates/Certificate.cs @@ -0,0 +1,25 @@ +using System.Security.Cryptography.X509Certificates; +using Kudu.SiteManagement.Certificates.Wrappers; + +namespace Kudu.SiteManagement.Certificates +{ + public sealed class Certificate + { + private readonly IX509Certificate2 _certificate; + + public string StoreName { get; private set; } + public string FriendlyName { get { return _certificate.FriendlyName; } } + public string Thumbprint { get { return _certificate.Thumbprint; } } + + public Certificate(IX509Certificate2 certificate, string storeName) + { + _certificate = certificate; + StoreName = storeName; + } + + public byte[] GetCertHash() + { + return _certificate.GetCertHash(); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/CertificateLookup.cs b/Kudu.SiteManagement/Certificates/CertificateLookup.cs new file mode 100644 index 000000000..bdabcc7aa --- /dev/null +++ b/Kudu.SiteManagement/Certificates/CertificateLookup.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Kudu.SiteManagement.Certificates.Wrappers; + +namespace Kudu.SiteManagement.Certificates +{ + public interface ICertificateLookup + { + Certificate ByFriendlyName(); + Certificate ByThumbprint(); + } + + public sealed class CertificateLookup : ICertificateLookup + { + private readonly string _value; + private readonly IEnumerable _stores; + private readonly Func _storeFactory; + + public CertificateLookup(string value, IEnumerable stores, Func storeFactory) + { + _value = value; + _stores = stores; + _storeFactory = storeFactory; + } + + public Certificate ByFriendlyName() + { + foreach (IX509Store store in _stores.Select(storeName => _storeFactory(storeName))) + { + try + { + store.Open(OpenFlags.ReadOnly); + IX509Certificate2 certificate = store + .Certificates + .FirstOrDefault(cert => cert.FriendlyName.Equals(_value, StringComparison.OrdinalIgnoreCase)); + + if (certificate != null) + { + return new Certificate(certificate, store.Name); + } + } + finally + { + store.Close(); + } + } + return null; + } + + public Certificate ByThumbprint() + { + foreach (IX509Store store in _stores.Select(storeName => _storeFactory(storeName))) + { + try + { + store.Open(OpenFlags.ReadOnly); + IX509Certificate2 certificate = store.Certificates + .Find(X509FindType.FindByThumbprint, _value, false) + .FirstOrDefault(); + + if (certificate != null) + { + return new Certificate(certificate, store.Name); + } + } + finally + { + store.Close(); + } + } + return null; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/CertificateSearcher.cs b/Kudu.SiteManagement/Certificates/CertificateSearcher.cs new file mode 100644 index 000000000..de7d7b0d2 --- /dev/null +++ b/Kudu.SiteManagement/Certificates/CertificateSearcher.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Kudu.SiteManagement.Certificates.Wrappers; +using Kudu.SiteManagement.Configuration; + +namespace Kudu.SiteManagement.Certificates +{ + public class CertificateSearcher : ICertificateSearcher + { + private readonly IKuduConfiguration _configuration; + private readonly Func _storeFactory; + + public CertificateSearcher(IKuduConfiguration configuration) + : this(configuration, (name) => new X509StoreWrapper(new X509Store(name, StoreLocation.LocalMachine))) + { + } + + //Note: Constructor mainly here for testing + public CertificateSearcher(IKuduConfiguration configuration, Func storeFactory) + { + _configuration = configuration; + _storeFactory = storeFactory; + } + + public ICertificateLookup Lookup(string value) + { + return new CertificateLookup(value, _configuration.CertificateStores.Select(store => store.Name), _storeFactory); + } + + public IEnumerable FindAll() + { + return _configuration + .CertificateStores + .SelectMany(storeCfg => + { + IX509Store store = _storeFactory(storeCfg.Name); + store.Open(OpenFlags.ReadOnly); + try + { + return store + .Certificates + .Select(cert => new Certificate(cert, store.Name)) + .ToList(); + } + finally + { + store.Close(); + } + }); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/ICertificateSearcher.cs b/Kudu.SiteManagement/Certificates/ICertificateSearcher.cs new file mode 100644 index 000000000..53dc1faf5 --- /dev/null +++ b/Kudu.SiteManagement/Certificates/ICertificateSearcher.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Kudu.SiteManagement.Certificates +{ + public interface ICertificateSearcher + { + ICertificateLookup Lookup(string value); + + IEnumerable FindAll(); + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/Wrappers/IX509Certificate2.cs b/Kudu.SiteManagement/Certificates/Wrappers/IX509Certificate2.cs new file mode 100644 index 000000000..4380b44e4 --- /dev/null +++ b/Kudu.SiteManagement/Certificates/Wrappers/IX509Certificate2.cs @@ -0,0 +1,11 @@ +namespace Kudu.SiteManagement.Certificates.Wrappers +{ + //Note: Wrapper intention is to facilitate mocking. + // This class has mostly been generated with resharper. + public interface IX509Certificate2 + { + string FriendlyName { get; } + string Thumbprint { get; } + byte[] GetCertHash(); + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/Wrappers/IX509Certificate2Collection.cs b/Kudu.SiteManagement/Certificates/Wrappers/IX509Certificate2Collection.cs new file mode 100644 index 000000000..16a885769 --- /dev/null +++ b/Kudu.SiteManagement/Certificates/Wrappers/IX509Certificate2Collection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace Kudu.SiteManagement.Certificates.Wrappers +{ + //Note: Wrapper intention is to facilitate mocking. + // This class has mostly been generated with resharper. + public interface IX509Certificate2Collection : IEnumerable + { + IX509Certificate2Collection Find(X509FindType findType, object findValue, bool validOnly); + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/Wrappers/IX509Store.cs b/Kudu.SiteManagement/Certificates/Wrappers/IX509Store.cs new file mode 100644 index 000000000..3b597060d --- /dev/null +++ b/Kudu.SiteManagement/Certificates/Wrappers/IX509Store.cs @@ -0,0 +1,18 @@ +using System.Security.Cryptography.X509Certificates; + +namespace Kudu.SiteManagement.Certificates.Wrappers +{ + //Note: Wrapper intention is to facilitate mocking. + // This class has mostly been generated with resharper. + public interface IX509Store + { + string Name { get; } + IX509Certificate2Collection Certificates { get; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "flags", + Justification = "The name 'flags' originate from the X509Store class, as this is meant to be an interface for a wrapper, " + + "we wan't to keep the names intact.")] + void Open(OpenFlags flags); + void Close(); + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/Wrappers/X509Certificate2CollectionWrapper.cs b/Kudu.SiteManagement/Certificates/Wrappers/X509Certificate2CollectionWrapper.cs new file mode 100644 index 000000000..f932c69ad --- /dev/null +++ b/Kudu.SiteManagement/Certificates/Wrappers/X509Certificate2CollectionWrapper.cs @@ -0,0 +1,39 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace Kudu.SiteManagement.Certificates.Wrappers +{ + //Note: Wrapper intention is to facilitate mocking. + // This class has mostly been generated with resharper. + public sealed class X509Certificate2CollectionWrapper : IX509Certificate2Collection + { + private readonly X509Certificate2Collection _collection; + + public X509Certificate2CollectionWrapper(X509Certificate2Collection collection) + { + _collection = collection; + } + + public IX509Certificate2Collection Find(X509FindType findType, object findValue, bool validOnly) + { + return new X509Certificate2CollectionWrapper(_collection.Find(findType, findValue, validOnly)); + } + + public IEnumerator GetEnumerator() + { + + + return _collection + .OfType() + .Select(cert => new X509Certificate2Wrapper(cert)) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/Wrappers/X509Certificate2Wrapper.cs b/Kudu.SiteManagement/Certificates/Wrappers/X509Certificate2Wrapper.cs new file mode 100644 index 000000000..c0152ec61 --- /dev/null +++ b/Kudu.SiteManagement/Certificates/Wrappers/X509Certificate2Wrapper.cs @@ -0,0 +1,31 @@ +using System.Security.Cryptography.X509Certificates; + +namespace Kudu.SiteManagement.Certificates.Wrappers +{ + //Note: Wrapper intention is to facilitate mocking. + // This class has mostly been generated with resharper. + public class X509Certificate2Wrapper : IX509Certificate2 + { + private readonly X509Certificate2 _cert; + + public string FriendlyName + { + get { return _cert.FriendlyName; } + } + + public string Thumbprint + { + get { return _cert.Thumbprint; } + } + + public X509Certificate2Wrapper(X509Certificate2 cert) + { + _cert = cert; + } + + public byte[] GetCertHash() + { + return _cert.GetCertHash(); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Certificates/Wrappers/X509StoreWrapper.cs b/Kudu.SiteManagement/Certificates/Wrappers/X509StoreWrapper.cs new file mode 100644 index 000000000..9e4acf385 --- /dev/null +++ b/Kudu.SiteManagement/Certificates/Wrappers/X509StoreWrapper.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace Kudu.SiteManagement.Certificates.Wrappers +{ + //Note: Wrapper intention is to facilitate mocking. + // This class has mostly been generated with resharper. + public sealed class X509StoreWrapper : IX509Store + { + private readonly X509Store _store; + + public string Name + { + get { return _store.Name; } + } + + public X509StoreWrapper(X509Store store) + { + _store = store; + } + + public void Open(OpenFlags flags) + { + _store.Open(flags); + } + + public void Close() + { + _store.Close(); + } + + public IX509Certificate2Collection Certificates + { + get { return new X509Certificate2CollectionWrapper(_store.Certificates); } + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/IBindingConfiguration.cs b/Kudu.SiteManagement/Configuration/IBindingConfiguration.cs new file mode 100644 index 000000000..954483c1b --- /dev/null +++ b/Kudu.SiteManagement/Configuration/IBindingConfiguration.cs @@ -0,0 +1,43 @@ +using Kudu.SiteManagement.Configuration.Section; +using Kudu.SiteManagement.Configuration.Section.Bindings; + +namespace Kudu.SiteManagement.Configuration +{ + public interface IBindingConfiguration + { + string Url { get; } + UriScheme Scheme { get; } + SiteType SiteType { get; } + string Certificate { get; } + bool RequireSni { get; } + } + + public class BindingConfiguration : IBindingConfiguration + { + public string Url { get; private set; } + public UriScheme Scheme { get; private set; } + public SiteType SiteType { get; private set; } + public string Certificate { get; private set; } + public bool RequireSni { get; private set; } + + public BindingConfiguration(BindingConfigurationElement binding) + : this(binding.Url, binding.Scheme, binding.SiteType, binding.Certificate, binding.RequireSni) + { + } + + public BindingConfiguration(string url, UriScheme scheme, SiteType siteType) + : this(url, scheme, siteType, null, false) + { + + } + + public BindingConfiguration(string url, UriScheme scheme, SiteType siteType, string certificate, bool requireSni = false) + { + Url = url; + Scheme = scheme; + SiteType = siteType; + Certificate = certificate; + RequireSni = requireSni; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/ICertificateConfiguration.cs b/Kudu.SiteManagement/Configuration/ICertificateConfiguration.cs new file mode 100644 index 000000000..43496f011 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/ICertificateConfiguration.cs @@ -0,0 +1,26 @@ +using System.Security.Cryptography.X509Certificates; +using Kudu.SiteManagement.Configuration.Section; +using Kudu.SiteManagement.Configuration.Section.Cert; + +namespace Kudu.SiteManagement.Configuration +{ + public interface ICertificateStoreConfiguration + { + StoreName Name { get; } + } + + public class CertificateStoreConfiguration : ICertificateStoreConfiguration + { + public StoreName Name { get; private set; } + + public CertificateStoreConfiguration(CertificateStoreConfigurationElement store) + : this(store.Name) + { + } + + public CertificateStoreConfiguration(StoreName name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/IKuduConfiguration.cs b/Kudu.SiteManagement/Configuration/IKuduConfiguration.cs new file mode 100644 index 000000000..42a746b56 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/IKuduConfiguration.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Web; +using Kudu.SiteManagement.Configuration.Section; + +namespace Kudu.SiteManagement.Configuration +{ + public interface IKuduConfiguration + { + string RootPath { get; } + string ApplicationsPath { get; } + string ServiceSitePath { get; } + bool CustomHostNamesEnabled { get; } + + IEnumerable Bindings { get; } + IEnumerable CertificateStores { get; } + } + + public class KuduConfiguration : IKuduConfiguration + { + public static IKuduConfiguration Load(string root) + { + return new KuduConfiguration(root, ConfigurationManager.GetSection("kudu.management") as KuduConfigurationSection, ConfigurationManager.AppSettings); + } + + private readonly KuduConfigurationSection _section; + private readonly NameValueCollection _appSettings; + + public string RootPath { get; private set; } + + public bool CustomHostNamesEnabled + { + get + { + if (_section != null) + return _section.CustomHostNamesEnabled; + + bool value; + bool.TryParse(_appSettings["enableCustomHostNames"], out value); + //NOTE: try parse will default the bool to false if it fails. + // this will catch cases as null and invalid with the intented behavior. + return value; + } + } + + public string ServiceSitePath + { + get + { + //RFC: The original Path Resolver had this: + // that seems a bit odd? + // const string @default = @"%SystemDrive%\KuduService\wwwroot"; + // @default = Environment.ExpandEnvironmentVariables(@default); + // if(Directory.Exists(@default)) return @default; + // + // - Do we wan't to do that, basically ignoring configuration?... + + return _section == null + ? PathRelativeToRoot(_appSettings["serviceSitePath"]) + : PathRelativeToRoot(_section.ServiceSite.Path); + } + } + + public string ApplicationsPath + { + get + { + return _section == null + ? PathRelativeToRoot(_appSettings["sitesPath"]) + : PathRelativeToRoot(_section.Applications.Path); + } + } + + public IEnumerable Bindings + { + get + { + if (_section == null || _section.Bindings == null) + return Enumerable.Empty() + .Union(LegacyBinding("urlBaseValue", SiteType.Live)) + .Union(LegacyBinding("serviceUrlBaseValue", SiteType.Service)); + + return _section.Bindings.Items.Select(binding => new BindingConfiguration(binding)); + } + } + + public IEnumerable CertificateStores + { + get + { + if (_section == null || _section.CertificateStores == null || !_section.CertificateStores.Items.Any()) + return new[] { new CertificateStoreConfiguration(StoreName.My) }; + + return _section.CertificateStores.Items.Select(store => new CertificateStoreConfiguration(store)); + } + } + + private KuduConfiguration(string root, KuduConfigurationSection section, NameValueCollection appSettings) + { + RootPath = root; + _section = section; + _appSettings = appSettings; + } + + private string PathRelativeToRoot(string path) + { + string combined = Path.Combine(RootPath, path); + return Path.GetFullPath(combined); + } + + private IEnumerable LegacyBinding(string key, SiteType type) + { + string legacyBinding = _appSettings[key]; + if (string.IsNullOrEmpty(legacyBinding)) + yield break; + yield return new BindingConfiguration(legacyBinding, UriScheme.Http, type, null); + } + } +} diff --git a/Kudu.SiteManagement/Configuration/Section/Bindings/ApplicationBindingConfigurationElement.cs b/Kudu.SiteManagement/Configuration/Section/Bindings/ApplicationBindingConfigurationElement.cs new file mode 100644 index 000000000..307c25c40 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/Bindings/ApplicationBindingConfigurationElement.cs @@ -0,0 +1,10 @@ +namespace Kudu.SiteManagement.Configuration.Section.Bindings +{ + public class ApplicationBindingConfigurationElement : BindingConfigurationElement + { + public ApplicationBindingConfigurationElement() + : base(SiteType.Live) + { + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/Bindings/BindingConfigurationElement.cs b/Kudu.SiteManagement/Configuration/Section/Bindings/BindingConfigurationElement.cs new file mode 100644 index 000000000..5904afe56 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/Bindings/BindingConfigurationElement.cs @@ -0,0 +1,42 @@ +using System.ComponentModel; +using System.Configuration; + +namespace Kudu.SiteManagement.Configuration.Section.Bindings +{ + public abstract class BindingConfigurationElement : NamedConfigurationElement + { + public SiteType SiteType { get; private set; } + + protected BindingConfigurationElement(SiteType siteType) + { + SiteType = siteType; + } + + //TODO: use [RegexStringValidator("...url pattern")] to validate + [ConfigurationProperty("url", IsRequired = true)] + public string Url + { + get { return (string)this["url"]; } + } + + [ConfigurationProperty("scheme", IsRequired = false, DefaultValue = UriScheme.Http)] + [TypeConverter(typeof(UriSchemeConverter))] + public UriScheme Scheme + { + get { return (UriScheme)this["scheme"]; } + } + + [ConfigurationProperty("certificate", IsRequired = false)] + public string Certificate + { + get { return (string)this["certificate"]; } + } + + [ConfigurationProperty("require-sni", IsRequired = false)] + public bool RequireSni + { + get { return (bool)this["require-sni"]; } + } + + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/Bindings/BindingsConfigurationElementCollection.cs b/Kudu.SiteManagement/Configuration/Section/Bindings/BindingsConfigurationElementCollection.cs new file mode 100644 index 000000000..204d71695 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/Bindings/BindingsConfigurationElementCollection.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Configuration; + +namespace Kudu.SiteManagement.Configuration.Section.Bindings +{ + public class BindingsConfigurationElementCollection : NamedElementCollection + { + protected override object GetElementKey(ConfigurationElement element) + { + BindingConfigurationElement binding = element as BindingConfigurationElement; + if(binding == null) + throw new ConfigurationErrorsException(); + + return binding.Scheme + "://" + binding.Url; + } + + protected override Type ResolveTypeName(string elementName) + { + if (elementName == "applicationBinding") + return typeof (ApplicationBindingConfigurationElement); + + if (elementName == "serviceBinding") + return typeof(ServiceBindingConfigurationElement); + + throw new ConfigurationErrorsException(); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/Bindings/ServiceBindingConfigurationElement.cs b/Kudu.SiteManagement/Configuration/Section/Bindings/ServiceBindingConfigurationElement.cs new file mode 100644 index 000000000..34dcdd907 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/Bindings/ServiceBindingConfigurationElement.cs @@ -0,0 +1,10 @@ +namespace Kudu.SiteManagement.Configuration.Section.Bindings +{ + public class ServiceBindingConfigurationElement : BindingConfigurationElement + { + public ServiceBindingConfigurationElement() + : base(SiteType.Service) + { + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/Cert/CertificateConfigurationElement.cs b/Kudu.SiteManagement/Configuration/Section/Cert/CertificateConfigurationElement.cs new file mode 100644 index 000000000..1056183ea --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/Cert/CertificateConfigurationElement.cs @@ -0,0 +1,20 @@ +using System.Configuration; + +namespace Kudu.SiteManagement.Configuration.Section.Cert +{ + //TODO: Quick and dirty, but might keep this to give as a reference list. + public class CertificateConfigurationElement : ConfigurationElement + { + [ConfigurationProperty("name", IsRequired = true)] + public string Name + { + get { return (string)this["name"]; } + } + + [ConfigurationProperty("store", IsRequired = false)] + public string Store + { + get { return (string)this["store"]; } + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/Cert/CertificateStoreConfigurationElement.cs b/Kudu.SiteManagement/Configuration/Section/Cert/CertificateStoreConfigurationElement.cs new file mode 100644 index 000000000..ab2342129 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/Cert/CertificateStoreConfigurationElement.cs @@ -0,0 +1,14 @@ +using System.Configuration; +using System.Security.Cryptography.X509Certificates; + +namespace Kudu.SiteManagement.Configuration.Section.Cert +{ + public class CertificateStoreConfigurationElement : NamedConfigurationElement + { + [ConfigurationProperty("name", IsRequired = true)] + public StoreName Name + { + get { return (StoreName)this["name"]; } + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/Cert/CertificateStoresConfigurationElementCollection.cs b/Kudu.SiteManagement/Configuration/Section/Cert/CertificateStoresConfigurationElementCollection.cs new file mode 100644 index 000000000..86bfa46f0 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/Cert/CertificateStoresConfigurationElementCollection.cs @@ -0,0 +1,25 @@ +using System; +using System.Configuration; + +namespace Kudu.SiteManagement.Configuration.Section.Cert +{ + public class CertificateStoresConfigurationElementCollection : NamedElementCollection + { + protected override object GetElementKey(ConfigurationElement element) + { + CertificateStoreConfigurationElement store = element as CertificateStoreConfigurationElement; + if (store == null) + throw new ConfigurationErrorsException(); + + return store.Name; + } + + protected override Type ResolveTypeName(string elementName) + { + if (elementName == "store" || elementName == "certificateStore") + return typeof(CertificateStoreConfigurationElement); + + throw new ConfigurationErrorsException(); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/KuduConfigurationSection.cs b/Kudu.SiteManagement/Configuration/Section/KuduConfigurationSection.cs new file mode 100644 index 000000000..e3883c795 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/KuduConfigurationSection.cs @@ -0,0 +1,39 @@ +using System.Configuration; +using Kudu.SiteManagement.Configuration.Section.Bindings; +using Kudu.SiteManagement.Configuration.Section.Cert; + +namespace Kudu.SiteManagement.Configuration.Section +{ + public class KuduConfigurationSection : ConfigurationSection + { + [ConfigurationProperty("enableCustomHostNames", IsRequired = false, DefaultValue = false)] + public bool CustomHostNamesEnabled + { + get { return (bool)this["enableCustomHostNames"]; } + } + + [ConfigurationProperty("serviceSite", IsRequired = true)] + public PathConfigurationElement ServiceSite + { + get { return this["serviceSite"] as PathConfigurationElement; } + } + + [ConfigurationProperty("applications", IsRequired = true)] + public PathConfigurationElement Applications + { + get { return this["applications"] as PathConfigurationElement; } + } + + [ConfigurationProperty("bindings", IsRequired = false)] + public BindingsConfigurationElementCollection Bindings + { + get { return this["bindings"] as BindingsConfigurationElementCollection; } + } + + [ConfigurationProperty("certificateStores", IsRequired = false)] + public CertificateStoresConfigurationElementCollection CertificateStores + { + get { return this["certificateStores"] as CertificateStoresConfigurationElementCollection; } + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/NamedConfigurationElement.cs b/Kudu.SiteManagement/Configuration/Section/NamedConfigurationElement.cs new file mode 100644 index 000000000..62d5d56fc --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/NamedConfigurationElement.cs @@ -0,0 +1,13 @@ +using System.Configuration; +using System.Xml; + +namespace Kudu.SiteManagement.Configuration.Section +{ + public abstract class NamedConfigurationElement : ConfigurationElement + { + public void DeserializeElement(XmlReader reader) + { + base.DeserializeElement(reader, false); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/NamedElementCollection.cs b/Kudu.SiteManagement/Configuration/Section/NamedElementCollection.cs new file mode 100644 index 000000000..fd5ef1546 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/NamedElementCollection.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Xml; + +namespace Kudu.SiteManagement.Configuration.Section +{ + public abstract class NamedElementCollection : ConfigurationElementCollection, ICollection where T : NamedConfigurationElement + { + public IEnumerable Items + { + get { return this; } + } + + protected override ConfigurationElement CreateNewElement() + { + //Note: We should hit OnDeserializeUnrecognizedElement instead with this type of configuration block. + throw new ConfigurationErrorsException(); + } + + protected override bool OnDeserializeUnrecognizedElement(string elementName, XmlReader reader) + { + Type elementType = ResolveTypeName(elementName); + if(elementType == null) + return base.OnDeserializeUnrecognizedElement(elementName, reader); + + NamedConfigurationElement element = Activator.CreateInstance(elementType) as NamedConfigurationElement; + if (element == null) + return base.OnDeserializeUnrecognizedElement(elementName, reader); + + element.DeserializeElement(reader); + BaseAdd(element, true); + return true; + } + + protected abstract Type ResolveTypeName(string elementName); + + #region IEnumerator / ICollection Implementation + + IEnumerator IEnumerable.GetEnumerator() + { + return this.OfType().GetEnumerator(); + } + + public void Add(T item) + { + BaseAdd(item); + } + + public void Clear() + { + BaseClear(); + } + + public bool Contains(T item) + { + return BaseIndexOf(item) != -1; + } + + public void CopyTo(T[] array, int arrayIndex) + { + CopyTo(array, arrayIndex); + } + + public bool Remove(T item) + { + if (BaseIndexOf(item) == -1) + return false; + + BaseRemove(item); + return true; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "Kudu.SiteManagement.Configuration.Section.NamedElementCollection`1.#System.Collections.Generic.ICollection`1.IsReadOnly", Justification = "This conflicts with the method IsReadOnly() of the base class which provides the same functionality.")] + bool ICollection.IsReadOnly + { + get + { + return base.IsReadOnly(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/PathConfigurationElement.cs b/Kudu.SiteManagement/Configuration/Section/PathConfigurationElement.cs new file mode 100644 index 000000000..c2c947646 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/PathConfigurationElement.cs @@ -0,0 +1,13 @@ +using System.Configuration; + +namespace Kudu.SiteManagement.Configuration.Section +{ + public class PathConfigurationElement : ConfigurationElement + { + [ConfigurationProperty("path", IsRequired = true)] + public string Path + { + get { return (string)this["path"]; } + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/UriSchemeConverter.cs b/Kudu.SiteManagement/Configuration/Section/UriSchemeConverter.cs new file mode 100644 index 000000000..7d4cff48e --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/UriSchemeConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel; +using System.Configuration; +using System.Globalization; + +namespace Kudu.SiteManagement.Configuration.Section +{ + public class UriSchemeConverter : ConfigurationConverterBase + { + public override object ConvertFrom(ITypeDescriptorContext ctx, CultureInfo ci, object data) + { + if(data == null) + return UriScheme.Http; + + UriScheme scheme; + if (Enum.TryParse(data.ToString(), true, out scheme)) + return scheme; + + throw new FormatException("Could not parse '" + data + "', expected http or https."); + } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Configuration/Section/UriSchemes.cs b/Kudu.SiteManagement/Configuration/Section/UriSchemes.cs new file mode 100644 index 000000000..0c3b8cfd3 --- /dev/null +++ b/Kudu.SiteManagement/Configuration/Section/UriSchemes.cs @@ -0,0 +1,9 @@ +using System; + +namespace Kudu.SiteManagement.Configuration.Section +{ + public enum UriScheme + { + Http, Https + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/Context/IKuduContext.cs b/Kudu.SiteManagement/Context/IKuduContext.cs new file mode 100644 index 000000000..474d9d56c --- /dev/null +++ b/Kudu.SiteManagement/Context/IKuduContext.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Kudu.SiteManagement.Configuration; + +namespace Kudu.SiteManagement.Context +{ + public interface IKuduContext + { + IPathResolver Paths { get; } + IKuduConfiguration Configuration { get; } + + Version IISVersion { get; } + IEnumerable IPAddresses { get; } + } + + public class KuduContext : IKuduContext + { + public IPathResolver Paths { get; private set; } + public IKuduConfiguration Configuration { get; private set; } + public Version IISVersion { get { return HttpRuntime.IISVersion; } } + public IEnumerable IPAddresses { get { return GetAddresses(); } } + + public KuduContext(IKuduConfiguration configuration, IPathResolver paths) + { + Configuration = configuration; + Paths = paths; + } + private static IEnumerable GetAddresses() + { + var host = Dns.GetHostEntry(Dns.GetHostName()); + return (from ip in host.AddressList where ip.AddressFamily == AddressFamily.InterNetwork select ip.ToString()).ToList(); + } + } +} diff --git a/Kudu.SiteManagement/DefaultSettingsResolver.cs b/Kudu.SiteManagement/DefaultSettingsResolver.cs deleted file mode 100644 index df402a2d8..000000000 --- a/Kudu.SiteManagement/DefaultSettingsResolver.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Configuration; -using System.IO; - -namespace Kudu.SiteManagement -{ - public class DefaultSettingsResolver : ISettingsResolver - { - private readonly string _sitesBaseUrl; - private readonly string _serviceSitesBaseUrl; - private readonly bool _customHostNames; - - public DefaultSettingsResolver() - : this(sitesBaseUrl: null, serviceSitesBaseUrl: null, enableCustomHostNames: null) - { - } - - public DefaultSettingsResolver(string sitesBaseUrl, string serviceSitesBaseUrl, string enableCustomHostNames) - { - // Ensure the base url is normalised to not have a leading dot, - // we will add this on later when joining the application name up - if (sitesBaseUrl != null) - { - _sitesBaseUrl = sitesBaseUrl.TrimStart('.'); - } - if (serviceSitesBaseUrl != null) - { - _serviceSitesBaseUrl = serviceSitesBaseUrl.TrimStart('.'); - } - - if (!String.IsNullOrEmpty(_serviceSitesBaseUrl) && !String.IsNullOrEmpty(_sitesBaseUrl)) - { - if (_serviceSitesBaseUrl.Equals(_sitesBaseUrl, StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException("serviceSitesBaseUrl cannot be the same as sitesBaseUrl."); - } - } - - if (enableCustomHostNames == null || !Boolean.TryParse(enableCustomHostNames, out _customHostNames)) - { - _customHostNames = false; - } - } - - public string SitesBaseUrl - { - get - { - return _sitesBaseUrl; - } - } - - public string ServiceSitesBaseUrl - { - get - { - return _serviceSitesBaseUrl; - } - } - - public bool CustomHostNames - { - get - { - return _customHostNames; - } - } - } -} diff --git a/Kudu.SiteManagement/IPathResolver.cs b/Kudu.SiteManagement/IPathResolver.cs index d1a261c62..8038b53cc 100644 --- a/Kudu.SiteManagement/IPathResolver.cs +++ b/Kudu.SiteManagement/IPathResolver.cs @@ -2,8 +2,6 @@ { public interface IPathResolver { - string ServiceSitePath { get; } - string SitesPath { get; } string GetApplicationPath(string applicationName); string GetLiveSitePath(string applicationName); } diff --git a/Kudu.SiteManagement/ISettingsResolver.cs b/Kudu.SiteManagement/ISettingsResolver.cs deleted file mode 100644 index 6332772f0..000000000 --- a/Kudu.SiteManagement/ISettingsResolver.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Kudu.SiteManagement -{ - public interface ISettingsResolver - { - string SitesBaseUrl { get; } - - string ServiceSitesBaseUrl { get; } - - bool CustomHostNames { get; } - } -} diff --git a/Kudu.SiteManagement/ISiteManager.cs b/Kudu.SiteManagement/ISiteManager.cs index 45741c456..7c92aba04 100644 --- a/Kudu.SiteManagement/ISiteManager.cs +++ b/Kudu.SiteManagement/ISiteManager.cs @@ -8,8 +8,9 @@ public interface ISiteManager IEnumerable GetSites(); Site GetSite(string applicationName); Task CreateSiteAsync(string applicationName); + Task DeleteSiteAsync(string applicationName); - bool AddSiteBinding(string applicationName, string siteBinding, SiteType siteType); + bool AddSiteBinding(string applicationName, KuduBinding binding); bool RemoveSiteBinding(string applicationName, string siteBinding, SiteType siteType); } } diff --git a/Kudu.SiteManagement/Kudu.SiteManagement.csproj b/Kudu.SiteManagement/Kudu.SiteManagement.csproj index b47c72c58..443e63bca 100644 --- a/Kudu.SiteManagement/Kudu.SiteManagement.csproj +++ b/Kudu.SiteManagement/Kudu.SiteManagement.csproj @@ -24,6 +24,7 @@ ..\packages\Microsoft.Web.Administration.7.0.0.0\lib\net20\Microsoft.Web.Administration.dll + False @@ -37,6 +38,7 @@ ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll + @@ -55,22 +57,51 @@ Properties\CommonAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + Code - - + + + + + + diff --git a/Kudu.SiteManagement/KuduBinding.cs b/Kudu.SiteManagement/KuduBinding.cs new file mode 100644 index 000000000..ee2a6baf4 --- /dev/null +++ b/Kudu.SiteManagement/KuduBinding.cs @@ -0,0 +1,15 @@ +using Kudu.SiteManagement.Configuration.Section; + +namespace Kudu.SiteManagement +{ + public struct KuduBinding + { + public UriScheme Schema { get; set; } + public string Ip { get; set; } + public int Port { get; set; } + public string Host { get; set; } + public bool Sni { get; set; } + public string Certificate { get; set; } + public SiteType SiteType { get; set; } + } +} \ No newline at end of file diff --git a/Kudu.SiteManagement/PathResolver.cs b/Kudu.SiteManagement/PathResolver.cs new file mode 100644 index 000000000..d6db4ea4b --- /dev/null +++ b/Kudu.SiteManagement/PathResolver.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using Kudu.SiteManagement.Configuration; + +namespace Kudu.SiteManagement +{ + public class PathResolver : IPathResolver + { + private readonly IKuduConfiguration _configuration; + + public PathResolver(IKuduConfiguration configuration) + { + _configuration = configuration; + } + + public string GetApplicationPath(string applicationName) + { + return Path.Combine(_configuration.ApplicationsPath, applicationName); + } + + public string GetLiveSitePath(string applicationName) + { + return Path.Combine(GetApplicationPath(applicationName), Constants.SiteFolder); + } + } +} diff --git a/Kudu.SiteManagement/SiteManager.cs b/Kudu.SiteManagement/SiteManager.cs index 463450df8..3bdc475b6 100644 --- a/Kudu.SiteManagement/SiteManager.cs +++ b/Kudu.SiteManagement/SiteManager.cs @@ -1,16 +1,23 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; using System.Net.NetworkInformation; +using System.Runtime.ConstrainedExecution; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Kudu.Client.Deployment; using Kudu.Contracts.Settings; using Kudu.Contracts.SourceControl; using Kudu.Core.Infrastructure; +using Kudu.SiteManagement.Certificates; +using Kudu.SiteManagement.Configuration; +using Kudu.SiteManagement.Configuration.Section; +using Kudu.SiteManagement.Context; using Microsoft.Web.Administration; using IIS = Microsoft.Web.Administration; @@ -19,25 +26,37 @@ namespace Kudu.SiteManagement public class SiteManager : ISiteManager { private const string HostingStartHtml = "hostingstart.html"; + private const string HostingStartHtmlContents = @" + +This web site has been successfully created + + + +

This web site has been successfully created


+ +"; private readonly static Random portNumberGenRnd = new Random((int)DateTime.UtcNow.Ticks); - private readonly IPathResolver _pathResolver; - private readonly bool _traceFailedRequests; private readonly string _logPath; - private readonly ISettingsResolver _settingsResolver; + private readonly bool _traceFailedRequests; + private readonly IKuduContext _context; + private readonly ICertificateSearcher _certificateSearcher; - public SiteManager(IPathResolver pathResolver, ISettingsResolver settingsResolver) - : this(pathResolver, traceFailedRequests: false, logPath: null, settingsResolver: settingsResolver) + public SiteManager(IKuduContext context, ICertificateSearcher certificateSearcher) + : this(context, certificateSearcher, false, null) { } - public SiteManager(IPathResolver pathResolver, bool traceFailedRequests, string logPath, ISettingsResolver settingsResolver) + public SiteManager(IKuduContext context, ICertificateSearcher certificateSearcher, bool traceFailedRequests, string logPath) { _logPath = logPath; - _pathResolver = pathResolver; _traceFailedRequests = traceFailedRequests; - _settingsResolver = settingsResolver; + _context = context; + _certificateSearcher = certificateSearcher; } public IEnumerable GetSites() @@ -78,61 +97,38 @@ public Site GetSite(string applicationName) private static List GetSiteUrls(IIS.Site site) { - var urls = new List(); - if (site == null) - { - return null; - } + { return null; } - foreach (IIS.Binding binding in site.Bindings) + return site.Bindings.Select(binding => new UriBuilder { - var builder = new UriBuilder - { - Host = String.IsNullOrEmpty(binding.Host) ? "localhost" : binding.Host, - Scheme = binding.Protocol, - Port = binding.EndPoint.Port == 80 ? -1 : binding.EndPoint.Port - }; - - urls.Add(builder.ToString()); - } - - return urls; + Host = string.IsNullOrEmpty(binding.Host) ? "localhost" : binding.Host, + Scheme = binding.Protocol, + Port = binding.EndPoint.Port == 80 ? -1 : binding.EndPoint.Port + }).Select(builder => builder.ToString()).ToList(); } public async Task CreateSiteAsync(string applicationName) { - using (var iis = GetServerManager()) + using (ServerManager iis = GetServerManager()) { try { // Determine the host header values - List siteBindings = GetDefaultBindings(applicationName, _settingsResolver.SitesBaseUrl); - List serviceSiteBindings = GetDefaultBindings(applicationName, _settingsResolver.ServiceSitesBaseUrl); + List siteBindings = BuildDefaultBindings(applicationName, _context.Configuration.Bindings.Where(b => b.SiteType == SiteType.Live)).ToList(); + List serviceSiteBindings = BuildDefaultBindings(applicationName, _context.Configuration.Bindings.Where(b => b.SiteType == SiteType.Service)).ToList(); // Create the service site for this site - string serviceSiteName = GetServiceSite(applicationName); - var serviceSite = CreateSiteAsync(iis, applicationName, serviceSiteName, _pathResolver.ServiceSitePath, serviceSiteBindings); + var serviceSite = CreateSiteAsync(iis, applicationName, GetServiceSite(applicationName), _context.Configuration.ServiceSitePath, serviceSiteBindings); // Create the main site string siteName = GetLiveSite(applicationName); - string root = _pathResolver.GetApplicationPath(applicationName); - string siteRoot = _pathResolver.GetLiveSitePath(applicationName); + string root = _context.Paths.GetApplicationPath(applicationName); + string siteRoot = _context.Paths.GetLiveSitePath(applicationName); string webRoot = Path.Combine(siteRoot, Constants.WebRoot); FileSystemHelpers.EnsureDirectory(webRoot); - File.WriteAllText(Path.Combine(webRoot, HostingStartHtml), @" - -This web site has been successfully created - - - -

This web site has been successfully created


- -"); + File.WriteAllText(Path.Combine(webRoot, HostingStartHtml), HostingStartHtmlContents); var site = CreateSiteAsync(iis, applicationName, siteName, webRoot, siteBindings); @@ -142,24 +138,20 @@ public async Task CreateSiteAsync(string applicationName) // Commit the changes to iis iis.CommitChanges(); - var serviceUrls = new List(); - foreach (var url in serviceSite.Bindings) - { - serviceUrls.Add(String.Format("http://{0}:{1}/", String.IsNullOrEmpty(url.Host) ? "localhost" : url.Host, url.EndPoint.Port)); - } + var serviceUrls = serviceSite.Bindings + .Select(url => String.Format("{0}://{1}:{2}/", url.Protocol, String.IsNullOrEmpty(url.Host) ? "localhost" : url.Host, url.EndPoint.Port)) + .ToList(); // Wait for the site to start - await OperationManager.AttemptAsync(() => WaitForSiteAsync(serviceUrls[0])); + await OperationManager.AttemptAsync(() => WaitForSiteAsync(serviceUrls.First())); // Set initial ScmType state to LocalGit var settings = new RemoteDeploymentSettingsManager(serviceUrls.First() + "api/settings"); await settings.SetValue(SettingsKeys.ScmType, ScmType.LocalGit); - var siteUrls = new List(); - foreach (var url in site.Bindings) - { - siteUrls.Add(String.Format("http://{0}:{1}/", String.IsNullOrEmpty(url.Host) ? "localhost" : url.Host, url.EndPoint.Port)); - } + var siteUrls = site.Bindings + .Select(url => String.Format("{0}://{1}:{2}/", url.Protocol, String.IsNullOrEmpty(url.Host) ? "localhost" : url.Host, url.EndPoint.Port)) + .ToList(); return new Site { @@ -182,13 +174,33 @@ public async Task CreateSiteAsync(string applicationName) } } + //NOTE: Small temprary object for configuration. + private struct BindingInformation + { + public string Binding { get; set; } + public IBindingConfiguration Configuration { get; set; } + //public string Url { get { return Configuration.Url; } } + public UriScheme Scheme { get { return Configuration.Scheme; } } + //public SiteType SiteType { get { return Configuration.SiteType; } } + public string Certificate { get { return Configuration.Certificate; } } + } + + private static IEnumerable BuildDefaultBindings(string applicationName, IEnumerable bindings) + { + return bindings.Select(configuration => configuration.Scheme == UriScheme.Http + ? new BindingInformation { Configuration = configuration, Binding = CreateBindingInformation(applicationName, configuration.Url) } + : new BindingInformation { Configuration = configuration, Binding = CreateBindingInformation(applicationName, configuration.Url, defaultPort: "443") }) + //NOTE: We order the bindings so we get the http bindings on top, this means we can easily prioritise those for testing site setup later. + .OrderBy(b => b.Scheme); + } + public async Task DeleteSiteAsync(string applicationName) { + string appPoolName = GetAppPool(applicationName); using (var iis = GetServerManager()) { // Get the app pool for this application - string appPoolName = GetAppPool(applicationName); - IIS.ApplicationPool kuduPool = iis.ApplicationPools[appPoolName]; + ApplicationPool kuduPool = iis.ApplicationPools[appPoolName]; if (kuduPool == null) { @@ -200,13 +212,26 @@ await Task.WhenAll( DeleteSiteAsync(iis, GetLiveSite(applicationName)), // Don't delete the physical files for the service site DeleteSiteAsync(iis, GetServiceSite(applicationName), deletePhysicalFiles: false) - ); + ); - iis.CommitChanges(); - string appPath = _pathResolver.GetApplicationPath(applicationName); - var sitePath = _pathResolver.GetLiveSitePath(applicationName); + try + { + iis.CommitChanges(); + Thread.Sleep(1000); + } + catch (NotImplementedException) + { + //NOTE: For some reason, deleting a site with a HTTPS bindings results in a NotImplementedException on Windows 7, but it seems to remove everything relevant anyways. + } + } + //NOTE: DeleteSiteAsync was not split into to usings before, but by calling CommitChanges midway, the iis manager goes into a read-only mode on Windows7 which then provokes + // an error on the next commit. On the next pass. Aquirering a new Manager seems like a more safe aproach. + using (var iis = GetServerManager()) + { + string appPath = _context.Paths.GetApplicationPath(applicationName); + string sitePath = _context.Paths.GetLiveSitePath(applicationName); try { DeleteSafe(sitePath); @@ -218,7 +243,7 @@ await Task.WhenAll( } finally { - // Remove the app pool and commit changes + iis.ApplicationPools.Remove(iis.ApplicationPools[appPoolName]); iis.CommitChanges(); @@ -230,94 +255,91 @@ await Task.WhenAll( } } - public bool AddSiteBinding(string applicationName, string siteBinding, SiteType siteType) + public bool AddSiteBinding(string applicationName, KuduBinding binding) { - IIS.Site site; - - if (!siteBinding.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) - { - siteBinding = "http://" + siteBinding; - } - - var uri = new Uri(siteBinding); - try { - using (var iis = GetServerManager()) + using (ServerManager iis = GetServerManager()) { - if (!IsAvailable(uri.Host, uri.Port, iis)) + if (!IsAvailable(binding.Host, binding.Port, iis)) { return false; } - if (siteType == SiteType.Live) - { - site = iis.Sites[GetLiveSite(applicationName)]; - } - else + IIS.Site site = binding.SiteType == SiteType.Live + ? iis.Sites[GetLiveSite(applicationName)] + : iis.Sites[GetServiceSite(applicationName)]; + + if (site == null) { - site = iis.Sites[GetServiceSite(applicationName)]; + return true; } - if (site != null) + string bindingInformation = string.Format("{0}:{1}:{2}", binding.Ip, binding.Port, binding.Host); + switch (binding.Schema) { - site.Bindings.Add("*:" + uri.Port + ":" + uri.Host, "http"); - iis.CommitChanges(); - - Thread.Sleep(1000); + case UriScheme.Http: + site.Bindings.Add(bindingInformation, "http"); + break; + + case UriScheme.Https: + Certificate cert = _certificateSearcher.Lookup(binding.Certificate).ByThumbprint(); + Binding bind = site.Bindings.Add(bindingInformation, cert.GetCertHash(), cert.StoreName); + if (binding.Sni) + { + bind.SetAttributeValue("sslFlags", SslFlags.Sni); + } + + break; } + iis.CommitChanges(); + Thread.Sleep(1000); } return true; } - catch + catch (Exception ex) { + Debug.WriteLine(ex); return false; } } public bool RemoveSiteBinding(string applicationName, string siteBinding, SiteType siteType) { - IIS.Site site; - try { - using (var iis = GetServerManager()) + using (ServerManager iis = GetServerManager()) { - if (siteType == SiteType.Live) - { - site = iis.Sites[GetLiveSite(applicationName)]; - } - else - { - site = iis.Sites[GetServiceSite(applicationName)]; - } + IIS.Site site = siteType == SiteType.Live + ? iis.Sites[GetLiveSite(applicationName)] + : iis.Sites[GetServiceSite(applicationName)]; - if (site != null) - { - var uri = new Uri(siteBinding); - var binding = site.Bindings.FirstOrDefault(x => x.Host.Equals(uri.Host) - && x.EndPoint.Port.Equals(uri.Port) - && x.Protocol.Equals(uri.Scheme)); + if (site == null) + { return true; } - if (binding != null) - { - site.Bindings.Remove(binding); - iis.CommitChanges(); + Uri uri = new Uri(siteBinding); + Binding binding = site.Bindings + .FirstOrDefault(x => x.Host.Equals(uri.Host) + && x.EndPoint.Port.Equals(uri.Port) + && x.Protocol.Equals(uri.Scheme)); - Thread.Sleep(1000); - } - } - } + if (binding == null) + { return true; } + site.Bindings.Remove(binding); + iis.CommitChanges(); + Thread.Sleep(1000); + } return true; } - catch + catch (Exception ex) { + Debug.WriteLine(ex); return false; } } - private static void MapServiceSitePath(IIS.ServerManager iis, string applicationName, string path, string siteRoot) + private static void MapServiceSitePath(ServerManager iis, string applicationName, string path, string siteRoot) { string serviceSiteName = GetServiceSite(applicationName); @@ -332,7 +354,7 @@ private static void MapServiceSitePath(IIS.ServerManager iis, string application site.Applications.Add(path, siteRoot); } - private static IIS.ApplicationPool EnsureAppPool(IIS.ServerManager iis, string appName) + private static ApplicationPool EnsureAppPool(ServerManager iis, string appName) { string appPoolName = GetAppPool(appName); var kuduAppPool = iis.ApplicationPools[appPoolName]; @@ -341,7 +363,7 @@ private static IIS.ApplicationPool EnsureAppPool(IIS.ServerManager iis, string a iis.ApplicationPools.Add(appPoolName); iis.CommitChanges(); kuduAppPool = iis.ApplicationPools[appPoolName]; - kuduAppPool.ManagedPipelineMode = IIS.ManagedPipelineMode.Integrated; + kuduAppPool.ManagedPipelineMode = ManagedPipelineMode.Integrated; kuduAppPool.ManagedRuntimeVersion = "v4.0"; kuduAppPool.AutoStart = true; kuduAppPool.ProcessModel.LoadUserProfile = true; @@ -352,18 +374,8 @@ private static IIS.ApplicationPool EnsureAppPool(IIS.ServerManager iis, string a return kuduAppPool; } - private static List GetDefaultBindings(string applicationName, string baseUrl) - { - var siteBindings = new List(); - if (!String.IsNullOrWhiteSpace(baseUrl)) - { - string binding = CreateBindingInformation(applicationName, baseUrl); - siteBindings.Add(binding); - } - return siteBindings; - } - private static int GetRandomPort(IIS.ServerManager iis) + private static int GetRandomPort(ServerManager iis) { int randomPort = portNumberGenRnd.Next(1025, 65535); while (!IsAvailable(randomPort, iis)) @@ -374,95 +386,101 @@ private static int GetRandomPort(IIS.ServerManager iis) return randomPort; } - private static bool IsAvailable(int port, IIS.ServerManager iis) + private static bool IsAvailable(int port, ServerManager iis) { var tcpConnections = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections(); - foreach (var connectionInfo in tcpConnections) - { - if (connectionInfo.LocalEndPoint.Port == port) - { - return false; - } - } - - foreach (var iisSite in iis.Sites) - { - foreach (var binding in iisSite.Bindings) - { - if (binding.EndPoint != null && binding.EndPoint.Port == port) - { - return false; - } - } - } - - return true; + return tcpConnections.All(connectionInfo => connectionInfo.LocalEndPoint.Port != port) + && iis.Sites + .SelectMany(iisSite => iisSite.Bindings) + .All(binding => binding.EndPoint == null || binding.EndPoint.Port != port); } - private static bool IsAvailable(string host, int port, IIS.ServerManager iis) + private static bool IsAvailable(string host, int port, ServerManager iis) { - foreach (var iisSite in iis.Sites) - { - foreach (var binding in iisSite.Bindings) - { - if (binding.EndPoint != null && binding.EndPoint.Port == port && binding.Host == host) - { - return false; - } - } - } - - return true; + return iis.Sites + .SelectMany(iisSite => iisSite.Bindings) + .All(binding => binding.EndPoint == null || binding.EndPoint.Port != port || binding.Host != host); } - private IIS.Site CreateSiteAsync(IIS.ServerManager iis, string applicationName, string siteName, string siteRoot, List siteBindings) + private IIS.Site CreateSiteAsync(ServerManager iis, string applicationName, string siteName, string siteRoot, List bindings) { var pool = EnsureAppPool(iis, applicationName); IIS.Site site; - - if (siteBindings != null && siteBindings.Count > 0) + if (bindings.Any()) { - site = iis.Sites.Add(siteName, "http", siteBindings.First(), siteRoot); + BindingInformation first = bindings.First(); + + //Binding primaryBinding; + site = first.Scheme == UriScheme.Http + ? iis.Sites.Add(siteName, "http", first.Binding, siteRoot) + : iis.Sites.Add(siteName, first.Binding, siteRoot, _certificateSearcher.Lookup(first.Certificate).ByFriendlyName().GetCertHash()); + if (first.Configuration.RequireSni) + { + site.Bindings.First().SetAttributeValue("sslFlags", SslFlags.Sni); + } + + //Note: Add the rest of the bindings normally. + foreach (BindingInformation binding in bindings.Skip(1)) + { + switch (binding.Scheme) + { + case UriScheme.Http: + site.Bindings.Add(binding.Binding, "http"); + break; + + case UriScheme.Https: + Certificate cert = _certificateSearcher.Lookup(binding.Certificate).ByFriendlyName(); + if (cert == null) + { + throw new ConfigurationErrorsException(string.Format("Could not find a certificate by the name '{0}'.", binding.Certificate)); + } + + Binding bind = site.Bindings.Add(binding.Binding, cert.GetCertHash(), cert.StoreName); + if (binding.Configuration.RequireSni) + { + bind.SetAttributeValue("sslFlags", SslFlags.Sni); + } + break; + } + } } else { - int sitePort = GetRandomPort(iis); - site = iis.Sites.Add(siteName, siteRoot, sitePort); + site = iis.Sites.Add(siteName, siteRoot, GetRandomPort(iis)); } site.ApplicationDefaults.ApplicationPoolName = pool.Name; - if (_traceFailedRequests) - { - site.TraceFailedRequestsLogging.Enabled = true; - string path = Path.Combine(_logPath, applicationName, "Logs"); - Directory.CreateDirectory(path); - site.TraceFailedRequestsLogging.Directory = path; - } + if (!_traceFailedRequests) + return site; + + site.TraceFailedRequestsLogging.Enabled = true; + string path = Path.Combine(_logPath, applicationName, "Logs"); + Directory.CreateDirectory(path); + site.TraceFailedRequestsLogging.Directory = path; return site; } - private static void EnsureDefaultDocument(IIS.ServerManager iis) + private static void EnsureDefaultDocument(ServerManager iis) { - Configuration applicationHostConfiguration = iis.GetApplicationHostConfiguration(); - ConfigurationSection defaultDocumentSection = applicationHostConfiguration.GetSection("system.webServer/defaultDocument"); + IIS.Configuration applicationHostConfiguration = iis.GetApplicationHostConfiguration(); + IIS.ConfigurationSection defaultDocumentSection = applicationHostConfiguration.GetSection("system.webServer/defaultDocument"); - ConfigurationElementCollection filesCollection = defaultDocumentSection.GetCollection("files"); + IIS.ConfigurationElementCollection filesCollection = defaultDocumentSection.GetCollection("files"); - if (!filesCollection.Any(ConfigurationElementContainsHostingStart)) - { - ConfigurationElement addElement = filesCollection.CreateElement("add"); + if (filesCollection.Any(ConfigurationElementContainsHostingStart)) + return; - addElement["value"] = HostingStartHtml; - filesCollection.Add(addElement); + IIS.ConfigurationElement addElement = filesCollection.CreateElement("add"); + addElement["value"] = HostingStartHtml; + filesCollection.Add(addElement); - iis.CommitChanges(); - } + iis.CommitChanges(); } - private static bool ConfigurationElementContainsHostingStart(ConfigurationElement configurationElement) + private static bool ConfigurationElementContainsHostingStart(IIS.ConfigurationElement configurationElement) { object valueAttribute = configurationElement["value"]; @@ -501,7 +519,7 @@ private static string CreateBindingInformation(string applicationName, string ba return String.Format("{0}:{1}:{2}", ip, port, applicationName + "." + host); } - private static Task DeleteSiteAsync(IIS.ServerManager iis, string siteName, bool deletePhysicalFiles = true) + private static Task DeleteSiteAsync(ServerManager iis, string siteName, bool deletePhysicalFiles = true) { var site = iis.Sites[siteName]; if (site != null) @@ -550,7 +568,7 @@ private static void DeleteSafe(string physicalPath) private static ServerManager GetServerManager() { - return new IIS.ServerManager(Environment.ExpandEnvironmentVariables("%windir%\\system32\\inetsrv\\config\\applicationHost.config")); + return new ServerManager(Environment.ExpandEnvironmentVariables("%windir%\\system32\\inetsrv\\config\\applicationHost.config")); } private static async Task WaitForSiteAsync(string serviceUrl) diff --git a/Kudu.SiteManagement/SiteType.cs b/Kudu.SiteManagement/SiteType.cs index 4f562ecaa..dbf0f9d54 100644 --- a/Kudu.SiteManagement/SiteType.cs +++ b/Kudu.SiteManagement/SiteType.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; diff --git a/Kudu.SiteManagement/SslFlags.cs b/Kudu.SiteManagement/SslFlags.cs new file mode 100644 index 000000000..8e4ed91e4 --- /dev/null +++ b/Kudu.SiteManagement/SslFlags.cs @@ -0,0 +1,13 @@ +using System; + +namespace Kudu.SiteManagement +{ + [Flags, System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Flags", Justification = "In this particular case, using just SSL is a bit to abstract to understand what it is.")] + public enum SslFlags + { + None = 0, + Sni = 1, + //Note: Not in use as of now. + CentralCertStore = 1 << 1 + } +} \ No newline at end of file diff --git a/Kudu.TestHarness/ApplicationManager.cs b/Kudu.TestHarness/ApplicationManager.cs index 3f4e9ca5c..63911a5d9 100644 --- a/Kudu.TestHarness/ApplicationManager.cs +++ b/Kudu.TestHarness/ApplicationManager.cs @@ -23,16 +23,14 @@ public class ApplicationManager { private static bool _testFailureOccured; private readonly ISiteManager _siteManager; - private readonly ISettingsResolver _settingsResolver; private readonly Site _site; private readonly string _appName; - internal ApplicationManager(ISiteManager siteManager, Site site, string appName, ISettingsResolver settingsResolver) + internal ApplicationManager(ISiteManager siteManager, Site site, string appName) { _siteManager = siteManager; _site = site; _appName = appName; - _settingsResolver = settingsResolver; // Always null in public Kudu, but makes the code more similar to private Kudu NetworkCredential credentials = null; @@ -251,14 +249,7 @@ public static async Task RunAsync(string testName, Func action) diff --git a/Kudu.TestHarness/SitePool.cs b/Kudu.TestHarness/SitePool.cs index 26794aa55..a1023b993 100644 --- a/Kudu.TestHarness/SitePool.cs +++ b/Kudu.TestHarness/SitePool.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Kudu.SiteManagement; +using Kudu.SiteManagement.Certificates; +using Kudu.SiteManagement.Configuration; +using Kudu.SiteManagement.Context; namespace Kudu.TestHarness { @@ -68,16 +72,13 @@ private static async Task CreateApplicationInternal() string operationName = "SitePool.CreateApplicationInternal " + applicationName; - var pathResolver = new DefaultPathResolver(PathHelper.ServiceSitePath, PathHelper.SitesPath); - var settingsResolver = new DefaultSettingsResolver(); - - var siteManager = GetSiteManager(pathResolver, settingsResolver); + var siteManager = GetSiteManager(new KuduTestContext()); Site site = siteManager.GetSite(applicationName); if (site != null) { TestTracer.Trace("{0} Site already exists at {1}. Reusing site", operationName, site.SiteUrl); - var appManager = new ApplicationManager(siteManager, site, applicationName, settingsResolver) + var appManager = new ApplicationManager(siteManager, site, applicationName) { SitePoolIndex = siteIndex }; @@ -107,16 +108,18 @@ private static async Task CreateApplicationInternal() site = await siteManager.CreateSiteAsync(applicationName); TestTracer.Trace("{0} Created new site at {1}", operationName, site.SiteUrl); - return new ApplicationManager(siteManager, site, applicationName, settingsResolver) + return new ApplicationManager(siteManager, site, applicationName) { SitePoolIndex = siteIndex }; } } - private static ISiteManager GetSiteManager(DefaultPathResolver pathResolver, DefaultSettingsResolver settingsResolver) + + private static ISiteManager GetSiteManager(IKuduContext context) { - return new SiteManager(pathResolver, traceFailedRequests: true, logPath: PathHelper.TestResultsPath, settingsResolver: settingsResolver); + //TODO: Mock Searcher. + return new SiteManager(context, new CertificateSearcher(context.Configuration), true, PathHelper.TestResultsPath); } // Try to write index.html. In case of failure with 502, we will include @@ -152,4 +155,36 @@ private static void WriteIndexHtml(ApplicationManager appManager) } } } + + public class KuduTestContext : IKuduContext + { + public IPathResolver Paths { get; private set; } + public IKuduConfiguration Configuration { get; private set; } + public Version IISVersion { get; private set; } + public IEnumerable IPAddresses { get; private set; } + + public KuduTestContext() + { + Paths = new PathResolver(Configuration = new KuduTestConfiguration()); + } + } + + public class KuduTestConfiguration : IKuduConfiguration + { + public Version IISVersion { get; private set; } + public IEnumerable IPAddresses { get; private set; } + + public string RootPath { get; private set; } + public string ApplicationsPath { get; private set; } + public string ServiceSitePath { get; private set; } + public bool CustomHostNamesEnabled { get; private set; } + public IEnumerable Bindings { get; private set; } + public IEnumerable CertificateStores { get; private set; } + + public KuduTestConfiguration() + { + ApplicationsPath = PathHelper.SitesPath; + ServiceSitePath = PathHelper.ServiceSitePath; + } + } } diff --git a/Kudu.TestHarness/TestRepositories.cs b/Kudu.TestHarness/TestRepositories.cs index 509a6e04b..f4a35e51c 100644 --- a/Kudu.TestHarness/TestRepositories.cs +++ b/Kudu.TestHarness/TestRepositories.cs @@ -49,6 +49,7 @@ internal class TestRepositories new TestRepositoryInfo("https://github.com/KuduApps/PreviewMvc5.git", "f361ad3"), new TestRepositoryInfo("https://github.com/KuduApps/PreviewSpa5.git", "9f6cde2"), new TestRepositoryInfo("https://github.com/KuduApps/PreviewWebApi5.git", "74a0ce0"), + new TestRepositoryInfo("https://github.com/KuduApps/ProjectKWebApplication.git", "fa5f2bc"), new TestRepositoryInfo("https://github.com/KuduApps/ProjectWithNoSolution.git", "5460398"), new TestRepositoryInfo("https://github.com/KuduApps/TargetPathTest.git", "7446104"), new TestRepositoryInfo("https://github.com/KuduApps/VersionPinnedNodeJsApp.git", "5f6c64f"), diff --git a/Kudu.Web/App_Start/Startup.cs b/Kudu.Web/App_Start/Startup.cs index c90ee273e..bfadb3001 100644 --- a/Kudu.Web/App_Start/Startup.cs +++ b/Kudu.Web/App_Start/Startup.cs @@ -1,9 +1,13 @@ using System; using System.Configuration; using System.IO; +using System.Linq; using System.Web; using Kudu.Client.Infrastructure; using Kudu.SiteManagement; +using Kudu.SiteManagement.Certificates; +using Kudu.SiteManagement.Configuration; +using Kudu.SiteManagement.Context; using Kudu.Web.Infrastructure; using Kudu.Web.Models; using Ninject; @@ -61,37 +65,30 @@ private static void RegisterServices(IKernel kernel) private static void SetupKuduServices(IKernel kernel) { - string root = HttpRuntime.AppDomainAppPath; - string serviceSitePath = ConfigurationManager.AppSettings["serviceSitePath"]; - string sitesPath = ConfigurationManager.AppSettings["sitesPath"]; - string sitesBaseUrl = ConfigurationManager.AppSettings["urlBaseValue"]; - string serviceSitesBaseUrl = ConfigurationManager.AppSettings["serviceUrlBaseValue"]; - string customHostNames = ConfigurationManager.AppSettings["enableCustomHostNames"]; - - serviceSitePath = Path.Combine(root, serviceSitePath); - sitesPath = Path.Combine(root, sitesPath); - - var pathResolver = new DefaultPathResolver(serviceSitePath, sitesPath); - var settingsResolver = new DefaultSettingsResolver(sitesBaseUrl, serviceSitesBaseUrl, customHostNames); - - kernel.Bind().ToConstant(pathResolver); - kernel.Bind().ToConstant(settingsResolver); + IKuduConfiguration configuration = KuduConfiguration.Load(HttpRuntime.AppDomainAppPath); + kernel.Bind().ToConstant(configuration); + kernel.Bind().To(); kernel.Bind().To().InSingletonScope(); + kernel.Bind().To(); + kernel.Bind().To(); + + //TODO: Instantialte from container instead of factory. kernel.Bind().ToMethod(_ => new KuduEnvironment { RunningAgainstLocalKuduService = true, IsAdmin = IdentityHelper.IsAnAdministrator(), - ServiceSitePath = pathResolver.ServiceSitePath, - SitesPath = pathResolver.SitesPath + ServiceSitePath = configuration.ServiceSitePath, + SitesPath = configuration.ApplicationsPath }); + // TODO: Integrate with membership system kernel.Bind().ToConstant(new BasicAuthCredentialProvider("admin", "kudu")); kernel.Bind().To().InRequestScope(); kernel.Bind().To(); // Sql CE setup - Directory.CreateDirectory(Path.Combine(root, "App_Data")); + Directory.CreateDirectory(Path.Combine(configuration.RootPath, "App_Data")); } } } diff --git a/Kudu.Web/Controllers/ApplicationController.cs b/Kudu.Web/Controllers/ApplicationController.cs index a6e7bb940..d13237bad 100644 --- a/Kudu.Web/Controllers/ApplicationController.cs +++ b/Kudu.Web/Controllers/ApplicationController.cs @@ -5,6 +5,10 @@ using System.Web.Mvc; using Kudu.Client.Infrastructure; using Kudu.SiteManagement; +using Kudu.SiteManagement.Certificates; +using Kudu.SiteManagement.Configuration; +using Kudu.SiteManagement.Configuration.Section; +using Kudu.SiteManagement.Context; using Kudu.Web.Infrastructure; using Kudu.Web.Models; @@ -14,18 +18,21 @@ public class ApplicationController : Controller { private readonly IApplicationService _applicationService; private readonly KuduEnvironment _environment; + private readonly IKuduContext _context; + private readonly ICertificateSearcher _certificates; private readonly ICredentialProvider _credentialProvider; - private readonly ISettingsResolver _settingsResolver; public ApplicationController(IApplicationService applicationService, ICredentialProvider credentialProvider, KuduEnvironment environment, - ISettingsResolver settingsResolver) + IKuduContext context, + ICertificateSearcher certificates) { _applicationService = applicationService; _credentialProvider = credentialProvider; _environment = environment; - _settingsResolver = settingsResolver; + _context = context; + _certificates = certificates; } protected override void OnActionExecuting(ActionExecutingContext filterContext) @@ -90,7 +97,7 @@ public async Task Delete(string slug) [HttpPost] [ActionName("add-custom-site-binding")] - public async Task AddCustomSiteBinding(string slug, string siteBinding) + public async Task AddCustomSiteBinding(string slug, string siteSchema, string siteIp, string sitePort, string siteHost, string siteRequireSni, string siteCertificate) { IApplication application = _applicationService.GetApplication(slug); @@ -99,7 +106,15 @@ public async Task AddCustomSiteBinding(string slug, string siteBin return HttpNotFound(); } - _applicationService.AddLiveSiteBinding(slug, siteBinding); + _applicationService.AddSiteBinding(slug, new KuduBinding { + Schema = siteSchema.Equals("https://", StringComparison.OrdinalIgnoreCase) ? UriScheme.Https : UriScheme.Http, + Ip = siteIp, + Port = int.Parse(sitePort), + Host = siteHost, + Sni = bool.Parse(siteRequireSni), + Certificate = siteCertificate, + SiteType = SiteType.Live + }); return await GetApplicationView("settings", "Details", slug); } @@ -122,7 +137,7 @@ public async Task RemoveCustomSiteBinding(string slug, string site [HttpPost] [ActionName("add-service-site-binding")] - public async Task AddServiceSiteBinding(string slug, string siteBinding) + public async Task AddServiceSiteBinding(string slug, string siteSchema, string siteIp, string sitePort, string siteHost, string siteRequireSni, string siteCertificate) { IApplication application = _applicationService.GetApplication(slug); @@ -131,7 +146,15 @@ public async Task AddServiceSiteBinding(string slug, string siteBi return HttpNotFound(); } - _applicationService.AddServiceSiteBinding(slug, siteBinding); + _applicationService.AddSiteBinding(slug, new KuduBinding { + Schema = siteSchema.Equals("https://", StringComparison.OrdinalIgnoreCase) ? UriScheme.Https : UriScheme.Http, + Ip = siteIp, + Port = int.Parse(sitePort), + Host = siteHost, + Sni = bool.Parse(siteRequireSni), + Certificate = siteCertificate, + SiteType = SiteType.Service + }); return await GetApplicationView("settings", "Details", slug); } @@ -158,7 +181,7 @@ private async Task GetApplicationView(string tab, string viewName, ICredentials credentials = _credentialProvider.GetCredentials(); var repositoryInfo = await application.GetRepositoryInfo(credentials); - var appViewModel = new ApplicationViewModel(application, _settingsResolver); + var appViewModel = new ApplicationViewModel(application, _context, _certificates.FindAll()); appViewModel.RepositoryInfo = repositoryInfo; ViewBag.slug = slug; diff --git a/Kudu.Web/Controllers/DeploymentsController.cs b/Kudu.Web/Controllers/DeploymentsController.cs index 3cd81b878..cb435280d 100644 --- a/Kudu.Web/Controllers/DeploymentsController.cs +++ b/Kudu.Web/Controllers/DeploymentsController.cs @@ -9,6 +9,9 @@ using Kudu.Core.Deployment; using Kudu.Core.SourceControl; using Kudu.SiteManagement; +using Kudu.SiteManagement.Certificates; +using Kudu.SiteManagement.Configuration; +using Kudu.SiteManagement.Context; using Kudu.Web.Infrastructure; using Kudu.Web.Models; @@ -18,15 +21,18 @@ public class DeploymentsController : Controller { private readonly IApplicationService _applicationService; private readonly ICredentialProvider _credentialProvider; - private readonly ISettingsResolver _settingsResolver; + private readonly IKuduContext _context; + private readonly ICertificateSearcher _certificates; public DeploymentsController(IApplicationService applicationService, ICredentialProvider credentialProvider, - ISettingsResolver settingsResolver) + IKuduContext context, + ICertificateSearcher certificates) { _applicationService = applicationService; _credentialProvider = credentialProvider; - _settingsResolver = settingsResolver; + _context = context; + _certificates = certificates; } protected override void OnActionExecuting(ActionExecutingContext filterContext) @@ -53,7 +59,7 @@ public async Task Index(string slug) await Task.WhenAll(deployResults, repositoryInfo); - var appViewModel = new ApplicationViewModel(application, _settingsResolver) + var appViewModel = new ApplicationViewModel(application, _context, _certificates.FindAll()) { RepositoryInfo = repositoryInfo.Result, Deployments = deployResults.Result.ToList() diff --git a/Kudu.Web/Kudu.Web.csproj b/Kudu.Web/Kudu.Web.csproj index f29bc10ba..bfc2c35ef 100644 --- a/Kudu.Web/Kudu.Web.csproj +++ b/Kudu.Web/Kudu.Web.csproj @@ -289,11 +289,11 @@ - False + True True 14905 / - http://localhost:23327/ + http://localhost:14905/ False False diff --git a/Kudu.Web/Models/ApplicationService.cs b/Kudu.Web/Models/ApplicationService.cs index d0c829bb4..e01d99afd 100644 --- a/Kudu.Web/Models/ApplicationService.cs +++ b/Kudu.Web/Models/ApplicationService.cs @@ -58,17 +58,6 @@ public IApplication GetApplication(string name) }; } - public bool AddLiveSiteBinding(string name, string siteBinding) - { - var application = GetApplication(name); - if (application == null) - { - return false; - } - - return _siteManager.AddSiteBinding(name, siteBinding, SiteType.Live); - } - public bool RemoveLiveSiteBinding(string name, string siteBinding) { var application = GetApplication(name); @@ -80,7 +69,7 @@ public bool RemoveLiveSiteBinding(string name, string siteBinding) return _siteManager.RemoveSiteBinding(name, siteBinding, SiteType.Live); } - public bool AddServiceSiteBinding(string name, string siteBinding) + public bool RemoveServiceSiteBinding(string name, string siteBinding) { var application = GetApplication(name); if (application == null) @@ -88,10 +77,10 @@ public bool AddServiceSiteBinding(string name, string siteBinding) return false; } - return _siteManager.AddSiteBinding(name, siteBinding, SiteType.Service); + return _siteManager.RemoveSiteBinding(name, siteBinding, SiteType.Service); } - public bool RemoveServiceSiteBinding(string name, string siteBinding) + public bool AddSiteBinding(string name, KuduBinding binding) { var application = GetApplication(name); if (application == null) @@ -99,7 +88,7 @@ public bool RemoveServiceSiteBinding(string name, string siteBinding) return false; } - return _siteManager.RemoveSiteBinding(name, siteBinding, SiteType.Service); + return _siteManager.AddSiteBinding(name, binding); } } @@ -110,4 +99,8 @@ public class SiteExistsException : InvalidOperationException public class SiteNotFoundException : InvalidOperationException { } + + + + } \ No newline at end of file diff --git a/Kudu.Web/Models/ApplicationViewModel.cs b/Kudu.Web/Models/ApplicationViewModel.cs index 9eafb71a3..296f36501 100644 --- a/Kudu.Web/Models/ApplicationViewModel.cs +++ b/Kudu.Web/Models/ApplicationViewModel.cs @@ -1,8 +1,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Web.Mvc; using Kudu.Core.Deployment; using Kudu.Core.SourceControl; using Kudu.SiteManagement; +using Kudu.SiteManagement.Certificates; +using Kudu.SiteManagement.Configuration; +using Kudu.SiteManagement.Context; namespace Kudu.Web.Models { @@ -10,16 +15,35 @@ public class ApplicationViewModel { public ApplicationViewModel() { + Schemas = new[] + { + new SelectListItem {Text = "Http://", Value = "Http://", Selected = true}, + new SelectListItem {Text = "Https://", Value = "Https://"} + }; } - public ApplicationViewModel(IApplication application, ISettingsResolver settingsResolver) + public ApplicationViewModel(IApplication application, IKuduContext context, + IEnumerable certificates) + : this() { + //Certificates = certificates; Name = application.Name; SiteUrl = application.SiteUrl; SiteUrls = application.SiteUrls; ServiceUrl = application.ServiceUrl; ServiceUrls = application.ServiceUrls; - CustomHostNames = settingsResolver.CustomHostNames; + + CustomHostNames = context.Configuration.CustomHostNamesEnabled; + + Certificates = certificates + .Select(cert => new SelectListItem { Text = cert.FriendlyName, Value = cert.Thumbprint }) + .ToArray(); + + IpAddresses = (new[] { new SelectListItem {Text = "All Unassigned", Value = "*", Selected = true } }) + .Union(context.IPAddresses.Select(ip => new SelectListItem { Text = ip, Value = ip })) + .ToArray(); + + SupportsSni = context.IISVersion.Major == 8; } [Required] @@ -30,7 +54,8 @@ public ApplicationViewModel(IApplication application, ISettingsResolver settings public IEnumerable ServiceUrls { get; set; } public bool CustomHostNames { get; private set; } public RepositoryInfo RepositoryInfo { get; set; } - + public bool SupportsSni { get; set; } + public string GitUrl { get @@ -62,5 +87,9 @@ public string CloneUrl return null; } } + + public IEnumerable Schemas { get; set; } + public IEnumerable Certificates { get; set; } + public IEnumerable IpAddresses { get; set; } } } \ No newline at end of file diff --git a/Kudu.Web/Models/IApplicationService.cs b/Kudu.Web/Models/IApplicationService.cs index f2e245413..c9b92e37e 100644 --- a/Kudu.Web/Models/IApplicationService.cs +++ b/Kudu.Web/Models/IApplicationService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Kudu.SiteManagement; namespace Kudu.Web.Models { @@ -9,9 +10,8 @@ public interface IApplicationService Task DeleteApplication(string name); IEnumerable GetApplications(); IApplication GetApplication(string name); - bool AddLiveSiteBinding(string name, string siteBinding); bool RemoveLiveSiteBinding(string name, string siteBinding); - bool AddServiceSiteBinding(string name, string siteBinding); bool RemoveServiceSiteBinding(string name, string siteBinding); + bool AddSiteBinding(string name, KuduBinding binding); } } diff --git a/Kudu.Web/Views/Application/Details.cshtml b/Kudu.Web/Views/Application/Details.cshtml index 16152f0c2..d1dda1ef6 100644 --- a/Kudu.Web/Views/Application/Details.cshtml +++ b/Kudu.Web/Views/Application/Details.cshtml @@ -1,9 +1,9 @@ -@using Kudu.Core.SourceControl +@using System.Activities.Expressions +@using Kudu.Core.SourceControl @using Kudu.Web.Models @using Kudu.Web.Infrastructure @model ApplicationViewModel - @{ ViewBag.Title = Model.Name; } @@ -30,6 +30,152 @@ + + +@helper AddBindingForm(string name, string action, string controller) +{ + //Note: Lets mimic the entire IIS dialog instead so it is more familiar to IIS administrators. + using (Html.BeginForm(action, controller, new { slug = Model.Name.GenerateSlug() }, FormMethod.Post)) + { + + @Html.ValidationSummary() + +
+
+ + @Html.DropDownList("siteSchema", Model.Schemas, new { onchange = "schemaChanged('" + name + "')", id = name + "SiteSchema", style = "width: 100%;" }) +
+
+ + @Html.DropDownList("siteIp", Model.IpAddresses, new { style = "width: 100%", id = name + "SiteIp" }) +
+
+ + @Html.TextBox("sitePort", "80", new { style = "width: 50%", id = name + "SitePort" }) +
+
+
+
+ + @Html.TextBox("siteHost", "", new { placeholder = "example.org", style = "width: 100%", id = name + "SiteHost" }) +
+
+ if (Model.SupportsSni) + { + + } + + + } +} + @if (Model.CustomHostNames) {
@@ -58,7 +204,7 @@ @uri.Scheme - @uri.Host + @uri.Host @uri.Port @@ -75,13 +221,7 @@ } - @using (Html.BeginForm("add-custom-site-binding", "Application", new { slug = Model.Name.GenerateSlug() }, FormMethod.Post, new { @class = "form-inline" })) - { - - @Html.ValidationSummary() - http:// @Html.TextBox("siteBinding", "", new { placeholder = "example.org" }) - - } + @AddBindingForm("app", "add-custom-site-binding", "Application")
@@ -113,7 +253,7 @@ @uri.Scheme - @uri.Host + @uri.Host @uri.Port @@ -127,27 +267,12 @@ { @Html.Hidden("siteBinding", "", new { id = "removeservicebinding" }) } - } - @using (Html.BeginForm("add-service-site-binding", "Application", new { slug = Model.Name.GenerateSlug() }, FormMethod.Post, new { @class = "form-inline" })) - { - - @Html.ValidationSummary() - http:// @Html.TextBox("siteBinding", "", new { placeholder = "example.org" }) - - } + @AddBindingForm("scm", "add-service-site-binding", "Application") - - } @using (Html.BeginForm("Delete", "Application", new { slug = Model.Name.GenerateSlug() })) diff --git a/Kudu.Web/Web.config b/Kudu.Web/Web.config index 8663b13a3..b4d538107 100644 --- a/Kudu.Web/Web.config +++ b/Kudu.Web/Web.config @@ -4,61 +4,102 @@ http://go.microsoft.com/fwlink/?LinkId=169433 --> - + +
+ + - + + - - - - + + + + + - - + The following attributes are support for binding configurations: + + - url: (Required) The url suffix for bindings. Supported formats: + kudu.localtest.me - only specify hostname suffix + kudu.localtest.me:8080 - uses suffix and port 8080 + 192.168.100.3:80:kudu.localtest.me - binds to internal ip, port 80, with hostname suffix + + - scheme: The scheme for bindings, either 'http' or 'https', if the attribute is not set, default is 'http'. + + - certificate: Friendly name of the certificate to be used for https bindings. + This is required when the scheme is set to 'https', otherwise it has no meaning. + + - require-sni: Sets if SNI is required for https bindings. + This is only used for https bindings and is only supported on IIS 8 and above. + + ### Remarks for IIS 7 Support: + + Since IIS 7 does not support SNI (Server Name Indication) there is some limitations on the used certificates when it + comes to support for HTTPS. + + This means that IIS 7 requires the use of wildcard certificates. + + + ### Certificate Stores: + ====================================================================================================================== + + By default StoreName.My is used when resolving certificates, but since IIS 8 allows us to use multiple stores, it + is possible to configure which stores to search when resolving certificates. + + To do this add a certificateStores configuration block as in the example: + + + + + + + A list of valid names can be found at: + https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.storename%28v=vs.110%29.aspx - + Using these settings allows some flexibility in the way IIS bindings are created by default. + --> + - @@ -72,10 +113,12 @@ + + diff --git a/Kudu.sln b/Kudu.sln index e157b9f37..d22c2d47e 100644 --- a/Kudu.sln +++ b/Kudu.sln @@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{627D3D .nuget\packages.config = .nuget\packages.config EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kudu.SiteManagement.Test", "Kudu.SiteManagement.Test\Kudu.SiteManagement.Test.csproj", "{2100FDEB-2259-4364-93F5-75FD5FC699F6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -194,6 +196,16 @@ Global {108774AC-560E-4E59-A602-80C9CBA906AE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {108774AC-560E-4E59-A602-80C9CBA906AE}.Release|Mixed Platforms.Build.0 = Release|Any CPU {108774AC-560E-4E59-A602-80C9CBA906AE}.Release|x86.ActiveCfg = Release|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Release|Any CPU.Build.0 = Release|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2100FDEB-2259-4364-93F5-75FD5FC699F6}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -205,5 +217,6 @@ Global {ACF3450A-8062-48D5-9C9D-8486261F290F} = {FA1EA7CE-E6A4-4E55-AD76-6B43572A9092} {5DEE4A10-5CA1-484D-B7A3-64C972EA2546} = {FA1EA7CE-E6A4-4E55-AD76-6B43572A9092} {108774AC-560E-4E59-A602-80C9CBA906AE} = {FA1EA7CE-E6A4-4E55-AD76-6B43572A9092} + {2100FDEB-2259-4364-93F5-75FD5FC699F6} = {FA1EA7CE-E6A4-4E55-AD76-6B43572A9092} EndGlobalSection EndGlobal