Search code examples
wcfservicehost

Programmatically configure ServiceHost endpoints with HTTPS?


I'm using Fileless Activation, here is my full web.config on the server side, which has two endpoints:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="entityFramework" 
             type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
             requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="RedStripe"
         connectionString="Data Source=S964;Initial Catalog=MyDatabase;Persist Security Info=True;User ID=sa;Password=***;MultipleActiveResultSets=True"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
  <system.web>
    <customErrors mode="Off"/>
  </system.web>
  <system.serviceModel>
    <serviceHostingEnvironment>
      <!-- where virtual .svc files are defined -->
      <serviceActivations>     
        <add service="Company.Project.Business.Services.AccountClassService" 
             relativeAddress="Account/AccountClassService.svc" 
             factory="Company.Project.WebHost.CustomServiceHostFactory"/>

        <add service="Company.Project.Business.Services.AccountService"
             relativeAddress="Account/AccountService.svc"
             factory="Company.Project.WebHost.CustomServiceHostFactory"/>

      </serviceActivations>
    </serviceHostingEnvironment>
  </system.serviceModel>
</configuration>

Here is my CustomServiceHostFactory:

public class CustomServiceHostFactory : ServiceHostFactory
{
    protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
        return new CustomServiceHost(serviceType, baseAddresses);
    }
}

And here is my CustomServiceHost:

public class CustomServiceHost : ServiceHost
{        
    public CustomServiceHost(Type serviceType, Uri[] baseAddresses)
        : base(serviceType, baseAddresses)
    {
    }

    protected override void InitializeRuntime()
    {
        AddServiceDebugBehavior();
        AddWcfMessageLoggingBehavior();
        AddGlobalErrorHandlingBehavior();
        AddServiceCredentialBehavior();
        AddEndpoints();
        ConfigureThrottling();
        base.InitializeRuntime();
    }

    private void AddEndpoints()
    {
        var wsHttpBinding = WcfHelpers.ConfigureWsHttpBinding();

        foreach (Uri address in BaseAddresses)
        {
            var endpoint = new ServiceEndpoint(
                ContractDescription.GetContract(Description.ServiceType),
                wsHttpBinding, new EndpointAddress(address));

            AddServiceEndpoint(endpoint);

            //adding mex
            AddServiceMetadataBehavior();
            AddServiceEndpoint(
                ServiceMetadataBehavior.MexContractName,
                MetadataExchangeBindings.CreateMexHttpBinding(),
                address.AbsoluteUri + "/mex");

            break;
        }
    }
    private void AddGlobalErrorHandlingBehavior()
    {
        var errorHanlderBehavior = Description.Behaviors.Find<GlobalErrorBehaviorAttribute>();

        if (errorHanlderBehavior == null)
        {
            Description.Behaviors.Add(new GlobalErrorBehaviorAttribute(typeof(GlobalErrorHandler)));
        }
    }

    private void AddServiceCredentialBehavior()
    {
        var credentialBehavior = Description.Behaviors.Find<ServiceCredentials>();

        if (credentialBehavior == null)
        {
            var customAuthenticationBehavior = new ServiceCredentials();
            customAuthenticationBehavior.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
            customAuthenticationBehavior.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNamePasswordValidator();
            Description.Behaviors.Add(customAuthenticationBehavior);
        }
    }
    private void AddServiceDebugBehavior()
    {
        var debugBehavior = Description.Behaviors.Find<ServiceDebugBehavior>();

        if (debugBehavior == null)
        {
            Description.Behaviors.Add(
                new ServiceDebugBehavior() {IncludeExceptionDetailInFaults = true});
        }
        else
        {
            if (!debugBehavior.IncludeExceptionDetailInFaults)
                debugBehavior.IncludeExceptionDetailInFaults = true;
        }
    }
    private void AddServiceMetadataBehavior()
    {
        var metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>();

        if (metadataBehavior == null)
        {
            ServiceMetadataBehavior serviceMetadataBehavior = new ServiceMetadataBehavior();
            serviceMetadataBehavior.HttpsGetEnabled = true;
            Description.Behaviors.Add(serviceMetadataBehavior);
        }
    }
    private void AddWcfMessageLoggingBehavior()
    {
        var messageInspectorBehavior = Description.Behaviors.Find<WcfMessageInspector>();

        if (messageInspectorBehavior == null)
        {
            Description.Behaviors.Add(new WcfMessageInspector());
        }
    }
    private void ConfigureThrottling()
    {
        var throttleBehavior = Description.Behaviors.Find<ServiceThrottlingBehavior>();

        if (throttleBehavior != null) return;

        throttleBehavior = new ServiceThrottlingBehavior
        {
            MaxConcurrentCalls = 100,
            MaxConcurrentInstances = 100,
            MaxConcurrentSessions = 100
        };

        Description.Behaviors.Add(throttleBehavior);
    }
}

Finally here is the WcfHelper where the binding is defined. This is in a shared location so I can programmatically configure the client side binding using the same:

public class WcfHelpers
{
    public static WSHttpBinding ConfigureWsHttpBinding()
    {
        return new WSHttpBinding
        {
            Name = "myWSHttpBinding",                
            OpenTimeout = new TimeSpan(0, 10, 0),
            CloseTimeout = new TimeSpan(0, 10, 0),
            SendTimeout = new TimeSpan(0, 10, 0),
            MaxBufferPoolSize = 104857600,
            MaxReceivedMessageSize = 104857600,
            Namespace = Constants.RedStripeNamespace,
            ReaderQuotas = new XmlDictionaryReaderQuotas()
            {
                MaxDepth = 104857600,
                MaxStringContentLength = 104857600,
                MaxArrayLength = 104857600,
                MaxBytesPerRead = 104857600,
                MaxNameTableCharCount = 104857600
            },
            Security =
            {
                Mode = SecurityMode.TransportWithMessageCredential,
                Message = { ClientCredentialType = MessageCredentialType.UserName }
            }
        };

    }
}

When I publish this WebHost project and try to browse to one of the two addreses like so: https://myserver/Project/Account/AccountService.svc I get the following error:

The provided URI scheme 'http' is invalid; expected 'https'. Parameter name: context.ListenUriBaseAddress

I notice that in the CustomServiceHost AddEndpoints() method, when looping over BaseAddresses, if I hardcode an address there like so: https://myserver/Project/Account/AccountService.svc I can then browse to it successfully. How do the BaseAddresses get built when using fileless activation and relative addressing? Where can I specify they use https (where it seems they are using http now)?

Thanks in advance.


Edit 1: This will fix the problem but seems like a total hack, where do I specify https using fileless activation so the relative address builds with https?

var endpoint = new ServiceEndpoint(ContractDescription.GetContract(Description.ServiceType),
wsHttpBinding, new EndpointAddress(address.OriginalString.Replace("http:", "https:")));

Edit 2: I think I'm gaining an understanding of what is going on here. Thank you @Andreas K for pointing me in the right direction. If I go into IIS and look at the bindings for the site, there are multiple as indicated by the image:enter image description here

I put some code to write to a database inside my AddEndpoints() method when looping over BaseAddresses. When I try to use the browser to get to the service like so: https://my.server.local/Project/Account/AccountService.svc, TWO entries are created in the database. http://my.server.local/Project/Account/AccountService.svc https://my.server.local/Project/Account/AccountService.svc

Thus, it seems the IIS SITE BINDING is being picked up. However, now I'm not sure why there aren't more entries in the database for the BaseAddresses. Where are the net.pipe, net.tcp, etc?


Solution

  • It turns out the BaseAddresses come from the IIS binding as mentioned in my Update 2, and again thanks to @Andreas K for pointing me to the right direction. In IIS I have one website with multiple applications under it. I have both http and https enabled on those bindings. I have updated my AddEndpoings() method in the CustomServiceHost to look like this:

    private void AddEndpoints()
    {
        var wsHttpBinding = WcfHelpers.ConfigureWsHttpBinding();
    
        foreach (var address in BaseAddresses.Where(a => a.Scheme == Uri.UriSchemeHttps))
        {
            var endpoint = new ServiceEndpoint(
                ContractDescription.GetContract(Description.ServiceType),
                wsHttpBinding, 
                new EndpointAddress(address));
    
            AddServiceEndpoint(endpoint);
            AddServiceMetadataBehavior();
        }
    }
    

    Since other applications under the site need http, my BaseAddresses always contains two (http and https). I needed to manually filter the http ones since I don't want to expose them for this particular site. Now that I know HOW they are being populated I am satisfied. Thanks all.