From b74707aac897c99d528a19cb629ebbefc51d474e Mon Sep 17 00:00:00 2001 From: Egidijus Lileika Date: Wed, 15 Dec 2021 16:46:43 +0200 Subject: [PATCH] Added logging support and refactored some code Log4J vuln inspired me to add some logging to this project :))) --- Cuipod/App.cs | 96 +++++++++++++++++------------- Cuipod/Cuipod.csproj | 4 ++ Cuipod/Request.cs | 21 ++----- Cuipod/Response.cs | 4 +- CuipodExample/CuipodExample.csproj | 7 +++ CuipodExample/Server.cs | 52 +++++++++------- CuipodExample/pages/index.gmi | 1 + README.md | 56 ++++++++++++----- 8 files changed, 143 insertions(+), 98 deletions(-) create mode 100644 CuipodExample/pages/index.gmi diff --git a/Cuipod/App.cs b/Cuipod/App.cs index edbb6c0..8bf6395 100644 --- a/Cuipod/App.cs +++ b/Cuipod/App.cs @@ -8,35 +8,30 @@ using System.Text; using System.Security.Authentication; using System.IO; -using RequestCallback = System.Action; +using Microsoft.Extensions.Logging; namespace Cuipod { + using RequestCallback = System.Action>; + public class App { + private readonly TcpListener _listener = new TcpListener(IPAddress.Any, 1965); + private readonly Dictionary _requestCallbacks = new Dictionary(); + private readonly byte[] _buffer = new byte[4096]; + private readonly Decoder _decoder = Encoding.UTF8.GetDecoder(); + private readonly string _directoryToServe; - private readonly TcpListener _listener; private readonly X509Certificate2 _serverCertificate; - private readonly Dictionary _requestCallbacks; + private readonly ILogger _logger; private RequestCallback _onBadRequestCallback; - //somewhat flaky implementation - probably deprecate it - public App(string directoryToServe, string certificateFile, string privateRSAKeyFilePath) + public App(string directoryToServe, X509Certificate2 certificate, ILogger logger) { _directoryToServe = directoryToServe; - _listener = new TcpListener(IPAddress.Any, 1965); - _requestCallbacks = new Dictionary(); - _serverCertificate = CertificateUtils.LoadCertificate(certificateFile, privateRSAKeyFilePath); - } - - public App(string directoryToServe, X509Certificate2 certificate) - { - - _directoryToServe = directoryToServe; - _listener = new TcpListener(IPAddress.Any, 1965); - _requestCallbacks = new Dictionary(); _serverCertificate = certificate; + _logger = logger; } public void OnRequest(string route, RequestCallback callback) @@ -49,13 +44,14 @@ namespace Cuipod _onBadRequestCallback = callback; } - public int Run() + public void Run() { - int status = 0; - Console.WriteLine("Serving capsule on 0.0.0.0:1965"); try { _listener.Start(); + + _logger.LogInformation("Serving capsule on {0}", _listener.Server.LocalEndPoint.ToString()); + while (true) { ProcessRequest(_listener.AcceptTcpClient()); @@ -63,15 +59,12 @@ namespace Cuipod } catch (SocketException e) { - Console.WriteLine("SocketException: {0}", e); - status = 1; + _logger.LogError("SocketException: {0}", e); } finally { _listener.Stop(); } - - return status; } private void ProcessRequest(TcpClient client) @@ -86,16 +79,16 @@ namespace Cuipod } catch (AuthenticationException e) { - Console.WriteLine("Exception: {0}", e.Message); + _logger.LogError("AuthenticationException: {0}", e.Message); if (e.InnerException != null) { - Console.WriteLine("Inner exception: {0}", e.InnerException.Message); + _logger.LogError("Inner exception: {0}", e.InnerException.Message); } - Console.WriteLine("Authentication failed - closing the connection."); + _logger.LogError("Authentication failed - closing the connection."); } catch (IOException e) { - Console.WriteLine("Exception: {0}", e.Message); + _logger.LogError("IOException: {0}", e.Message); } finally { @@ -110,33 +103,37 @@ namespace Cuipod sslStream.AuthenticateAsServer(_serverCertificate, false, SslProtocols.Tls12 | SslProtocols.Tls13, false); // Read a message from the client. - string rawURL = ReadRequest(sslStream); + string rawRequest = ReadRequest(sslStream); Response response = new Response(_directoryToServe); - if (rawURL == null) + if (rawRequest == null) { + _logger.LogDebug("rawRequest is null - bad request"); response.Status = StatusCode.BadRequest; return response; } - Console.WriteLine(rawURL); + _logger.LogDebug("Raw request: \"{0}\"", rawRequest); - int protocolDelimiter = rawURL.IndexOf("://"); + const string protocol= "gemini"; + const string protocolSeparator = "://"; + + int protocolDelimiter = rawRequest.IndexOf(protocolSeparator); if (protocolDelimiter == -1) { response.Status = StatusCode.BadRequest; return response; } - string protocol = rawURL.Substring(0, protocolDelimiter); - if (protocol != "gemini") + string requestProtocol = rawRequest.Substring(0, protocolDelimiter); + if (requestProtocol != protocol) { response.Status = StatusCode.BadRequest; return response; } - string url = rawURL.Substring(protocolDelimiter + 3); + string url = rawRequest.Substring(protocolDelimiter + protocolSeparator.Length); int domainNameDelimiter = url.IndexOf("/"); if (domainNameDelimiter == -1) { @@ -144,22 +141,38 @@ namespace Cuipod return response; } string domainName = url.Substring(0, domainNameDelimiter); + string baseURL = protocol + protocolSeparator + domainName; - Request request = new Request("gemini://" + domainName , url.Substring(domainNameDelimiter)); + string route = url.Substring(domainNameDelimiter); + string parameters = ""; + int parametersDelimiter = route.IndexOf("?"); + if (parametersDelimiter != -1) + { + parameters = route.Substring(parametersDelimiter + 1); + route = route.Substring(0, parametersDelimiter); + } + + _logger.LogDebug("Request info:"); + _logger.LogDebug("\tBaseURL: \"{0}\"", baseURL); + _logger.LogDebug("\tRoute: \"{0}\"", route); + _logger.LogDebug("\tParameters: \"{0}\"", parameters); + + Request request = new Request(baseURL, route, parameters); if (response.Status == StatusCode.Success) { RequestCallback callback; _requestCallbacks.TryGetValue(request.Route, out callback); if (callback != null) { - callback(request, response); + callback(request, response, _logger); } else if (_onBadRequestCallback != null) { - _onBadRequestCallback(request, response); + _onBadRequestCallback(request, response, _logger); } else { + _logger.LogWarning("Bad request: No suitable request callback"); response.Status = StatusCode.BadRequest; return response; } @@ -170,13 +183,10 @@ namespace Cuipod private string ReadRequest(SslStream sslStream) { - byte[] buffer = new byte[2048]; - Decoder decoder = Encoding.UTF8.GetDecoder(); - StringBuilder requestData = new StringBuilder(); - int bytes = sslStream.Read(buffer, 0, buffer.Length); - char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; - decoder.GetChars(buffer, 0, bytes, chars, 0); + int bytes = sslStream.Read(_buffer, 0, _buffer.Length); + char[] chars = new char[_decoder.GetCharCount(_buffer, 0, bytes)]; + _decoder.GetChars(_buffer, 0, bytes, chars, 0); string line = new string(chars); if (line.EndsWith("\r\n")) { diff --git a/Cuipod/Cuipod.csproj b/Cuipod/Cuipod.csproj index 2f002aa..90c5d0c 100644 --- a/Cuipod/Cuipod.csproj +++ b/Cuipod/Cuipod.csproj @@ -4,4 +4,8 @@ net5.0 + + + + diff --git a/Cuipod/Request.cs b/Cuipod/Request.cs index 9e080dd..46cea01 100644 --- a/Cuipod/Request.cs +++ b/Cuipod/Request.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Cuipod +namespace Cuipod { public class Request { @@ -10,20 +6,11 @@ namespace Cuipod public string Route { get; internal set; } public string Parameters { get; internal set; } - public Request(string baseURL, string route) + internal Request(string baseURL, string route, string parameters) { BaseURL = baseURL; - - int parametersDelimiter = route.IndexOf("?"); - if (parametersDelimiter != -1) - { - Parameters = route.Substring(parametersDelimiter + 1); - Route = route.Substring(0, parametersDelimiter); - } - else - { - Route = route; - } + Route = route; + Parameters = parameters; } } } diff --git a/Cuipod/Response.cs b/Cuipod/Response.cs index d17c047..35166a1 100644 --- a/Cuipod/Response.cs +++ b/Cuipod/Response.cs @@ -8,10 +8,10 @@ namespace Cuipod { public StatusCode Status { get; set; } - private string _directoryToServe; + private readonly string _directoryToServe; private string _requestBody = ""; - public Response(string directoryToServe) + internal Response(string directoryToServe) { _directoryToServe = directoryToServe; Status = StatusCode.Success; diff --git a/CuipodExample/CuipodExample.csproj b/CuipodExample/CuipodExample.csproj index cf992af..e370129 100644 --- a/CuipodExample/CuipodExample.csproj +++ b/CuipodExample/CuipodExample.csproj @@ -7,10 +7,17 @@ + + + + Always + + + diff --git a/CuipodExample/Server.cs b/CuipodExample/Server.cs index c359fdb..c850029 100644 --- a/CuipodExample/Server.cs +++ b/CuipodExample/Server.cs @@ -1,5 +1,6 @@ using Cuipod; using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; using System; using System.Security.Cryptography.X509Certificates; @@ -11,30 +12,25 @@ namespace CuipodExample { CommandLineApplication commandLineApplication = new CommandLineApplication(); commandLineApplication.HelpOption("-h | --help"); - CommandArgument directoryToServe = commandLineApplication.Argument( - "directory", - "Directory to server (required)" - ); CommandArgument certificateFile = commandLineApplication.Argument( - "pfx certificate file", + "certificate", "Path to certificate (required)" ); - CommandArgument pfxPassword = commandLineApplication.Argument( - "pfx password", - "pfx password" + CommandArgument privateRSAKeyFilePath = commandLineApplication.Argument( + "key", + "Path to private Pkcs8 RSA key (required)" ); commandLineApplication.OnExecute(() => { - if (directoryToServe.Value == null || certificateFile.Value == null ) + if (certificateFile.Value == null || privateRSAKeyFilePath.Value == null) { commandLineApplication.ShowHelp(); return 1; } - var pass = (pfxPassword != null) ? pfxPassword.Value.ToString() : ""; - var cert = new X509Certificate2(certificateFile.Value.ToString(), pass); + X509Certificate2 cert = CertificateUtils.LoadCertificate(certificateFile.Value, privateRSAKeyFilePath.Value); - return AppMain(directoryToServe.Value, cert); + return AppMain("pages/", cert); }); try @@ -49,18 +45,31 @@ namespace CuipodExample private static int AppMain(string directoryToServe, X509Certificate2 certificate) { + using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder + .AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + }) + .SetMinimumLevel(LogLevel.Debug) + ); + + ILogger logger = loggerFactory.CreateLogger(); + App app = new App( directoryToServe, - certificate + certificate, + logger ); // Serve files - app.OnRequest("/", (request, response) => { + app.OnRequest("/", (request, response, logger) => { response.RenderFileContent("index.gmi"); }); // Input example - app.OnRequest("/input", (request, response) => { + app.OnRequest("/input", (request, response, logger) => { if (request.Parameters == null) { response.SetInputHint("Please enter something: "); @@ -74,7 +83,7 @@ namespace CuipodExample } }); - app.OnRequest("/show", (request, response) => { + app.OnRequest("/show", (request, response, logger) => { if (request.Parameters == null) { // redirect to input @@ -89,18 +98,19 @@ namespace CuipodExample }); // Or dynamically render content - app.OnRequest("/dynamic/content", (request, response) => { - response.RenderPlainTextLine("# woah much content!"); - response.RenderPlainTextLine("More utilities to render content will come soon!"); + app.OnRequest("/dynamic/content", (request, response, logger) => { + response.RenderPlainTextLine("# woah much dynamic content!"); }); // Optional but nice. In case it is specified and client will do a bad route // request we will respond with Success status and render result from this lambda - app.OnBadRequest((request, response) => { + app.OnBadRequest((request, response, logger) => { response.RenderPlainTextLine("# Ohh No!!! Request is bad :("); }); - return app.Run(); + app.Run(); + + return 0; } } } diff --git a/CuipodExample/pages/index.gmi b/CuipodExample/pages/index.gmi new file mode 100644 index 0000000..255cdfe --- /dev/null +++ b/CuipodExample/pages/index.gmi @@ -0,0 +1 @@ +# Hello world! \ No newline at end of file diff --git a/README.md b/README.md index cace916..b183e46 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # cuipod -Simple yet flexible framework for Gemini protocol servers - -Framework is written in C# and based on .NET 5.0 framework. -The project is still in very early stage so bugs are expected. Feel free to raise an issue ticket or even raise PR! +Simple yet flexible framework for Gemini protocol servers written in C# (.NET 5.0) ## Example +For testing purposes you can generate certificate with this command +``` +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout privatekey.key -out certificate.crt +``` ```csharp +using System; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; using Cuipod; namespace CuipodExample @@ -15,19 +19,35 @@ namespace CuipodExample { static int Main(string[] args) { + X509Certificate2 cert = CertificateUtils.LoadCertificate( + "/certificate.crt", // Path to certificate + "/privatekey.key" // Path to private Pkcs8 RSA key + ); + + using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder + .AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + }) + .SetMinimumLevel(LogLevel.Debug) + ); + ILogger logger = loggerFactory.CreateLogger(); + App app = new App( - "/", // directory to serve - "/certificate.crt", // path to certificate - "/privatekey.key" // path to private Pkcs8 RSA key + "pages/", // Directory to serve + certificate, + logger ); // Serve files - app.OnRequest("/", (request, response) => { + app.OnRequest("/", (request, response, logger) => { response.RenderFileContent("index.gmi"); }); // Input example - app.OnRequest("/input", (request, response) => { + app.OnRequest("/input", (request, response, logger) => { if (request.Parameters == null) { response.SetInputHint("Please enter something: "); @@ -41,7 +61,7 @@ namespace CuipodExample } }); - app.OnRequest("/show", (request, response) => { + app.OnRequest("/show", (request, response, logger) => { if (request.Parameters == null) { // redirect to input @@ -56,19 +76,25 @@ namespace CuipodExample }); // Or dynamically render content - app.OnRequest("/dynamic/content", (request, response) => { - response.RenderPlainTextLine("# woah much content!"); - response.RenderPlainTextLine("More utilities to render content will come soon!"); + app.OnRequest("/dynamic/content", (request, response, logger) => { + response.RenderPlainTextLine("# woah much dynamic content!"); }); // Optional but nice. In case it is specified and client will do a bad route // request we will respond with Success status and render result from this lambda - app.OnBadRequest((request, response) => { + app.OnBadRequest((request, response, logger) => { response.RenderPlainTextLine("# Ohh No!!! Request is bad :("); }); - return app.Run(); + app.Run(); + + return 0; } } } ``` + +Full example project is in `CuipodExample` directory + +# Contribution +Feel free to raise an issue ticket or even raise a pull request. \ No newline at end of file