committing initial code. It was a fun day!

Gemini is very nice and simple <3
This commit is contained in:
Egidijus Lileika
2021-02-06 17:59:11 +02:00
parent b9fc631211
commit 69435b34a5
9 changed files with 392 additions and 1 deletions

155
Cuipod/App.cs Normal file
View File

@@ -0,0 +1,155 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Security.Authentication;
using System.IO;
namespace Cuipod
{
public class App
{
private readonly string _directoryToServe;
private readonly TcpListener _listener;
private readonly X509Certificate2 _serverCertificate;
private readonly Dictionary<string, Action<Response>> _requestCallbacks;
private Action<Response> _onBadRequestCallback;
public App(string directoryToServe, string certificateFile, string privateRSAKeyFilePath)
{
_directoryToServe = directoryToServe;
IPAddress localAddress = IPAddress.Parse("127.0.0.1");
_listener = new TcpListener(localAddress, 1965);
_requestCallbacks = new Dictionary<string, Action<Response>>();
_serverCertificate = CertificateUtils.LoadCertificate(certificateFile, privateRSAKeyFilePath);
}
public void OnRequest(string route, Action<Response> callback)
{
_requestCallbacks.Add(route, callback);
}
public void OnBadRequest(Action<Response> callback)
{
_onBadRequestCallback = callback;
}
public void Run()
{
Console.WriteLine("Serving capsule on 127.0.0.1:1965");
try
{
_listener.Start();
while (true)
{
ProcessRequest(_listener.AcceptTcpClient());
}
}
catch (SocketException e)
{
Console.WriteLine("SocketException: {0}", e);
}
finally
{
_listener.Stop();
}
}
private void ProcessRequest(TcpClient client)
{
SslStream sslStream = new SslStream(client.GetStream(), false);
try
{
sslStream.AuthenticateAsServer(_serverCertificate, false, SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false);
// Read a message from the client.
string rawURI = ReadRequest(sslStream);
Response response = new Response(_directoryToServe);
int protocolDelimiter = rawURI.IndexOf("://");
if (protocolDelimiter == -1)
{
response.Status = StatusCode.BadRequest;
}
string protocol = rawURI.Substring(0, protocolDelimiter);
if (protocol != "gemini")
{
response.Status = StatusCode.BadRequest;
}
string url = rawURI.Substring(protocolDelimiter + 3);
int domainNameDelimiter = url.IndexOf("/");
if (domainNameDelimiter == -1)
{
response.Status = StatusCode.BadRequest;
}
string domainName = url.Substring(0, domainNameDelimiter);
// TODO: validate domain name from cert?
string route = url.Substring(domainNameDelimiter);
if (response.Status == StatusCode.Success)
{
Action<Response> callback = null;
_requestCallbacks.TryGetValue(route, out callback);
if (callback != null)
{
callback(response);
} else if (_onBadRequestCallback != null)
{
_onBadRequestCallback(response);
} else
{
response.Status = StatusCode.BadRequest;
}
}
sslStream.Write(response.Encode());
}
catch (AuthenticationException e)
{
Console.WriteLine("Exception: {0}", e.Message);
if (e.InnerException != null)
{
Console.WriteLine("Inner exception: {0}", e.InnerException.Message);
}
Console.WriteLine("Authentication failed - closing the connection.");
}
catch (IOException e)
{
Console.WriteLine("Exception: {0}", e.Message);
}
finally
{
sslStream.Close();
client.Close();
}
}
private string ReadRequest(SslStream sslStream)
{
byte[] buffer = new byte[2048];
Decoder decoder = Encoding.UTF8.GetDecoder();
StringBuilder requestData = new StringBuilder();
string line;
do
{
int bytes = sslStream.Read(buffer, 0, buffer.Length);
char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
decoder.GetChars(buffer, 0, bytes, chars, 0);
line = new string(chars);
requestData.Append(line);
} while (!line.EndsWith("\r\n"));
return requestData.ToString().TrimEnd('\r', '\n'); ;
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace Cuipod
{
public static class CertificateUtils
{
static public X509Certificate2 LoadCertificate(string certFilePath, string privateRSAKeyFilePath)
{
X509Certificate2 cert = new X509Certificate2(certFilePath);
string[] lines = File.ReadAllLines(privateRSAKeyFilePath, Encoding.UTF8);
string privateKeyData = "";
for (int i = 1; i < lines.Length - 1; ++i)
{
privateKeyData += lines[i];
}
byte[] bytes = Convert.FromBase64String(privateKeyData);
RSA rsa = RSA.Create();
// For now we always expect to have cert with BEGIN PRIVATE KEY label
// TODO: add handling for other types, maybe??
rsa.ImportPkcs8PrivateKey(bytes, out _);
X509Certificate2 pubPrivEphemeral = cert.CopyWithPrivateKey(rsa);
cert = new X509Certificate2(pubPrivEphemeral.Export(X509ContentType.Pfx));
return cert;
}
}
}

7
Cuipod/Cuipod.csproj Normal file
View File

@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
</Project>

41
Cuipod/Response.cs Normal file
View File

@@ -0,0 +1,41 @@
using System;
using System.IO;
using System.Text;
namespace Cuipod
{
public class Response
{
public StatusCode Status { get; internal set; }
private string _directoryToServe;
private string _requestBody = "";
public Response(string directoryToServe)
{
_directoryToServe = directoryToServe;
Status = StatusCode.Success;
}
public void RenderPlainTextLine(string text)
{
_requestBody += text + "\r\n";
}
public void RenderFileContent(string relativePathToFile)
{
_requestBody += File.ReadAllText(_directoryToServe + relativePathToFile, Encoding.UTF8) + "\r\n";
}
internal static string WriteHeader(StatusCode statusCode)
{
return ((int)statusCode).ToString() + " text/gemini\r\n";
}
internal byte[] Encode()
{
string wholeResponse = WriteHeader(Status) + _requestBody;
return Encoding.UTF8.GetBytes(wholeResponse);
}
}
}

25
Cuipod/StatusCode.cs Normal file
View File

@@ -0,0 +1,25 @@
namespace Cuipod
{
public enum StatusCode
{
Input = 10,
SensitiveInput = 11,
Success = 20,
RedirectTemp = 30,
RedirectPerm = 31,
FailureTemp = 40,
ServerUnavailable = 41,
CGIError = 42,
ProxyError = 43,
SlowDown = 44,
FailurePerm = 50,
NotFound = 51,
Gone = 52,
ProxyRefused = 53,
BadRequest = 59,
ClientCertRequired = 60,
CertNotAuthorised = 61,
CertNotValid = 62
}
}