diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index 011138e..8172fc8 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -57,6 +57,14 @@ jobs: run: dotnet nuget push Bunkum.Protocols.Http/bin/Release/Bunkum.Protocols.Http.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_TOKEN} env: NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} + - name: Upload Bunkum.Protocols.Https + run: dotnet nuget push Bunkum.Protocols.Https/bin/Release/Bunkum.Protocols.Https.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_TOKEN} + env: + NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} + - name: Upload Bunkum.Protocols.TlsSupport + run: dotnet nuget push Bunkum.Protocols.TlsSupport/bin/Release/Bunkum.Protocols.TlsSupport.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_TOKEN} + env: + NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} - name: Upload Bunkum.Protocols.Gopher run: dotnet nuget push Bunkum.Protocols.Gopher/bin/Release/Bunkum.Protocols.Gopher.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_TOKEN} env: diff --git a/Bunkum.Protocols.Gemini/Bunkum.Protocols.Gemini.csproj b/Bunkum.Protocols.Gemini/Bunkum.Protocols.Gemini.csproj index 01b8349..8820305 100644 --- a/Bunkum.Protocols.Gemini/Bunkum.Protocols.Gemini.csproj +++ b/Bunkum.Protocols.Gemini/Bunkum.Protocols.Gemini.csproj @@ -36,6 +36,7 @@ + diff --git a/Bunkum.Protocols.Gemini/BunkumGeminiServer.cs b/Bunkum.Protocols.Gemini/BunkumGeminiServer.cs index 5bcc1be..1d35c75 100644 --- a/Bunkum.Protocols.Gemini/BunkumGeminiServer.cs +++ b/Bunkum.Protocols.Gemini/BunkumGeminiServer.cs @@ -3,6 +3,7 @@ using Bunkum.Core.Configuration; using Bunkum.Listener; using Bunkum.Protocols.Gemini.Socket; +using Bunkum.Protocols.TlsSupport; using NotEnoughLogs; using NotEnoughLogs.Sinks; @@ -11,6 +12,7 @@ namespace Bunkum.Protocols.Gemini; public class BunkumGeminiServer : BunkumServer { private readonly X509Certificate2 _cert; + private readonly SslConfiguration _sslConfiguration; /// /// Create a new BunkumGeminiServer @@ -21,14 +23,14 @@ public class BunkumGeminiServer : BunkumServer public BunkumGeminiServer(SslConfiguration? sslConfiguration = null, LoggerConfiguration? configuration = null, List? sinks = null) : base(configuration, sinks) { //If the SSL configuration is not specified, load the config from JSON - sslConfiguration ??= Config.LoadFromJsonFile("geminissl.json", this.Logger); + this._sslConfiguration = sslConfiguration ?? Config.LoadFromJsonFile("geminissl.json", this.Logger); - this._cert = new X509Certificate2(File.ReadAllBytes(sslConfiguration.SslCertificate), sslConfiguration.CertificatePassword); + this._cert = new X509Certificate2(File.ReadAllBytes(sslConfiguration.SslCertificate), this._sslConfiguration.CertificatePassword); } protected override BunkumListener CreateDefaultListener(Uri listenEndpoint, bool useForwardedIp, Logger logger) { - return new SocketGeminiListener(this._cert, listenEndpoint, logger); + return new SocketGeminiListener(this._cert, this._sslConfiguration, listenEndpoint, logger); } protected override string ProtocolUriName => "gemini"; } \ No newline at end of file diff --git a/Bunkum.Protocols.Gemini/Socket/SocketGeminiListener.cs b/Bunkum.Protocols.Gemini/Socket/SocketGeminiListener.cs index cd87786..bbc6809 100644 --- a/Bunkum.Protocols.Gemini/Socket/SocketGeminiListener.cs +++ b/Bunkum.Protocols.Gemini/Socket/SocketGeminiListener.cs @@ -2,7 +2,7 @@ using System.Net; using System.Net.Security; using System.Net.Sockets; -using System.Security.Authentication; +using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; @@ -11,6 +11,7 @@ using Bunkum.Listener.Extensions; using Bunkum.Listener.Protocol; using Bunkum.Listener.Request; +using Bunkum.Protocols.TlsSupport; using NotEnoughLogs; namespace Bunkum.Protocols.Gemini.Socket; @@ -20,14 +21,16 @@ public partial class SocketGeminiListener : BunkumGeminiListener private System.Net.Sockets.Socket? _socket; private readonly Uri _listenEndpoint; private readonly X509Certificate2 _cert; + private readonly SslConfiguration _sslConfiguration; [GeneratedRegex("^[a-zA-Z]+$")] private static partial Regex LettersRegex(); - public SocketGeminiListener(X509Certificate2 cert, Uri listenEndpoint, Logger logger) : base(logger) + public SocketGeminiListener(X509Certificate2 cert, SslConfiguration sslConfiguration, Uri listenEndpoint, Logger logger) : base(logger) { this._listenEndpoint = listenEndpoint; this._cert = cert; + this._sslConfiguration = sslConfiguration; this.Logger.LogInfo(ListenerCategory.Startup, "Internal Gemini server is listening at URL {0}", listenEndpoint); } @@ -88,13 +91,19 @@ private async Task ReadRequestIntoContext(System.Net.Sockets.So { SslStream stream = new(rawStream); - await stream.AuthenticateAsServerAsync(new SslServerAuthenticationOptions + SslServerAuthenticationOptions authOptions = new() { - EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13, + EnabledSslProtocols = this._sslConfiguration.EnabledSslProtocols, ServerCertificate = this._cert, ClientCertificateRequired = true, - RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true, - }); + RemoteCertificateValidationCallback = (_, _, _, _) => true, + }; + + // If the cipher suite set is enabled, and we are not on windows, enable the selected cipher suites + if (this._sslConfiguration.EnabledCipherSuites != null && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + authOptions.CipherSuitesPolicy = new CipherSuitesPolicy(this._sslConfiguration.EnabledCipherSuites); + + await stream.AuthenticateAsServerAsync(authOptions); Uri uri = new(GetPath(stream)); diff --git a/Bunkum.Protocols.Gemini/SslConfiguration.cs b/Bunkum.Protocols.Gemini/SslConfiguration.cs deleted file mode 100644 index 512e933..0000000 --- a/Bunkum.Protocols.Gemini/SslConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Bunkum.Core.Configuration; - -namespace Bunkum.Protocols.Gemini; - -public class SslConfiguration : Config -{ - public override int CurrentConfigVersion => 1; - public override int Version { get; set; } - - protected override void Migrate(int oldVer, dynamic oldConfig) - {} - - public string SslCertificate { get; set; } = "cert.pfx"; - public string? CertificatePassword { get; set; } = "password here or null"; -} \ No newline at end of file diff --git a/Bunkum.Protocols.Http/BunkumHttpServer.cs b/Bunkum.Protocols.Http/BunkumHttpServer.cs index 83cf6a9..de394b2 100644 --- a/Bunkum.Protocols.Http/BunkumHttpServer.cs +++ b/Bunkum.Protocols.Http/BunkumHttpServer.cs @@ -1,4 +1,6 @@ +using System.Security.Cryptography.X509Certificates; using Bunkum.Core; +using Bunkum.Core.Configuration; using Bunkum.Listener; using NotEnoughLogs; using NotEnoughLogs.Sinks; diff --git a/Bunkum.Protocols.Http/Socket/SocketHttpListener.cs b/Bunkum.Protocols.Http/Socket/SocketHttpListener.cs index 8220791..e41ea5f 100644 --- a/Bunkum.Protocols.Http/Socket/SocketHttpListener.cs +++ b/Bunkum.Protocols.Http/Socket/SocketHttpListener.cs @@ -1,6 +1,10 @@ using System.Diagnostics; using System.Net; +using System.Net.Security; using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using System.Web; using Bunkum.Listener; @@ -16,14 +20,27 @@ public partial class SocketHttpListener : BunkumHttpListener private System.Net.Sockets.Socket? _socket; private readonly Uri _listenEndpoint; private readonly bool _useForwardedIp; - + private readonly X509Certificate2? _cert; + private readonly SslProtocols _enabledSslProtocols; + private readonly TlsCipherSuite[]? _enabledCipherSuites; + [GeneratedRegex("^[a-zA-Z]+$")] private static partial Regex LettersRegex(); - public SocketHttpListener(Uri listenEndpoint, bool useForwardedIp, Logger logger) : base(logger) + public SocketHttpListener( + Uri listenEndpoint, + bool useForwardedIp, + Logger logger, + X509Certificate2? certificate = null, + SslProtocols enabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13, + TlsCipherSuite[]? enabledCipherSuites = null) + : base(logger) { this._listenEndpoint = listenEndpoint; this._useForwardedIp = useForwardedIp; + this._cert = certificate; + this._enabledCipherSuites = enabledCipherSuites; + this._enabledSslProtocols = enabledSslProtocols; this.Logger.LogInfo(ListenerCategory.Startup, "Internal HTTP server is listening at URL " + listenEndpoint); } @@ -75,8 +92,31 @@ public override void StartListening() } } - private ListenerContext ReadRequestIntoContext(System.Net.Sockets.Socket client, Stream stream) + private ListenerContext ReadRequestIntoContext(System.Net.Sockets.Socket client, Stream rawStream) { + Stream stream = rawStream; + SslStream? sslStream = null; + if (this._cert != null) + { + sslStream = new SslStream(rawStream); + + SslServerAuthenticationOptions authOptions = new() + { + EnabledSslProtocols = this._enabledSslProtocols, + ServerCertificate = this._cert, + ClientCertificateRequired = false, + RemoteCertificateValidationCallback = (_, _, _, _) => true, + }; + + // If the cipher suite set is enabled, and we are not on windows, enable the selected cipher suites + if (this._enabledCipherSuites != null && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + authOptions.CipherSuitesPolicy = new CipherSuitesPolicy(this._enabledCipherSuites); + + sslStream.AuthenticateAsServer(authOptions); + + stream = sslStream; + } + Span method = stackalloc char[RequestLineMethodLimit]; Span path = stackalloc char[RequestLinePathLimit]; Span version = stackalloc char[RequestLineVersionLimit]; @@ -199,6 +239,10 @@ private ListenerContext ReadRequestIntoContext(System.Net.Sockets.Socket client, } context.InputStream = inputStream; + // If this an SSL connection, set the remote certificate + if (sslStream != null) + context.RemoteCertificate = sslStream.RemoteCertificate; + return context; } diff --git a/Bunkum.Protocols.Http/Socket/SocketHttpListenerContext.cs b/Bunkum.Protocols.Http/Socket/SocketHttpListenerContext.cs index 09e15e0..b8bd7a2 100644 --- a/Bunkum.Protocols.Http/Socket/SocketHttpListenerContext.cs +++ b/Bunkum.Protocols.Http/Socket/SocketHttpListenerContext.cs @@ -11,10 +11,13 @@ public SocketHttpListenerContext(System.Net.Sockets.Socket socket, Stream stream protected override async Task SendResponseInternal(HttpStatusCode code, ArraySegment? data = null) { - // this is dumb and stupid - this.ResponseHeaders.Add("Server", "Bunkum"); - this.ResponseHeaders.Add("Connection", "close"); - this.ResponseHeaders.Add("Date", DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'")); + // These are "TryAdd" and not "Add" since if you are proxying requests, + // both of these should be set to the headers sent by the proxied server, and not be generated by Bunkum directly + this.ResponseHeaders.TryAdd("Server", "Bunkum"); + this.ResponseHeaders.TryAdd("Date", DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'")); + // This is an unconditional set because Bunkum does not support keep-alive connections, + // theres currently no reason for anything other than Bunkum to set this value + this.ResponseHeaders["Connection"] = "close"; List response = new() { $"HTTP/1.1 {code.GetHashCode()} {code.ToString()}" }; // TODO: spaced code names ("Not Found" instead of "NotFound") foreach ((string? key, string? value) in this.ResponseHeaders) diff --git a/Bunkum.Protocols.Https/Bunkum.Protocols.Https.csproj b/Bunkum.Protocols.Https/Bunkum.Protocols.Https.csproj new file mode 100644 index 0000000..5c7ff46 --- /dev/null +++ b/Bunkum.Protocols.Https/Bunkum.Protocols.Https.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Bunkum.Protocols.Https/BunkumHttpsServer.cs b/Bunkum.Protocols.Https/BunkumHttpsServer.cs new file mode 100644 index 0000000..eb17778 --- /dev/null +++ b/Bunkum.Protocols.Https/BunkumHttpsServer.cs @@ -0,0 +1,33 @@ +using System.Security.Cryptography.X509Certificates; +using Bunkum.Core; +using Bunkum.Core.Configuration; +using Bunkum.Listener; +using Bunkum.Protocols.TlsSupport; +using NotEnoughLogs; +using NotEnoughLogs.Sinks; + +namespace Bunkum.Protocols.Https; + +public class BunkumHttpsServer : BunkumServer +{ + private readonly X509Certificate2? _cert; + private readonly SslConfiguration _sslConfiguration; + + public BunkumHttpsServer(LoggerConfiguration? configuration = null, List? sinks = null, + SslConfiguration? sslConfiguration = null) : base(configuration, sinks) + { + //If the SSL configuration is not specified, load the config from JSON + this._sslConfiguration = sslConfiguration ?? Config.LoadFromJsonFile("ssl.json", this.Logger); + + this._cert = new X509Certificate2(File.ReadAllBytes(this._sslConfiguration.SslCertificate), this._sslConfiguration.CertificatePassword); + } + + /// + protected override BunkumListener CreateDefaultListener(Uri listenEndpoint, bool useForwardedIp, Logger logger) + { + return new Http.Socket.SocketHttpListener(listenEndpoint, useForwardedIp, logger, this._cert, this._sslConfiguration.EnabledSslProtocols, this._sslConfiguration.EnabledCipherSuites); + } + + /// + protected override string ProtocolUriName => "https"; +} \ No newline at end of file diff --git a/Bunkum.Protocols.TlsSupport/Bunkum.Protocols.TlsSupport.csproj b/Bunkum.Protocols.TlsSupport/Bunkum.Protocols.TlsSupport.csproj new file mode 100644 index 0000000..91c48da --- /dev/null +++ b/Bunkum.Protocols.TlsSupport/Bunkum.Protocols.TlsSupport.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Bunkum.Protocols.TlsSupport/SslConfiguration.cs b/Bunkum.Protocols.TlsSupport/SslConfiguration.cs new file mode 100644 index 0000000..7eaecf8 --- /dev/null +++ b/Bunkum.Protocols.TlsSupport/SslConfiguration.cs @@ -0,0 +1,53 @@ +using System.Net.Security; +using System.Security.Authentication; +using Bunkum.Core.Configuration; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Bunkum.Protocols.TlsSupport; + +public class SslConfiguration : Config +{ + public override int CurrentConfigVersion => 1; + public override int Version { get; set; } + + protected override void Migrate(int oldVer, dynamic oldConfig) + {} + + /// + /// The path to the certificate + /// + public string SslCertificate { get; set; } = "cert.pfx"; + /// + /// The password for the certificate, null if none + /// + public string? CertificatePassword { get; set; } + + /// + /// The SSL protocols which are enabled. If null, enables TLS1.3 and TLS1.2 + /// + [JsonProperty("EnabledSslProtocols", ItemConverterType = typeof(StringEnumConverter))] + private SslProtocols[]? _EnabledSslProtocols { get; set; } + /// + /// The cipher suites which are enabled. If null, lets the system decide + /// + [JsonProperty(ItemConverterType = typeof(StringEnumConverter))] + public TlsCipherSuite[]? EnabledCipherSuites { get; set; } + + [JsonIgnore] + public SslProtocols EnabledSslProtocols + { + get + { + SslProtocols protocols = SslProtocols.None; + + if (this._EnabledSslProtocols == null) + protocols = SslProtocols.Tls12 | SslProtocols.Tls13; + else + protocols = this._EnabledSslProtocols + .Aggregate(protocols, (current, protocol) => current | protocol); + + return protocols; + } + } +} \ No newline at end of file diff --git a/Bunkum.sln b/Bunkum.sln index fc30410..bcdc1ed 100644 --- a/Bunkum.sln +++ b/Bunkum.sln @@ -69,6 +69,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Database", "Database", "{ED EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataStore", "DataStore", "{D09D7F3C-B198-40D8-B7E0-CC2CFFC75CDD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bunkum.Protocols.Https", "Bunkum.Protocols.Https\Bunkum.Protocols.Https.csproj", "{FEDB1C3F-5549-4B44-94CE-54FFE590A35A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bunkum.Protocols.TlsSupport", "Bunkum.Protocols.TlsSupport\Bunkum.Protocols.TlsSupport.csproj", "{B7B1AAD8-2E00-439E-A587-7434E51C1E9E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -174,6 +178,14 @@ Global {23BC7994-0E93-4FB3-8D31-16EC3BE1198B}.Debug|Any CPU.Build.0 = Debug|Any CPU {23BC7994-0E93-4FB3-8D31-16EC3BE1198B}.Release|Any CPU.ActiveCfg = Release|Any CPU {23BC7994-0E93-4FB3-8D31-16EC3BE1198B}.Release|Any CPU.Build.0 = Release|Any CPU + {FEDB1C3F-5549-4B44-94CE-54FFE590A35A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEDB1C3F-5549-4B44-94CE-54FFE590A35A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEDB1C3F-5549-4B44-94CE-54FFE590A35A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEDB1C3F-5549-4B44-94CE-54FFE590A35A}.Release|Any CPU.Build.0 = Release|Any CPU + {B7B1AAD8-2E00-439E-A587-7434E51C1E9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7B1AAD8-2E00-439E-A587-7434E51C1E9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7B1AAD8-2E00-439E-A587-7434E51C1E9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7B1AAD8-2E00-439E-A587-7434E51C1E9E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {B6B9F1DD-7E85-402E-88A7-EF6B88E6CE9A} = {77922B8B-F6FE-47D6-8742-6BA528BC8D7C} @@ -200,5 +212,7 @@ Global {DEE9CFC0-9529-451F-9633-AF5B49498AFB} = {EDBF1652-710C-4CF9-8890-AF3B46FDA8E1} {D09D7F3C-B198-40D8-B7E0-CC2CFFC75CDD} = {77922B8B-F6FE-47D6-8742-6BA528BC8D7C} {993BD323-2E2C-4E79-9252-F92B8CEB834D} = {D09D7F3C-B198-40D8-B7E0-CC2CFFC75CDD} + {FEDB1C3F-5549-4B44-94CE-54FFE590A35A} = {BBAA9024-6FEC-4C68-9F96-83D2055EC6D2} + {B7B1AAD8-2E00-439E-A587-7434E51C1E9E} = {BBAA9024-6FEC-4C68-9F96-83D2055EC6D2} EndGlobalSection EndGlobal