Skip to content

Commit

Permalink
Add better support for Certificates (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
sevensolutions authored Oct 5, 2024
1 parent 52e9845 commit e2796bb
Show file tree
Hide file tree
Showing 16 changed files with 826 additions and 108 deletions.
8 changes: 8 additions & 0 deletions src/NomadIIS.Tests/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage( "Interoperability", "CA1416:Plattformkompatibilität überprüfen" )]
30 changes: 30 additions & 0 deletions src/NomadIIS.Tests/IisHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ public void ShouldNotExist ()
Assert.Fail( $"Website with name \"{_name}\" exists but shouldn't." );
}

public IisWebsiteBindingHandle Binding ( int index )
=> new IisWebsiteBindingHandle( GetWebsite().Bindings[index] );

private Site GetWebsite ()
{
var website = FindWebsite();
Expand All @@ -132,3 +135,30 @@ private Site GetWebsite ()
private Site? FindWebsite ()
=> _owner.ServerManager.Sites.FirstOrDefault( x => x.Name == _name );
}

public sealed class IisWebsiteBindingHandle
{
private readonly Binding _binding;

public IisWebsiteBindingHandle ( Binding binding )
{
_binding = binding;
}

public void IsHttps ()
{
if ( _binding.Protocol is null || _binding.Protocol != "https" )
Assert.Fail( "Binding is not https but should be." );
}

public void CertificateThumbprintIs ( string certificateThumbprint )
{
if ( _binding.CertificateHash is null || _binding.CertificateHash.Length == 0 )
Assert.Fail( "The binding has no certificate set." );

var tp = Convert.ToHexString( _binding.CertificateHash );

if ( !string.Equals( tp, certificateThumbprint, StringComparison.InvariantCultureIgnoreCase ) )
Assert.Fail( $"Certificate hash should be {certificateThumbprint}, but is {tp}." );
}
}
97 changes: 97 additions & 0 deletions src/NomadIIS.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using Microsoft.Web.Administration;
using System;
using System.IO;
using Xunit.Abstractions;
using NomadIIS.Services;
using System.Linq;

namespace NomadIIS.Tests;

Expand Down Expand Up @@ -222,6 +225,100 @@ public async Task JobWithSettings_PoolShouldHaveSettings ()
_output.WriteLine( "Job stopped." );
}

[Fact]
public async Task JobWithCertificateFile_WebsiteShouldUseCertificate ()
{
var certificateFile = Path.GetTempFileName() + ".pfx";

var certificateThumbprint = CertificateHelper.GenerateSelfSignedCertificate(
"NomadIISTest", TimeSpan.FromDays( 2 ), certificateFile, "super#secure" );

var jobHcl = $$"""
job "https-job-with-cert-file" {
datacenters = ["dc1"]
type = "service"

group "app" {
count = 1

network {
port "httplabel" {}
}

task "app" {
driver = "iis"

config {
application {
path = "C:\\inetpub\\wwwroot"
}

binding {
type = "https"
port = "httplabel"

certificate {
file = "{{certificateFile.Replace( "\\", "\\\\" )}}"
password = "super#secure"
}
}
}
}
}
}
""";

_output.WriteLine( "Submitting job..." );

var jobId = await _fixture.ScheduleJobAsync( jobHcl );

_output.WriteLine( $"Job Id: {jobId}" );

var allocations = await _fixture.ListJobAllocationsAsync( jobId );

if ( allocations is null || allocations.Length == 0 )
Assert.Fail( "No job allocations" );

var poolAndWebsiteName = $"nomad-{allocations[0].Id}-app";

_output.WriteLine( $"AppPool and Website Name: {poolAndWebsiteName}" );

_fixture.AccessIIS( iis =>
{
iis.AppPool( poolAndWebsiteName ).ShouldExist();
iis.Website( poolAndWebsiteName ).ShouldExist();
iis.Website( poolAndWebsiteName ).Binding( 0 ).IsHttps();
iis.Website( poolAndWebsiteName ).Binding( 0 ).CertificateThumbprintIs( certificateThumbprint );
} );

var allocation = await _fixture.ReadAllocationAsync( allocations[0].Id );

Assert.NotNull( allocation );

var appPort = allocation.Resources.Networks[0].DynamicPorts.First( x => x.Label == "httplabel" ).Value;

var serverCertificate = _fixture.GetServerCertificate( "localhost", appPort );

Assert.NotNull( serverCertificate );

Assert.Equal( "CN=NomadIISTest", serverCertificate.Subject );

_output.WriteLine( "Stopping job..." );

await _fixture.StopJobAsync( jobId );

_output.WriteLine( "Job stopped." );

_fixture.AccessIIS( iis =>
{
iis.AppPool( poolAndWebsiteName ).ShouldNotExist();
iis.Website( poolAndWebsiteName ).ShouldNotExist();
} );

// TODO: Certificate should have been removed
}

#if MANAGEMENT_API
[Fact]
public async Task ManagementApi_TakeScreenshot ()
Expand Down
31 changes: 31 additions & 0 deletions src/NomadIIS.Tests/NomadApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,34 @@ public enum JobStatus
[JsonPropertyName( "dead" )]
Dead
}

public sealed class AllocationResponse
{
[JsonPropertyName( "ID" )]
public string Id { get; set; } = default!;
[JsonPropertyName( "Name" )]
public string Name { get; set; } = default!;
[JsonPropertyName( "Resources" )]
public AllocationResources Resources { get; set; } = default!;
}
public sealed class AllocationResources
{
[JsonPropertyName( "Networks" )]
public AllocationNetwork[] Networks { get; set; } = default!;
}
public sealed class AllocationNetwork
{
[JsonPropertyName( "DynamicPorts" )]
public NetworkDynamicPort[] DynamicPorts { get; set; } = default!;
}
public sealed class NetworkDynamicPort
{
[JsonPropertyName( "Label" )]
public string Label { get; set; } = default!;
[JsonPropertyName( "Value" )]
public int Value { get; set; } = default!;
[JsonPropertyName( "To" )]
public int To { get; set; } = default!;
[JsonPropertyName( "HostNetwork" )]
public string HostNetwork { get; set; } = default!;
}
49 changes: 42 additions & 7 deletions src/NomadIIS.Tests/NomadIISFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;

Expand Down Expand Up @@ -53,7 +56,7 @@ public async Task InitializeAsync ()
pluginDir = @"..\..\..\..\NomadIIS\bin\Debug\net8.0";

var pluginDirectory = Path.GetFullPath( pluginDir );

#if MANAGEMENT_API
var configFile = Path.GetFullPath( @"Data\configs\with_api.hcl" );
#else
Expand Down Expand Up @@ -177,23 +180,36 @@ await TryUntilAsync( async () =>
return null;
} );

// Wait a bit to let the task stabilize
await Task.Delay( 3000 );
}

return jobId;
}

public async Task StopJobAsync ( string jobId )
{
await _httpClient.DeleteAsync( $"job/{jobId}" );
await _httpClient.DeleteAsync( $"job/{jobId}?purge=true" );

await TryUntilAsync( async () =>
await TryUntilAsync<bool?>( async () =>
{
var job = await ReadJobAsync( jobId );
try
{
var job = await ReadJobAsync( jobId );
if ( job is not null && job.Status == JobStatus.Dead )
return job;
if ( job is not null && job.Status == JobStatus.Dead )
return true;
return null;
return null;
}
catch ( HttpRequestException ex )
{
if ( ex.StatusCode == System.Net.HttpStatusCode.NotFound )
return true;
return null;
}
} );
}

Expand All @@ -203,13 +219,32 @@ await TryUntilAsync( async () =>
public Task<JobAllocationResponse[]?> ListJobAllocationsAsync ( string jobId )
=> _httpClient.GetFromJsonAsync<JobAllocationResponse[]>( $"job/{jobId}/allocations" );

public Task<AllocationResponse?> ReadAllocationAsync ( string allocId )
=> _httpClient.GetFromJsonAsync<AllocationResponse>( $"allocation/{allocId}" );

public void AccessIIS ( Action<IisHandle> action )
{
using var handle = new IisHandle();

action( handle );
}

public X509Certificate? GetServerCertificate ( string hostName, int port )
{
// Establish a TCP connection to the server
using var client = new TcpClient( hostName, port );

using var sslStream = new SslStream( client.GetStream(), false, ValidateServerCertificate, null );

// Initiate the SSL handshake
sslStream.AuthenticateAsClient( hostName );

// Get the server's certificate
return sslStream?.RemoteCertificate;

bool ValidateServerCertificate ( object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors ) => true;
}

#if MANAGEMENT_API
public async Task<byte[]> TakeScreenshotAsync ( string allocId, string taskName )
{
Expand Down
8 changes: 8 additions & 0 deletions src/NomadIIS/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage( "Interoperability", "CA1416:Plattformkompatibilität überprüfen" )]
2 changes: 0 additions & 2 deletions src/NomadIIS/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
using System.Net;
using System.Security.Principal;

#pragma warning disable CA1416 // Plattformkompatibilität überprüfen
using ( var identity = WindowsIdentity.GetCurrent() )
{
var principal = new WindowsPrincipal( identity );
Expand All @@ -27,7 +26,6 @@
return -1;
}
}
#pragma warning restore CA1416 // Plattformkompatibilität überprüfen

var excludeRouting = Matching.FromSource( "Microsoft.AspNetCore.Routing" );
var excludeHosting = Matching.FromSource( "Microsoft.AspNetCore.Hosting" );
Expand Down
Loading

0 comments on commit e2796bb

Please sign in to comment.