Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save MatthewDavidCampbell/b64fbc10becd87bb35c1dc81c2c0c96b to your computer and use it in GitHub Desktop.
Save MatthewDavidCampbell/b64fbc10becd87bb35c1dc81c2c0c96b to your computer and use it in GitHub Desktop.
Kestrel 3.1 Service Fabric Https certificate
<?xml version="1.0" encoding="utf-8"?>
<ApplicationManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ApplicationTypeName="Biometria.Produktion.PublicApi.ApplicationType" ApplicationTypeVersion="1.0.0" xmlns="http://schemas.microsoft.com/2011/01/fabric">
<Parameters>
<!-- Commented out parameters but MyCert would slide in here -->
</Parameters>
<!-- Import the ServiceManifest from the ServicePackage. The ServiceManifestName and ServiceManifestVersion
should match the Name and Version attributes of the ServiceManifest element defined in the
ServiceManifest.xml file. -->
<ServiceManifestImport>
<ServiceManifestRef ServiceManifestName="Gary.ServicePkg" ServiceManifestVersion="1.0.0" />
<ConfigOverrides>
<ConfigOverride Name="Config">
<Settings>
<!-- Commented out sections -->
</Settings>
</ConfigOverride>
</ConfigOverrides>
<EnvironmentOverrides CodePackageRef="Code">
<!-- Commented out environment variables -->
</EnvironmentOverrides>
<Policies>
<EndpointBindingPolicy EndpointRef="KestrelEndpointHttps" CertificateRef="MyCert" />
</Policies>
</ServiceManifestImport>
<DefaultServices>
<!-- The section below creates instances of service types, when an instance of this
application type is created. You can also create one or more instances of service type using the
ServiceFabric PowerShell module.
The attribute ServiceTypeName below must match the name defined in the imported ServiceManifest.xml file. -->
<Service Name="Gary.Service" ServicePackageActivationMode="ExclusiveProcess">
<StatelessService ServiceTypeName="Gary.ServiceType" InstanceCount="1">
<SingletonPartition />
</StatelessService>
</Service>
</DefaultServices>
<Certificates>
<EndpointCertificate X509FindValue="[MyCert]" Name="MyCert" />
</Certificates>
</ApplicationManifest>
using System;
using System.Fabric;
using System.Fabric.Description;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.ServiceFabric.Services.Communication.Runtime;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace Gary.Service
{
public class LocalKestrelCommunicationListener : ICommunicationListener
{
public LocalKestrelCommunicationListener(
StatelessServiceContext context,
Action<IWebHostBuilder> configureKestrel
)
{
(Context, ConfigureKestrel) = (context, configureKestrel);
}
public LocalKestrelCommunicationListener(
StatelessServiceContext context,
Action<IWebHostBuilder> configureKestrel,
string endpointName
)
{
(Context, ConfigureKestrel, EndpointName) = (context, configureKestrel, endpointName);
}
/// <summary>
/// Force stateless services
/// </summary>
protected StatelessServiceContext Context { get; }
/// <summary>
/// Additional Kestrel configuration (i.e. Startup class etc.)
/// </summary>
protected Action<IWebHostBuilder> ConfigureKestrel { get; }
/// <summary>
/// Specific endpoint name (likely from service manifest) otherwise empty
/// </summary>
protected string EndpointName { get; } = string.Empty;
/// <summary>
/// Host
/// </summary>
public IHost Host { get; set; }
/// <summary>
/// Hard-coded HTTP without TLS as protocol
/// </summary>
public EndpointProtocol[] EndpointProtocol { get; } = new [] { System.Fabric.Description.EndpointProtocol.Http, System.Fabric.Description.EndpointProtocol.Https }; // Remarks: Force HTTP without TLS
/// <summary>
/// HTTP endpoint description (i.e. protocol | port | etc.)
/// </summary>
public EndpointResourceDescription EndpointResourceDescription => Context
.CodePackageActivationContext
.GetEndpoints()
.Where(x => {
return string.IsNullOrEmpty(EndpointName) || EndpointName == x.Name;
})
.FirstOrDefault(x => EndpointProtocol.Contains(x.Protocol));
/// <summary>
/// Internal URL associated with the Endpoint description
/// </summary>
public string InternalUrl => EndpointResourceDescription is null ? "http://+:0" : $"{EndpointResourceDescription.Protocol}://+:{EndpointResourceDescription.Port}";
/// <summary>
/// TODO: Flex configuration giving listener PartitionId and ReplicaId as url suffix
/// </summary>
protected string UrlSuffix { get; private set; } = string.Empty;
/// <summary>
/// Delegates to close
/// </summary>
public void Abort()
{
CloseAsync(default!).Wait();
}
/// <summary>
/// Closes communication listener to a terminal state
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task CloseAsync(CancellationToken cancellationToken)
{
if (Host != null)
{
await Host.StopAsync(cancellationToken);
Host.Dispose();
}
}
/// <summary>
/// Open communication listener
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string> OpenAsync(CancellationToken cancellationToken)
{
var url = InternalUrl.ToLower();
var description = EndpointResourceDescription;
if (description is null)
{
throw new ApplicationException($"Missing endpoint [protocol: {EndpointProtocol}, name: {EndpointName}]");
}
Host = CreateHostBuilder(description, url).Build();
await Host.StartAsync(cancellationToken);
if (url.Contains("://+:"))
{
url = url.Replace("://+:", $"://{Context.PublishAddress}:");
}
else if (url.Contains("://[::]:"))
{
url = url.Replace("://[::]:", $"://{Context.PublishAddress}:");
}
return url.TrimEnd(new[] { '/' }) + this.UrlSuffix;
}
/// <summary>
/// Create Kestrel
/// </summary>
/// <param name="description"></param>
/// <param name="url"></param>
/// <returns></returns>
public IHostBuilder CreateHostBuilder(EndpointResourceDescription description, string url) => Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder(new string[] { })
.ConfigureWebHostDefaults(x => {
// See https://docs.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.0#call-insecure-grpc-services-with-net-core-client
x.ConfigureKestrel(options =>
{
var loggerFactory = options.ApplicationServices.GetService<ILoggerFactory>();
options.Listen(
IPAddress.IPv6Any,
description.Port,
listenOptions => {
if (description.Protocol == System.Fabric.Description.EndpointProtocol.Http)
{
listenOptions.Protocols = HttpProtocols.Http1;
}
if (description.Protocol == System.Fabric.Description.EndpointProtocol.Https)
{
var certificate = GetFabricCertificate(loggerFactory);
listenOptions.Protocols = HttpProtocols.Http2;
if (certificate != null) {
listenOptions.UseHttps(certificate);
}
}
}
);
});
x.UseUrls(url);
ConfigureKestrel(x);
});
public X509Certificate2 GetFabricCertificate(ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger<LocalKestrelCommunicationListener>();
using var storage = new X509Store(StoreName.My, StoreLocation.LocalMachine);
try
{
storage.Open(OpenFlags.ReadOnly);
var certificates = storage
.Certificates
.OfType<X509Certificate2>()
.Where(x => x.GetNameInfo(X509NameType.SimpleName, false).EndsWith(".se")); // TODO: Hard coded
foreach (var certificate in certificates)
{
logger.LogInformation($"Certificate [subject: {certificate.Subject}, thumbprint: {certificate.Thumbprint}, issuer: {certificate.Issuer}, friendly name: {certificate.FriendlyName}, version: {certificate.Version}]");
}
return certificates.LastOrDefault(); // TODO: Last always primary cert
} finally
{
storage.Close();
}
}
}
}
using Microsoft.AspNetCore.Hosting;
using Microsoft.ServiceFabric.Services.Communication.Runtime;
using Microsoft.ServiceFabric.Services.Runtime;
using System.Collections.Generic;
using System.Fabric;
using System.Fabric.Description;
using System.Linq;
namespace Gary.Service
{
internal sealed class PublicApiService : StatelessService
{
public PublicApiService(
StatelessServiceContext context
) : base(context)
{
}
protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
{
var endpoints = Context
.CodePackageActivationContext
.GetEndpoints()
.Where(endpoint => endpoint.Protocol == EndpointProtocol.Http || endpoint.Protocol == EndpointProtocol.Https)
.Select(endpoint => endpoint.Name);
return endpoints.Select(endpoint => new ServiceInstanceListener(context =>
new LocalKestrelCommunicationListener(
context,
(builder) => builder
.ConfigureAppConfiguration(ConfigurationManager.BuildConfigurationAction)
.UseStartup<Startup>(),
endpoint
),
endpoint
));
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<ServiceManifest Name="Gary.ServicePkg"
Version="1.0.0"
xmlns="http://schemas.microsoft.com/2011/01/fabric"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ServiceTypes>
<!-- This is the name of your ServiceType.
This name must match the string used in RegisterServiceType call in Program.cs. -->
<StatelessServiceType ServiceTypeName="Gary.ServiceType" />
</ServiceTypes>
<!-- Code package is your service executable. -->
<CodePackage Name="Code" Version="1.0.0">
<EntryPoint>
<ExeHost>
<Program>Biometria.Produktion.PublicApi.Service.exe</Program>
<WorkingFolder>CodePackage</WorkingFolder>
</ExeHost>
</EntryPoint>
<EnvironmentVariables>
<EnvironmentVariable Name="ASPNETCORE_ENVIRONMENT" Value=""/>
<EnvironmentVariable Name="AzureServicesAuthConnectionString" Value=""/>
</EnvironmentVariables>
</CodePackage>
<!-- Config package is the contents of the Config directoy under PackageRoot that contains an
independently-updateable and versioned set of custom configuration settings for your service. -->
<ConfigPackage Name="Config" Version="1.0.0" />
<Resources>
<Endpoints>
<!-- This endpoint is used by the communication listener to obtain the port on which to
listen. Please note that if your service is partitioned, this port is shared with
replicas of different partitions that are placed in your code. -->
<Endpoint Protocol="http" Name="KestrelEndpoint" Type="Input" Port="8401" />
<Endpoint Protocol="https" Name="KestrelEndpointHttps" Type="Input" Port="443" />
</Endpoints>
</Resources>
</ServiceManifest>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment