File: UserCallbacks\HandshakeUserCallback.cs
Web Access
Project: src\aspnetcore\src\Servers\Kestrel\Transport.DirectTls\src\Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.csproj (Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.Connection;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.DirectTls.UserCallbacks;
 
/// <summary>
/// One suspended handshake's user code, executed on the thread pool instead of on the pump (epoll) thread.
/// </summary>
/// <remarks>
/// User-supplied handshake callbacks - the ClientHello listener, the server-certificate selector, and the
/// client-certificate validation callback - can block for an arbitrarily long time (a disk read, a key vault
/// round trip, a lock). A pump thread owns accept plus all I/O readiness for every connection assigned to it,
/// so running any of them inline stalls every one of those connections. Instead the pump parks the handshake
/// (de-registering its fd from the epoll set so it cannot generate pump work while parked) and queues this
/// work item. Everything a derived item touches was copied out of the session on the pump thread beforehand:
/// it never calls into <c>TlsSocketSession</c>, which stays single-threaded and owned by its pump. When the
/// user code returns - or throws - the result is handed back to the owning pump through
/// <see cref="TlsEventPump.CompleteUserCallback"/>, which resumes the handshake on the pump thread.
/// <para>
/// Each suspension point has its own derived type carrying only its own state, so the pump resumes by
/// switching on the work item's type. <see cref="Execute"/> is deliberately not virtual: the try/catch/finally
/// it wraps every callback in is what guarantees that a suspended handshake reports back exactly once, whether
/// the user code returns or throws, so a derived type must not be able to replace it.
/// </para>
/// </remarks>
internal abstract class HandshakeUserCallback : IThreadPoolWorkItem
{
    private readonly TlsEventPump _pump;
 
    protected HandshakeUserCallback(TlsEventPump pump, int fd, DirectTlsConnection? connection)
    {
        _pump = pump;
        Fd = fd;
        Connection = connection;
    }
 
    /// <summary>The handshaking file descriptor this callback belongs to.</summary>
    public int Fd { get; }
 
    /// <summary>
    /// The connection allocated early (at <c>NeedsTlsContext</c>) so user code sees a stable
    /// <see cref="ConnectionContext"/>. Null when nothing needed one that early: the pump resolved the TLS
    /// context inline because no user code runs at <c>NeedsTlsContext</c> (so only the client-certificate
    /// suspension is reachable, and it does not use this), or the pump has no memory pool (tests). In both
    /// cases the connection is allocated when the handshake completes instead.
    /// </summary>
    public DirectTlsConnection? Connection { get; }
 
    /// <summary>The exception the user code threw, if any. Non-null means the pump drops the connection.</summary>
    public Exception? Failure { get; private set; }
 
    /// <inheritdoc />
    public void Execute()
    {
        try
        {
            RunUserCode();
        }
        catch (Exception ex)
        {
            // A throwing user callback (or a selector that resolved no certificate) fails this one connection.
            // The pump logs it and drops the handshake when it picks the result up; it must never escape onto
            // a thread pool thread, where it would tear the process down.
            Failure = ex;
        }
        finally
        {
            ReleaseTransientState();
 
            // Hand the result back to the owning pump. Nothing here may touch the session, the epoll set, or
            // the handshake bookkeeping - those are pump-thread-only.
            _pump.CompleteUserCallback(this);
        }
    }
 
    /// <summary>
    /// Runs the endpoint-supplied callback on the thread pool and records its result on this instance. Any
    /// throw is captured by <see cref="Execute"/> as <see cref="Failure"/>.
    /// </summary>
    protected abstract void RunUserCode();
 
    /// <summary>
    /// Releases anything borrowed for the duration of the callback, whether it returned or threw. Runs before
    /// the result is handed back to the pump.
    /// </summary>
    protected virtual void ReleaseTransientState()
    {
    }
}