< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Certificates.ClientCertificateValidationCompiler
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Certificates/ClientCertificateValidationCompiler.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
97%
Covered lines: 122
Uncovered lines: 3
Coverable lines: 125
Total lines: 255
Line coverage: 97.6%
Branch coverage
87%
Covered branches: 28
Total branches: 32
Branch coverage: 87.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 01/21/2026 - 17:07:46 Line coverage: 97.6% (122/125) Branch coverage: 87.5% (28/32) Total lines: 255 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36 01/21/2026 - 17:07:46 Line coverage: 97.6% (122/125) Branch coverage: 87.5% (28/32) Total lines: 255 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Compile(...)100%44100%
BuildCacheKey(...)100%11100%
CompileCore(...)100%44100%
CompileCSharp(...)100%11100%
CompileVbNet(...)100%11100%
BuildMetadataReferences(...)87.5%88100%
SafeHasLocation(...)100%2260%
LoadCallbackDelegate(...)50%22100%
ThrowIfErrors(...)83.33%66100%
GetStartLine(...)83.33%6687.5%
WrapCSharp(...)100%11100%
WrapVbNet(...)100%11100%
Indent(...)100%11100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Certificates/ClientCertificateValidationCompiler.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Collections.Immutable;
 3using System.Net.Security;
 4using System.Reflection;
 5using System.Security.Cryptography.X509Certificates;
 6using System.Text;
 7using Kestrun.Hosting;
 8using Kestrun.Scripting;
 9using Microsoft.CodeAnalysis;
 10using Microsoft.CodeAnalysis.CSharp;
 11using Microsoft.CodeAnalysis.VisualBasic;
 12
 13namespace Kestrun.Certificates;
 14
 15/// <summary>
 16/// Compiles C# or VB.NET code into a TLS client certificate validation callback.
 17/// </summary>
 18/// <remarks>
 19/// This is intended for advanced scenarios where a pure .NET delegate is required (e.g. Kestrel TLS handshake callbacks
 20/// The compiled delegate executes inside the Kestrel TLS handshake path, so it must be fast and thread-safe.
 21/// </remarks>
 22public static class ClientCertificateValidationCompiler
 23{
 124    private static readonly ConcurrentDictionary<string, Lazy<Func<X509Certificate2, X509Chain, SslPolicyErrors, bool>>>
 25
 26    /// <summary>
 27    /// Compiles code into a TLS client certificate validation callback.
 28    /// </summary>
 29    /// <param name="host">The Kestrun host (used for logging).</param>
 30    /// <param name="code">
 31    /// The code that forms the body of a method returning <c>bool</c>.
 32    /// The method signature is:
 33    /// <c>bool Validate(X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)</c>.
 34    /// </param>
 35    /// <param name="language">The language used for <paramref name="code"/>.</param>
 36    /// <returns>A compiled callback delegate.</returns>
 37    public static Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> Compile(
 38        KestrunHost host,
 39        string code,
 40        ScriptLanguage language = ScriptLanguage.CSharp)
 41    {
 1142        ArgumentNullException.ThrowIfNull(host);
 1043        if (string.IsNullOrWhiteSpace(code))
 44        {
 445            throw new ArgumentNullException(nameof(code), "Client certificate validation code cannot be null or whitespa
 46        }
 47
 648        var cacheKey = BuildCacheKey(language, code);
 1149        var lazy = Cache.GetOrAdd(cacheKey, _ => new Lazy<Func<X509Certificate2, X509Chain, SslPolicyErrors, bool>>(
 1650            () => CompileCore(host, code, language), isThreadSafe: true));
 51
 652        return lazy.Value;
 53    }
 54
 55    private static string BuildCacheKey(ScriptLanguage language, string code)
 656        => ((int)language).ToString(System.Globalization.CultureInfo.InvariantCulture) + ":" + code;
 57
 58    private static Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> CompileCore(
 59        KestrunHost host,
 60        string code,
 61        ScriptLanguage language)
 62    {
 563        return language switch
 564        {
 365            ScriptLanguage.CSharp => CompileCSharp(host, code),
 166            ScriptLanguage.VBNet => CompileVbNet(host, code),
 167            _ => throw new NotSupportedException($"ClientCertificateValidation supports only CSharp and VBNet, not {lang
 568        };
 69    }
 70
 71    private static Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> CompileCSharp(KestrunHost host, string code)
 72    {
 373        var source = WrapCSharp(code);
 374        var startLine = GetStartLine(source, "// ---- User code starts here ----");
 75
 376        var parseOptions = new CSharpParseOptions(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp12);
 377        var tree = CSharpSyntaxTree.ParseText(source, parseOptions);
 78
 379        var refs = BuildMetadataReferences(includeVisualBasicRuntime: false);
 380        var compilation = CSharpCompilation.Create(
 381            assemblyName: $"TlsClientCertValidation_{Guid.NewGuid():N}",
 382            syntaxTrees: [tree],
 383            references: refs,
 384            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
 85
 386        using var ms = new MemoryStream();
 387        var emit = compilation.Emit(ms);
 388        ThrowIfErrors(emit.Diagnostics, startLine, host, languageLabel: "C#");
 89
 290        ms.Position = 0;
 291        return LoadCallbackDelegate(ms.ToArray(), typeName: "ClientCertValidationScript", methodName: "Validate");
 292    }
 93
 94    private static Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> CompileVbNet(KestrunHost host, string code)
 95    {
 196        var source = WrapVbNet(code);
 197        var startLine = GetStartLine(source, "' ---- User code starts here ----");
 98
 199        var parseOptions = new VisualBasicParseOptions(Microsoft.CodeAnalysis.VisualBasic.LanguageVersion.VisualBasic16_
 1100        var tree = VisualBasicSyntaxTree.ParseText(source, parseOptions);
 101
 1102        var refs = BuildMetadataReferences(includeVisualBasicRuntime: true);
 1103        var compilation = VisualBasicCompilation.Create(
 1104            assemblyName: $"TlsClientCertValidation_{Guid.NewGuid():N}",
 1105            syntaxTrees: [tree],
 1106            references: refs,
 1107            options: new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
 108
 1109        using var ms = new MemoryStream();
 1110        var emit = compilation.Emit(ms);
 1111        ThrowIfErrors(emit.Diagnostics, startLine, host, languageLabel: "VB.NET");
 112
 1113        ms.Position = 0;
 1114        return LoadCallbackDelegate(ms.ToArray(), typeName: "ClientCertValidationScript", methodName: "Validate");
 1115    }
 116
 117    private static IEnumerable<MetadataReference> BuildMetadataReferences(bool includeVisualBasicRuntime)
 118    {
 119        // Baseline references (includes X509 and many common assemblies)
 4120        var baseRefs = DelegateBuilder.BuildBaselineReferences();
 121
 122        // Ensure the assembly containing SslPolicyErrors is referenced.
 4123        var netSecurityAsm = typeof(SslPolicyErrors).Assembly;
 4124        var netSecurityRef = string.IsNullOrWhiteSpace(netSecurityAsm.Location)
 4125            ? null
 4126            : MetadataReference.CreateFromFile(netSecurityAsm.Location);
 127
 128        // Add already-loaded assemblies to improve binding for "using" namespaces.
 4129        var loaded = AppDomain.CurrentDomain.GetAssemblies()
 940130            .Where(a => !a.IsDynamic && SafeHasLocation(a))
 919131            .Select(a => MetadataReference.CreateFromFile(a.Location));
 132
 4133        IEnumerable<MetadataReference> refs = baseRefs;
 4134        if (netSecurityRef is not null)
 135        {
 4136            refs = refs.Append(netSecurityRef);
 137        }
 138
 4139        refs = refs.Concat(loaded);
 140
 4141        if (includeVisualBasicRuntime)
 142        {
 1143            refs = refs.Append(MetadataReference.CreateFromFile(typeof(Microsoft.VisualBasic.Constants).Assembly.Locatio
 144        }
 145
 4146        return refs;
 147    }
 148
 149    private static bool SafeHasLocation(Assembly a)
 150    {
 151        try
 152        {
 929153            var loc = a.Location;
 929154            return !string.IsNullOrEmpty(loc) && File.Exists(loc);
 155        }
 0156        catch
 157        {
 0158            return false;
 159        }
 929160    }
 161
 162    private static Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> LoadCallbackDelegate(byte[] asmBytes, string
 163    {
 3164        var asm = Assembly.Load(asmBytes);
 3165        var method = asm.GetType(typeName, throwOnError: true)!
 3166            .GetMethod(methodName, BindingFlags.Public | BindingFlags.Static)
 3167            ?? throw new MissingMethodException(typeName, methodName);
 168
 3169        return (Func<X509Certificate2, X509Chain, SslPolicyErrors, bool>)method
 3170            .CreateDelegate(typeof(Func<X509Certificate2, X509Chain, SslPolicyErrors, bool>));
 171    }
 172
 173    private static void ThrowIfErrors(ImmutableArray<Diagnostic> diagnostics, int startLine, KestrunHost host, string la
 174    {
 8175        var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
 4176        if (errors.Length == 0)
 177        {
 3178            return;
 179        }
 180
 1181        host.Logger.Error("{Lang} client certificate validation compilation completed with {Count} error(s).", languageL
 182
 1183        var sb = new StringBuilder();
 1184        _ = sb.AppendLine($"{languageLabel} client certificate validation compilation failed:");
 4185        foreach (var error in errors)
 186        {
 1187            var location = error.Location.IsInSource
 1188                ? $" at line {error.Location.GetLineSpan().StartLinePosition.Line - startLine + 1}"
 1189                : string.Empty;
 1190            var msg = $"  Error [{error.Id}]: {error.GetMessage()}{location}";
 1191            host.Logger.Error(msg);
 1192            _ = sb.AppendLine(msg);
 193        }
 194
 1195        throw new CompilationErrorException(sb.ToString().TrimEnd(), diagnostics);
 196    }
 197
 198    private static int GetStartLine(string source, string marker)
 199    {
 4200        var idx = source.IndexOf(marker, StringComparison.Ordinal);
 4201        if (idx < 0)
 202        {
 0203            return 0;
 204        }
 205
 4206        var line = 0;
 2180207        for (var i = 0; i < idx; i++)
 208        {
 1086209            if (source[i] == '\n')
 210            {
 30211                line++;
 212            }
 213        }
 214
 4215        return line;
 216    }
 217
 218    private static string WrapCSharp(string code)
 3219        => $$"""
 3220using System;
 3221using System.Net.Security;
 3222using System.Security.Cryptography.X509Certificates;
 3223
 3224public static class ClientCertValidationScript
 3225{
 3226    public static bool Validate(X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
 3227    {
 3228        // ---- User code starts here ----
 3229{{Indent(code, 8)}}
 3230    }
 3231}
 3232""";
 233
 234    private static string WrapVbNet(string code)
 1235        => $$"""
 1236Imports System
 1237Imports System.Net.Security
 1238Imports System.Security.Cryptography.X509Certificates
 1239
 1240Public Module ClientCertValidationScript
 1241    Public Function Validate(certificate As X509Certificate2, chain As X509Chain, sslPolicyErrors As SslPolicyErrors) As
 1242        ' ---- User code starts here ----
 1243{{Indent(code, 8)}}
 1244    End Function
 1245End Module
 1246""";
 247
 248    private static string Indent(string code, int spaces)
 249    {
 4250        var pad = new string(' ', spaces);
 4251        var lines = code.Replace("\r\n", "\n", StringComparison.Ordinal).Replace("\r", "\n", StringComparison.Ordinal)
 4252            .Split('\n');
 8253        return string.Join("\n", lines.Select(l => pad + l));
 254    }
 255}