Added logging support and refactored some code

Log4J vuln inspired me to add some logging to this project :)))
This commit is contained in:
Egidijus Lileika 2021-12-15 16:46:43 +02:00
parent a3a6a34e57
commit b74707aac8
8 changed files with 143 additions and 98 deletions

View File

@ -8,35 +8,30 @@ using System.Text;
using System.Security.Authentication; using System.Security.Authentication;
using System.IO; using System.IO;
using RequestCallback = System.Action<Cuipod.Request, Cuipod.Response>; using Microsoft.Extensions.Logging;
namespace Cuipod namespace Cuipod
{ {
using RequestCallback = System.Action<Cuipod.Request, Cuipod.Response, ILogger<App>>;
public class App public class App
{ {
private readonly TcpListener _listener = new TcpListener(IPAddress.Any, 1965);
private readonly Dictionary<string, RequestCallback> _requestCallbacks = new Dictionary<string, RequestCallback>();
private readonly byte[] _buffer = new byte[4096];
private readonly Decoder _decoder = Encoding.UTF8.GetDecoder();
private readonly string _directoryToServe; private readonly string _directoryToServe;
private readonly TcpListener _listener;
private readonly X509Certificate2 _serverCertificate; private readonly X509Certificate2 _serverCertificate;
private readonly Dictionary<string, RequestCallback> _requestCallbacks; private readonly ILogger<App> _logger;
private RequestCallback _onBadRequestCallback; private RequestCallback _onBadRequestCallback;
//somewhat flaky implementation - probably deprecate it public App(string directoryToServe, X509Certificate2 certificate, ILogger<App> logger)
public App(string directoryToServe, string certificateFile, string privateRSAKeyFilePath)
{ {
_directoryToServe = directoryToServe; _directoryToServe = directoryToServe;
_listener = new TcpListener(IPAddress.Any, 1965);
_requestCallbacks = new Dictionary<string, RequestCallback>();
_serverCertificate = CertificateUtils.LoadCertificate(certificateFile, privateRSAKeyFilePath);
}
public App(string directoryToServe, X509Certificate2 certificate)
{
_directoryToServe = directoryToServe;
_listener = new TcpListener(IPAddress.Any, 1965);
_requestCallbacks = new Dictionary<string, RequestCallback>();
_serverCertificate = certificate; _serverCertificate = certificate;
_logger = logger;
} }
public void OnRequest(string route, RequestCallback callback) public void OnRequest(string route, RequestCallback callback)
@ -49,13 +44,14 @@ namespace Cuipod
_onBadRequestCallback = callback; _onBadRequestCallback = callback;
} }
public int Run() public void Run()
{ {
int status = 0;
Console.WriteLine("Serving capsule on 0.0.0.0:1965");
try try
{ {
_listener.Start(); _listener.Start();
_logger.LogInformation("Serving capsule on {0}", _listener.Server.LocalEndPoint.ToString());
while (true) while (true)
{ {
ProcessRequest(_listener.AcceptTcpClient()); ProcessRequest(_listener.AcceptTcpClient());
@ -63,15 +59,12 @@ namespace Cuipod
} }
catch (SocketException e) catch (SocketException e)
{ {
Console.WriteLine("SocketException: {0}", e); _logger.LogError("SocketException: {0}", e);
status = 1;
} }
finally finally
{ {
_listener.Stop(); _listener.Stop();
} }
return status;
} }
private void ProcessRequest(TcpClient client) private void ProcessRequest(TcpClient client)
@ -86,16 +79,16 @@ namespace Cuipod
} }
catch (AuthenticationException e) catch (AuthenticationException e)
{ {
Console.WriteLine("Exception: {0}", e.Message); _logger.LogError("AuthenticationException: {0}", e.Message);
if (e.InnerException != null) 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) catch (IOException e)
{ {
Console.WriteLine("Exception: {0}", e.Message); _logger.LogError("IOException: {0}", e.Message);
} }
finally finally
{ {
@ -110,33 +103,37 @@ namespace Cuipod
sslStream.AuthenticateAsServer(_serverCertificate, false, SslProtocols.Tls12 | SslProtocols.Tls13, false); sslStream.AuthenticateAsServer(_serverCertificate, false, SslProtocols.Tls12 | SslProtocols.Tls13, false);
// Read a message from the client. // Read a message from the client.
string rawURL = ReadRequest(sslStream); string rawRequest = ReadRequest(sslStream);
Response response = new Response(_directoryToServe); Response response = new Response(_directoryToServe);
if (rawURL == null) if (rawRequest == null)
{ {
_logger.LogDebug("rawRequest is null - bad request");
response.Status = StatusCode.BadRequest; response.Status = StatusCode.BadRequest;
return response; 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) if (protocolDelimiter == -1)
{ {
response.Status = StatusCode.BadRequest; response.Status = StatusCode.BadRequest;
return response; return response;
} }
string protocol = rawURL.Substring(0, protocolDelimiter); string requestProtocol = rawRequest.Substring(0, protocolDelimiter);
if (protocol != "gemini") if (requestProtocol != protocol)
{ {
response.Status = StatusCode.BadRequest; response.Status = StatusCode.BadRequest;
return response; return response;
} }
string url = rawURL.Substring(protocolDelimiter + 3); string url = rawRequest.Substring(protocolDelimiter + protocolSeparator.Length);
int domainNameDelimiter = url.IndexOf("/"); int domainNameDelimiter = url.IndexOf("/");
if (domainNameDelimiter == -1) if (domainNameDelimiter == -1)
{ {
@ -144,22 +141,38 @@ namespace Cuipod
return response; return response;
} }
string domainName = url.Substring(0, domainNameDelimiter); 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) if (response.Status == StatusCode.Success)
{ {
RequestCallback callback; RequestCallback callback;
_requestCallbacks.TryGetValue(request.Route, out callback); _requestCallbacks.TryGetValue(request.Route, out callback);
if (callback != null) if (callback != null)
{ {
callback(request, response); callback(request, response, _logger);
} }
else if (_onBadRequestCallback != null) else if (_onBadRequestCallback != null)
{ {
_onBadRequestCallback(request, response); _onBadRequestCallback(request, response, _logger);
} }
else else
{ {
_logger.LogWarning("Bad request: No suitable request callback");
response.Status = StatusCode.BadRequest; response.Status = StatusCode.BadRequest;
return response; return response;
} }
@ -170,13 +183,10 @@ namespace Cuipod
private string ReadRequest(SslStream sslStream) private string ReadRequest(SslStream sslStream)
{ {
byte[] buffer = new byte[2048];
Decoder decoder = Encoding.UTF8.GetDecoder();
StringBuilder requestData = new StringBuilder(); StringBuilder requestData = new StringBuilder();
int bytes = sslStream.Read(buffer, 0, buffer.Length); int bytes = sslStream.Read(_buffer, 0, _buffer.Length);
char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; char[] chars = new char[_decoder.GetCharCount(_buffer, 0, bytes)];
decoder.GetChars(buffer, 0, bytes, chars, 0); _decoder.GetChars(_buffer, 0, bytes, chars, 0);
string line = new string(chars); string line = new string(chars);
if (line.EndsWith("\r\n")) if (line.EndsWith("\r\n"))
{ {

View File

@ -4,4 +4,8 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
</ItemGroup>
</Project> </Project>

View File

@ -1,8 +1,4 @@
using System; namespace Cuipod
using System.Collections.Generic;
using System.Text;
namespace Cuipod
{ {
public class Request public class Request
{ {
@ -10,20 +6,11 @@ namespace Cuipod
public string Route { get; internal set; } public string Route { get; internal set; }
public string Parameters { 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; 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;
} }
} }
} }

View File

@ -8,10 +8,10 @@ namespace Cuipod
{ {
public StatusCode Status { get; set; } public StatusCode Status { get; set; }
private string _directoryToServe; private readonly string _directoryToServe;
private string _requestBody = ""; private string _requestBody = "";
public Response(string directoryToServe) internal Response(string directoryToServe)
{ {
_directoryToServe = directoryToServe; _directoryToServe = directoryToServe;
Status = StatusCode.Success; Status = StatusCode.Success;

View File

@ -7,10 +7,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Cuipod\Cuipod.csproj" /> <ProjectReference Include="..\Cuipod\Cuipod.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="pages\index.gmi">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View File

@ -1,5 +1,6 @@
using Cuipod; using Cuipod;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
using System; using System;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
@ -11,30 +12,25 @@ namespace CuipodExample
{ {
CommandLineApplication commandLineApplication = new CommandLineApplication(); CommandLineApplication commandLineApplication = new CommandLineApplication();
commandLineApplication.HelpOption("-h | --help"); commandLineApplication.HelpOption("-h | --help");
CommandArgument directoryToServe = commandLineApplication.Argument(
"directory",
"Directory to server (required)"
);
CommandArgument certificateFile = commandLineApplication.Argument( CommandArgument certificateFile = commandLineApplication.Argument(
"pfx certificate file", "certificate",
"Path to certificate (required)" "Path to certificate (required)"
); );
CommandArgument pfxPassword = commandLineApplication.Argument( CommandArgument privateRSAKeyFilePath = commandLineApplication.Argument(
"pfx password", "key",
"pfx password" "Path to private Pkcs8 RSA key (required)"
); );
commandLineApplication.OnExecute(() => commandLineApplication.OnExecute(() =>
{ {
if (directoryToServe.Value == null || certificateFile.Value == null ) if (certificateFile.Value == null || privateRSAKeyFilePath.Value == null)
{ {
commandLineApplication.ShowHelp(); commandLineApplication.ShowHelp();
return 1; return 1;
} }
var pass = (pfxPassword != null) ? pfxPassword.Value.ToString() : ""; X509Certificate2 cert = CertificateUtils.LoadCertificate(certificateFile.Value, privateRSAKeyFilePath.Value);
var cert = new X509Certificate2(certificateFile.Value.ToString(), pass);
return AppMain(directoryToServe.Value, cert); return AppMain("pages/", cert);
}); });
try try
@ -49,18 +45,31 @@ namespace CuipodExample
private static int AppMain(string directoryToServe, X509Certificate2 certificate) 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<App> logger = loggerFactory.CreateLogger<App>();
App app = new App( App app = new App(
directoryToServe, directoryToServe,
certificate certificate,
logger
); );
// Serve files // Serve files
app.OnRequest("/", (request, response) => { app.OnRequest("/", (request, response, logger) => {
response.RenderFileContent("index.gmi"); response.RenderFileContent("index.gmi");
}); });
// Input example // Input example
app.OnRequest("/input", (request, response) => { app.OnRequest("/input", (request, response, logger) => {
if (request.Parameters == null) if (request.Parameters == null)
{ {
response.SetInputHint("Please enter something: "); 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) if (request.Parameters == null)
{ {
// redirect to input // redirect to input
@ -89,18 +98,19 @@ namespace CuipodExample
}); });
// Or dynamically render content // Or dynamically render content
app.OnRequest("/dynamic/content", (request, response) => { app.OnRequest("/dynamic/content", (request, response, logger) => {
response.RenderPlainTextLine("# woah much content!"); response.RenderPlainTextLine("# woah much dynamic content!");
response.RenderPlainTextLine("More utilities to render content will come soon!");
}); });
// Optional but nice. In case it is specified and client will do a bad route // 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 // 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 :("); response.RenderPlainTextLine("# Ohh No!!! Request is bad :(");
}); });
return app.Run(); app.Run();
return 0;
} }
} }
} }

View File

@ -0,0 +1 @@
# Hello world!

View File

@ -1,12 +1,16 @@
# cuipod # cuipod
Simple yet flexible framework for Gemini protocol servers Simple yet flexible framework for Gemini protocol servers written in C# (.NET 5.0)
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!
## Example ## 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 ```csharp
using System;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
using Cuipod; using Cuipod;
namespace CuipodExample namespace CuipodExample
@ -15,19 +19,35 @@ namespace CuipodExample
{ {
static int Main(string[] args) static int Main(string[] args)
{ {
X509Certificate2 cert = CertificateUtils.LoadCertificate(
"<dir_with_cert>/certificate.crt", // Path to certificate
"<dir_with_cert>/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<App> logger = loggerFactory.CreateLogger<App>();
App app = new App( App app = new App(
"<directory_to_serve>/", // directory to serve "pages/", // Directory to serve
"<dir_with_cert>/certificate.crt", // path to certificate certificate,
"<dir_with_cert>/privatekey.key" // path to private Pkcs8 RSA key logger
); );
// Serve files // Serve files
app.OnRequest("/", (request, response) => { app.OnRequest("/", (request, response, logger) => {
response.RenderFileContent("index.gmi"); response.RenderFileContent("index.gmi");
}); });
// Input example // Input example
app.OnRequest("/input", (request, response) => { app.OnRequest("/input", (request, response, logger) => {
if (request.Parameters == null) if (request.Parameters == null)
{ {
response.SetInputHint("Please enter something: "); 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) if (request.Parameters == null)
{ {
// redirect to input // redirect to input
@ -56,19 +76,25 @@ namespace CuipodExample
}); });
// Or dynamically render content // Or dynamically render content
app.OnRequest("/dynamic/content", (request, response) => { app.OnRequest("/dynamic/content", (request, response, logger) => {
response.RenderPlainTextLine("# woah much content!"); response.RenderPlainTextLine("# woah much dynamic content!");
response.RenderPlainTextLine("More utilities to render content will come soon!");
}); });
// Optional but nice. In case it is specified and client will do a bad route // 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 // 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 :("); 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.