diff --git a/src/Servers/Connections.Abstractions/src/Features/IConnectionSocketFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IConnectionSocketFeature.cs new file mode 100644 index 000000000000..1b0a52507b6d --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/IConnectionSocketFeature.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Sockets; + +namespace Microsoft.AspNetCore.Connections.Features +{ + /// + /// Provides access to the connection's underlying . + /// + public interface IConnectionSocketFeature + { + /// + /// Gets the underlying . + /// + Socket Socket { get; } + } +} diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt index 575c538f6a9b..9d21a5e74882 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,5 +1,7 @@ #nullable enable *REMOVED*Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature +Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature.Socket.get -> System.Net.Sockets.Socket! Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Connections.Experimental.IMultiplexedConnectionBuilder Microsoft.AspNetCore.Connections.Experimental.IMultiplexedConnectionBuilder.ApplicationServices.get -> System.IServiceProvider! diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.FeatureCollection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.FeatureCollection.cs new file mode 100644 index 000000000000..8645f7e6459c --- /dev/null +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.FeatureCollection.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Sockets; +using Microsoft.AspNetCore.Connections.Features; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal +{ + internal sealed partial class SocketConnection : IConnectionSocketFeature + { + public Socket Socket => _socket; + + private void InitiaizeFeatures() + { + _currentIConnectionSocketFeature = this; + } + } +} diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs index 49f6c24f6446..35b7c66113ec 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { - internal sealed class SocketConnection : TransportConnection + internal sealed partial class SocketConnection : TransportConnection { private static readonly int MinAllocBufferSize = SlabMemoryPool.BlockSize / 2; @@ -70,6 +70,8 @@ internal SocketConnection(Socket socket, // Set the transport and connection id Transport = _originalTransport = pair.Transport; Application = pair.Application; + + InitiaizeFeatures(); } public PipeWriter Input => Application.Output; diff --git a/src/Servers/Kestrel/shared/TransportConnection.Generated.cs b/src/Servers/Kestrel/shared/TransportConnection.Generated.cs index 0f32b7d54549..18b69ce0e46d 100644 --- a/src/Servers/Kestrel/shared/TransportConnection.Generated.cs +++ b/src/Servers/Kestrel/shared/TransportConnection.Generated.cs @@ -27,6 +27,9 @@ internal partial class TransportConnection : IFeatureCollection, internal protected IMemoryPoolFeature? _currentIMemoryPoolFeature; internal protected IConnectionLifetimeFeature? _currentIConnectionLifetimeFeature; + // Other reserved feature slots + internal protected IConnectionSocketFeature? _currentIConnectionSocketFeature; + private int _featureRevision; private List>? MaybeExtra; @@ -39,6 +42,7 @@ private void FastReset() _currentIMemoryPoolFeature = this; _currentIConnectionLifetimeFeature = this; + _currentIConnectionSocketFeature = null; } // Internal for testing @@ -130,6 +134,10 @@ private void ExtraFeatureSet(Type key, object? value) { feature = _currentIConnectionLifetimeFeature; } + else if (key == typeof(IConnectionSocketFeature)) + { + feature = _currentIConnectionSocketFeature; + } else if (MaybeExtra != null) { feature = ExtraFeatureGet(key); @@ -162,6 +170,10 @@ private void ExtraFeatureSet(Type key, object? value) { _currentIConnectionLifetimeFeature = (IConnectionLifetimeFeature?)value; } + else if (key == typeof(IConnectionSocketFeature)) + { + _currentIConnectionSocketFeature = (IConnectionSocketFeature?)value; + } else { ExtraFeatureSet(key, value); @@ -196,6 +208,10 @@ private void ExtraFeatureSet(Type key, object? value) { feature = Unsafe.As(ref _currentIConnectionLifetimeFeature); } + else if (typeof(TFeature) == typeof(IConnectionSocketFeature)) + { + feature = Unsafe.As(ref _currentIConnectionSocketFeature); + } else if (MaybeExtra != null) { feature = (TFeature?)(ExtraFeatureGet(typeof(TFeature))); @@ -231,6 +247,10 @@ private void ExtraFeatureSet(Type key, object? value) { _currentIConnectionLifetimeFeature = Unsafe.As(ref feature); } + else if (typeof(TFeature) == typeof(IConnectionSocketFeature)) + { + _currentIConnectionSocketFeature = Unsafe.As(ref feature); + } else { ExtraFeatureSet(typeof(TFeature), feature); @@ -259,6 +279,10 @@ private IEnumerable> FastEnumerable() { yield return new KeyValuePair(typeof(IConnectionLifetimeFeature), _currentIConnectionLifetimeFeature); } + if (_currentIConnectionSocketFeature != null) + { + yield return new KeyValuePair(typeof(IConnectionSocketFeature), _currentIConnectionSocketFeature); + } if (MaybeExtra != null) { diff --git a/src/Servers/Kestrel/test/Sockets.FunctionalTests/SocketTranspotTests.cs b/src/Servers/Kestrel/test/Sockets.FunctionalTests/SocketTranspotTests.cs new file mode 100644 index 000000000000..ae619a1b7e21 --- /dev/null +++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/SocketTranspotTests.cs @@ -0,0 +1,54 @@ +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Sockets.FunctionalTests +{ + public class SocketTranspotTests : LoggedTestBase + { + [Fact] + public async Task SocketTransportExposesSocketsFeature() + { + var builder = TransportSelector.GetHostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseKestrel() + .UseUrls("http://127.0.0.1:0") + .Configure(app => + { + app.Run(context => + { + var socket = context.Features.Get().Socket; + Assert.NotNull(socket); + Assert.Equal(ProtocolType.Tcp, socket.ProtocolType); + var ip = (IPEndPoint)socket.RemoteEndPoint; + Assert.Equal(ip.Address, context.Connection.RemoteIpAddress); + Assert.Equal(ip.Port, context.Connection.RemotePort); + + return Task.CompletedTask; + }); + }); + }) + .ConfigureServices(AddTestLogging); + + using var host = builder.Build(); + using var client = new HttpClient(); + + await host.StartAsync(); + + var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); + response.EnsureSuccessStatusCode(); + + await host.StopAsync(); + } + } +} diff --git a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs index 60d00f404a2b..912615803957 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs @@ -11,7 +11,18 @@ public static string GenerateFile() { // NOTE: This list MUST always match the set of feature interfaces implemented by TransportConnection. // See also: shared/TransportConnection.FeatureCollection.cs - var features = new[] + + var allFeatures = new[] + { + "IConnectionIdFeature", + "IConnectionTransportFeature", + "IConnectionItemsFeature", + "IMemoryPoolFeature", + "IConnectionLifetimeFeature", + "IConnectionSocketFeature" + }; + + var implementedFeatures = new[] { "IConnectionIdFeature", "IConnectionTransportFeature", @@ -27,8 +38,8 @@ public static string GenerateFile() return FeatureCollectionGenerator.GenerateFile( namespaceName: "Microsoft.AspNetCore.Connections", className: "TransportConnection", - allFeatures: features, - implementedFeatures: features, + allFeatures: allFeatures, + implementedFeatures: implementedFeatures, extraUsings: usings, fallbackFeatures: null); }