Skip to content

Commit 75a4eac

Browse files
gscatgscat
authored andcommitted
wip: UseSsl, builder improvements
1 parent 6a2515a commit 75a4eac

File tree

11 files changed

+458
-61
lines changed

11 files changed

+458
-61
lines changed

cadente/Sisk.Cadente.CoreEngine/CadenteHttpServerEngine.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
// File name: CadenteHttpServerEngine.cs
99
// Repository: https://github.com/sisk-http/core
1010

11+
using System;
1112
using System.Net;
13+
using System.Runtime.InteropServices.JavaScript;
1214
using System.Threading.Channels;
15+
using Sisk.Core.Http;
1316
using Sisk.Core.Http.Engine;
1417

1518
namespace Sisk.Cadente.CoreEngine {
@@ -22,6 +25,7 @@ namespace Sisk.Cadente.CoreEngine {
2225
public sealed class CadenteHttpServerEngine : HttpServerEngine, IDisposable {
2326
private List<HttpHost> hosts = [];
2427
private List<string> prefixes = [];
28+
private ListeningHostSslOptions? sslOptions;
2529
private TimeSpan idleConnectionTimeout = TimeSpan.FromSeconds ( 90 );
2630
private bool isDisposed;
2731

@@ -57,10 +61,15 @@ public override TimeSpan IdleConnectionTimeout {
5761
public override HttpServerEngineContextEventLoopMecanism EventLoopMecanism => HttpServerEngineContextEventLoopMecanism.InlineAsyncronousGetContext;
5862

5963
/// <inheritdoc/>
60-
public override void AddListeningPrefix ( string prefix ) {
64+
public override void AddListeningPrefix ( string prefix ) {
6165
prefixes.Add ( prefix );
6266
}
6367

68+
/// <inheritdoc/>
69+
public override void UseListeningHostSslOptions ( ListeningHostSslOptions sslOptions ) {
70+
this.sslOptions = sslOptions;
71+
}
72+
6473
/// <inheritdoc/>
6574
public override void ClearPrefixes () {
6675
prefixes.Clear ();
@@ -84,8 +93,14 @@ public override void StartServer () {
8493
host = new HttpHost ( new IPEndPoint ( dnsHost.AddressList [ 0 ], uri.Port ) );
8594
}
8695

87-
host.TimeoutManager.ClientReadTimeout = idleConnectionTimeout;
88-
host.TimeoutManager.ClientWriteTimeout = idleConnectionTimeout;
96+
if (sslOptions is { }) {
97+
host.HttpsOptions = new ( sslOptions.ServerCertificate ) {
98+
AllowedProtocols = sslOptions.AllowedProtocols,
99+
CheckCertificateRevocation = sslOptions.CheckCertificateRevocation,
100+
ClientCertificateRequired = sslOptions.ClientCertificateRequired
101+
};
102+
}
103+
89104
host.Handler = new CadenteHttpEngineHostHandler ( this );
90105
setupHostAction?.Invoke ( host );
91106

cadente/Sisk.Cadente.CoreEngine/CadenteHttpServerEngineContext.cs

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,48 +49,53 @@ public CadenteHttpServerEngineContext ( CadenteHttpServerEngineRequest request,
4949
/// <inheritdoc/>
5050
public override Task<HttpServerEngineWebSocket> AcceptWebSocketAsync ( string? subProtocol ) {
5151

52-
string? wsKey = _request._context.Request.Headers.Get ( "Sec-WebSocket-Key" ).FirstOrDefault ()
52+
try {
53+
string? wsKey = _request._context.Request.Headers.Get ( "Sec-WebSocket-Key" ).FirstOrDefault ()
5354
?? throw new InvalidOperationException ( "Missing 'Sec-WebSocket-Key' header in WebSocket upgrade request." );
5455

55-
byte [] wsAcceptToken = SHA1.HashData ( Encoding.UTF8.GetBytes ( $"{wsKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) );
56+
byte [] wsAcceptToken = SHA1.HashData ( Encoding.UTF8.GetBytes ( $"{wsKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) );
5657

57-
var underlyingResponse = _request._context.Response;
58+
var underlyingResponse = _request._context.Response;
5859

59-
underlyingResponse.StatusCode = 101;
60-
underlyingResponse.StatusDescription = "Switching Protocols";
60+
underlyingResponse.StatusCode = 101;
61+
underlyingResponse.StatusDescription = "Switching Protocols";
6162

62-
underlyingResponse.Headers.Set ( new HttpHeader ( "Connection", "Upgrade" ) );
63-
underlyingResponse.Headers.Set ( new HttpHeader ( "Upgrade", "websocket" ) );
64-
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Accept", Convert.ToBase64String ( wsAcceptToken ) ) );
65-
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Version", "13" ) );
63+
underlyingResponse.Headers.Set ( new HttpHeader ( "Connection", "Upgrade" ) );
64+
underlyingResponse.Headers.Set ( new HttpHeader ( "Upgrade", "websocket" ) );
65+
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Accept", Convert.ToBase64String ( wsAcceptToken ) ) );
66+
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Version", "13" ) );
6667

67-
if (subProtocol is { Length: > 0 }) {
68+
if (subProtocol is { Length: > 0 }) {
6869

69-
string [] clientSubProtocols = _request._context.Request.Headers.Get ( "Sec-WebSocket-Protocol" )
70-
.SelectMany ( s => s.Split ( ",", StringSplitOptions.RemoveEmptyEntries ) )
71-
.Select ( s => s.Trim () )
72-
.ToArray ();
70+
string [] clientSubProtocols = _request._context.Request.Headers.Get ( "Sec-WebSocket-Protocol" )
71+
.SelectMany ( s => s.Split ( ",", StringSplitOptions.RemoveEmptyEntries ) )
72+
.Select ( s => s.Trim () )
73+
.ToArray ();
7374

74-
if (!clientSubProtocols.Contains ( subProtocol, StringComparer.Ordinal )) {
75+
if (!clientSubProtocols.Contains ( subProtocol, StringComparer.Ordinal )) {
7576

76-
underlyingResponse.StatusCode = 426;
77-
underlyingResponse.StatusDescription = "Upgrade Required";
77+
underlyingResponse.StatusCode = 426;
78+
underlyingResponse.StatusDescription = "Upgrade Required";
7879

79-
throw new InvalidOperationException ( $"The requested sub-protocol '{subProtocol}' is not supported by the client." );
80-
}
80+
throw new InvalidOperationException ( $"The requested sub-protocol '{subProtocol}' is not supported by the client." );
81+
}
8182

82-
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Protocol", subProtocol ) );
83-
}
83+
underlyingResponse.Headers.Set ( new HttpHeader ( "Sec-WebSocket-Protocol", subProtocol ) );
84+
}
8485

85-
Stream underlyingStream = underlyingResponse.GetResponseStream ( chunked: false );
86+
Stream underlyingStream = underlyingResponse.GetResponseStream ( chunked: false );
8687

87-
var ws = WebSocket.CreateFromStream ( underlyingStream, new WebSocketCreationOptions () {
88-
IsServer = true,
89-
SubProtocol = subProtocol,
90-
KeepAliveInterval = TimeSpan.FromSeconds ( 20 )
91-
} );
88+
var ws = WebSocket.CreateFromStream ( underlyingStream, new WebSocketCreationOptions () {
89+
IsServer = true,
90+
SubProtocol = subProtocol,
91+
KeepAliveInterval = TimeSpan.FromSeconds ( 20 )
92+
} );
9293

93-
return Task.FromResult ( HttpServerEngineWebSocket.CreateFromWebSocket ( ws ) );
94+
return Task.FromResult ( HttpServerEngineWebSocket.CreateFromWebSocket ( ws ) );
95+
}
96+
catch (Exception ex) {
97+
throw new Sisk.Core.Http.HttpRequestException("Failed to enter WebSocket context. See inner exception.", ex);
98+
}
9499
}
95100
}
96101
}

src/Helpers/CertificateHelper.cs

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ namespace Sisk.Core.Helpers;
2222
/// Provides a set of useful functions to issue self-signed development certificates.
2323
/// </summary>
2424
public static class CertificateHelper {
25+
private const string PfxPassword = "sisk";
2526

2627
// -> https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/Common/src/System/Text/StringOrCharArray.cs#L140
2728
// we need to make sure each hashcode for each string is the same.
@@ -58,20 +59,60 @@ static int ComputeArrayHash ( string [] array ) {
5859
/// <param name="dnsNames">The certificate DNS names.</param>
5960
public static X509Certificate2 CreateTrustedDevelopmentCertificate ( params string [] dnsNames ) {
6061
int dnsHash = ComputeArrayHash ( dnsNames );
61-
X509Certificate2 x509Certificate2;
62-
using (var store = new X509Store ( StoreName.Root, StoreLocation.CurrentUser )) {
63-
store.Open ( OpenFlags.ReadWrite );
64-
65-
var siskCert = store.Certificates.FirstOrDefault ( c => c.Issuer.Contains ( $"Sisk Development CA {dnsHash}" ) );
66-
if (siskCert is null) {
67-
x509Certificate2 = CreateDevelopmentCertificate ( dnsNames );
68-
store.Add ( x509Certificate2 );
69-
}
70-
else {
71-
x509Certificate2 = siskCert;
72-
}
62+
string basePath = Path.Combine (
63+
Environment.GetFolderPath ( Environment.SpecialFolder.LocalApplicationData ),
64+
".sisk", "development-certs" );
65+
66+
Directory.CreateDirectory ( basePath );
67+
68+
string fileName = $"SiskDevelopment_{dnsHash}.pfx";
69+
string pfxPath = Path.Combine ( basePath, fileName );
70+
71+
X509Certificate2 certificate;
72+
73+
if (File.Exists ( pfxPath )) {
74+
certificate = LoadPfxFromDisk ( pfxPath );
75+
}
76+
else {
77+
using var fresh = CreateDevelopmentCertificate ( dnsNames );
78+
79+
File.WriteAllBytes (
80+
pfxPath,
81+
fresh.Export ( X509ContentType.Pfx, PfxPassword ) );
82+
83+
certificate = LoadPfxFromDisk ( pfxPath );
84+
}
85+
86+
EnsureTrusted ( certificate );
87+
88+
return certificate;
89+
}
90+
91+
private static X509Certificate2 LoadPfxFromDisk ( string path ) {
92+
#if NET9_0_OR_GREATER
93+
return X509CertificateLoader.LoadPkcs12 (
94+
File.ReadAllBytes ( path ),
95+
PfxPassword,
96+
X509KeyStorageFlags.Exportable |
97+
X509KeyStorageFlags.PersistKeySet |
98+
X509KeyStorageFlags.UserKeySet );
99+
#else
100+
return new X509Certificate2 (
101+
path,
102+
PfxPassword,
103+
X509KeyStorageFlags.Exportable |
104+
X509KeyStorageFlags.PersistKeySet |
105+
X509KeyStorageFlags.UserKeySet );
106+
#endif
107+
}
108+
109+
private static void EnsureTrusted ( X509Certificate2 certificate ) {
110+
// deixa a confiança local (TrustedPeople) ou raiz (Root)
111+
using var trusted = new X509Store ( StoreName.Root, StoreLocation.CurrentUser );
112+
trusted.Open ( OpenFlags.ReadWrite );
113+
if (trusted.Certificates.Cast<X509Certificate2> ().FirstOrDefault ( c => c.Thumbprint == certificate.Thumbprint ) is null) {
114+
trusted.Add ( certificate );
73115
}
74-
return x509Certificate2;
75116
}
76117

77118
/// <summary>
@@ -82,31 +123,43 @@ public static X509Certificate2 CreateDevelopmentCertificate ( params string [] d
82123
if (dnsNames.Length == 0)
83124
throw new ArgumentException ( "At least one DNS name must be specified.", nameof ( dnsNames ) );
84125

85-
SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder ();
126+
var sanBuilder = new SubjectAlternativeNameBuilder ();
86127
sanBuilder.AddIpAddress ( IPAddress.Loopback );
87128
sanBuilder.AddIpAddress ( IPAddress.IPv6Loopback );
88129

89-
foreach (string dnsName in dnsNames.Distinct ( StringComparer.OrdinalIgnoreCase )) {
130+
foreach (string dnsName in dnsNames.Distinct ( StringComparer.OrdinalIgnoreCase ))
90131
sanBuilder.AddDnsName ( dnsName.ToLowerInvariant () );
91-
}
92132

93-
X500DistinguishedName distinguishedName = new X500DistinguishedName ( $"CN = Sisk Development CA {ComputeArrayHash ( dnsNames )},OU = IT,O = Sao Paulo,L = Brazil,S = Sao Paulo,C = Brazil" );
133+
var distinguishedName = new X500DistinguishedName (
134+
$"CN = Sisk Development CA #{ComputeArrayHash ( dnsNames )},OU = IT,O = Sao Paulo,L = Brazil,S = Sao Paulo,C = Brazil" );
94135

95-
using (RSA rsa = RSA.Create ( 2048 )) {
96-
var request = new CertificateRequest ( distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1 );
136+
using RSA rsa = RSA.Create ( 2048 );
137+
var request = new CertificateRequest ( distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1 );
97138

98-
request.CertificateExtensions.Add (
99-
new X509EnhancedKeyUsageExtension ( new OidCollection { new Oid ( "1.3.6.1.5.5.7.3.1" ) }, false ) );
100-
request.CertificateExtensions.Add (
101-
sanBuilder.Build () );
139+
request.CertificateExtensions.Add (
140+
new X509EnhancedKeyUsageExtension ( new OidCollection { new Oid ( "1.3.6.1.5.5.7.3.1" ) }, false ) );
141+
request.CertificateExtensions.Add ( sanBuilder.Build () );
102142

103-
var certificate = request.CreateSelfSigned ( new DateTimeOffset ( DateTime.UtcNow.AddDays ( -1 ) ), new DateTimeOffset ( DateTime.UtcNow.AddDays ( 3650 ) ) );
143+
using var certificate = request.CreateSelfSigned (
144+
DateTimeOffset.UtcNow.AddDays ( -1 ),
145+
DateTimeOffset.UtcNow.AddYears ( 10 ) );
146+
147+
var pfxBytes = certificate.Export ( X509ContentType.Pfx, PfxPassword );
104148

105149
#if NET9_0_OR_GREATER
106-
return X509CertificateLoader.LoadPkcs12 ( certificate.Export ( X509ContentType.Pfx, "sisk" ), "sisk", X509KeyStorageFlags.DefaultKeySet );
150+
return X509CertificateLoader.LoadPkcs12 (
151+
pfxBytes,
152+
PfxPassword,
153+
X509KeyStorageFlags.Exportable |
154+
X509KeyStorageFlags.PersistKeySet |
155+
X509KeyStorageFlags.UserKeySet );
107156
#else
108-
return new X509Certificate2 ( certificate.Export ( X509ContentType.Pfx, "sisk" ), "sisk", X509KeyStorageFlags.DefaultKeySet );
157+
return new X509Certificate2(
158+
pfxBytes,
159+
PfxPassword,
160+
X509KeyStorageFlags.Exportable |
161+
X509KeyStorageFlags.PersistKeySet |
162+
X509KeyStorageFlags.UserKeySet);
109163
#endif
110-
}
111164
}
112165
}

src/Http/Engine/HttpListenerAbstractEngine.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ public override void StopServer () {
8484
throw new NotImplementedException ();
8585
}
8686

87+
/// <inheritdoc/>
88+
public override void UseListeningHostSslOptions ( ListeningHostSslOptions sslOptions ) {
89+
throw new NotSupportedException ( "The native .NET HttpListener does not support SSL." );
90+
}
91+
8792
sealed class HttpListenerContextAbstraction ( HttpListenerContext context ) : HttpServerEngineContext {
8893

8994
HttpListenerContext _context = context;

src/Http/Engine/HttpServerEngine.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ public abstract class HttpServerEngine : IDisposable {
3232
/// <param name="prefix">The prefix to add.</param>
3333
public abstract void AddListeningPrefix ( string prefix );
3434

35+
/// <summary>
36+
/// Configures SSL options for the listening host.
37+
/// </summary>
38+
/// <param name="sslOptions">The SSL options to apply.</param>
39+
public abstract void UseListeningHostSslOptions ( ListeningHostSslOptions sslOptions );
40+
3541
/// <summary>
3642
/// Clears all listening prefixes from the server.
3743
/// </summary>

src/Http/Hosting/HttpServerHostContextBuilder.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Diagnostics.CodeAnalysis;
1111
using System.Globalization;
1212
using System.Reflection;
13+
using System.Security.Cryptography.X509Certificates;
1314
using Sisk.Core.Entity;
1415
using Sisk.Core.Http.Engine;
1516
using Sisk.Core.Http.Handlers;
@@ -66,6 +67,9 @@ public HttpServerHostContext Build () {
6667
if (listeningHost.Ports.Count == 0)
6768
listeningHost.Ports.Add ( ListeningPort.GetRandomPort () );
6869

70+
if (listeningHost.SslOptions is { })
71+
configuration.Engine.UseListeningHostSslOptions ( listeningHost.SslOptions );
72+
6973
return _context;
7074
}
7175

@@ -241,6 +245,50 @@ public HttpServerHostContextBuilder UseEngine ( HttpServerEngine engine ) {
241245
return this;
242246
}
243247

248+
/// <summary>
249+
/// Sets the HTTP server engine using a default constructor and applies the specified configuration action.
250+
/// </summary>
251+
/// <typeparam name="TEngine">The type of the HTTP server engine to use, which must inherit from <see cref="HttpServerEngine"/> and have a parameterless constructor.</typeparam>
252+
/// <param name="setup">An action that configures the newly created engine instance.</param>
253+
/// <returns>The current <see cref="HttpServerHostContextBuilder"/> instance.</returns>
254+
public HttpServerHostContextBuilder UseEngine<TEngine> ( Action<TEngine> setup ) where TEngine : HttpServerEngine, new() {
255+
var engine = new TEngine ();
256+
setup ( engine );
257+
configuration.Engine = engine;
258+
return this;
259+
}
260+
261+
/// <summary>
262+
/// Sets the HTTP server engine using a factory method.
263+
/// </summary>
264+
/// <typeparam name="TEngine">The type of the HTTP server engine to use, which must inherit from <see cref="HttpServerEngine"/>.</typeparam>
265+
/// <param name="create">A factory method that creates an instance of the engine.</param>
266+
/// <returns>The current <see cref="HttpServerHostContextBuilder"/> instance.</returns>
267+
public HttpServerHostContextBuilder UseEngine<TEngine> ( Func<TEngine> create ) where TEngine : HttpServerEngine {
268+
configuration.Engine = create ();
269+
return this;
270+
}
271+
272+
/// <summary>
273+
/// Configures SSL for the listening host using the specified options.
274+
/// </summary>
275+
/// <param name="sslOptions">The SSL options to apply to the listening host.</param>
276+
/// <returns>The current <see cref="HttpServerHostContextBuilder"/> instance.</returns>
277+
public HttpServerHostContextBuilder UseSsl ( ListeningHostSslOptions sslOptions ) {
278+
listeningHost.SslOptions = sslOptions;
279+
return this;
280+
}
281+
282+
/// <summary>
283+
/// Configures SSL for the listening host using the specified certificate.
284+
/// </summary>
285+
/// <param name="certificate">The X509 certificate to use for SSL.</param>
286+
/// <returns>The current <see cref="HttpServerHostContextBuilder"/> instance.</returns>
287+
public HttpServerHostContextBuilder UseSsl ( X509Certificate2 certificate ) {
288+
listeningHost.SslOptions = new ListeningHostSslOptions ( certificate );
289+
return this;
290+
}
291+
244292
/// <summary>
245293
/// Calls an action that has an <see cref="CrossOriginResourceSharingHeaders"/> instance from the main listening host as an argument.
246294
/// </summary>

src/Http/HttpRequest.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,18 @@ namespace Sisk.Core.Http {
3030
/// Represents an exception that is thrown while a request is being interpreted by the HTTP server.
3131
/// </summary>
3232
public sealed class HttpRequestException : Exception {
33-
internal HttpRequestException ( string message ) : base ( message ) { }
34-
internal HttpRequestException ( string message, Exception? innerException ) : base ( message, innerException ) { }
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="HttpRequestException"/> class with a specified error message.
35+
/// </summary>
36+
/// <param name="message">The message that describes the error.</param>
37+
public HttpRequestException ( string message ) : base ( message ) { }
38+
39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="HttpRequestException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
41+
/// </summary>
42+
/// <param name="message">The message that describes the error.</param>
43+
/// <param name="innerException">The exception that is the cause of the current exception, or <see langword="null"/> if no inner exception is specified.</param>
44+
public HttpRequestException ( string message, Exception? innerException ) : base ( message, innerException ) { }
3545
}
3646

3747
/// <summary>

0 commit comments

Comments
 (0)