committing initial code. It was a fun day!
Gemini is very nice and simple <3
This commit is contained in:
parent
b9fc631211
commit
69435b34a5
31
Cuipod.sln
Normal file
31
Cuipod.sln
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 16
|
||||||
|
VisualStudioVersion = 16.0.30621.155
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cuipod", "Cuipod\Cuipod.csproj", "{2B6AD5A5-F10B-4FC3-BF12-5DD26FAF3796}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CuipodExample", "CuipodExample\CuipodExample.csproj", "{BD343B0B-29EB-4498-8CD2-D9483B6BDA98}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{2B6AD5A5-F10B-4FC3-BF12-5DD26FAF3796}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2B6AD5A5-F10B-4FC3-BF12-5DD26FAF3796}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2B6AD5A5-F10B-4FC3-BF12-5DD26FAF3796}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2B6AD5A5-F10B-4FC3-BF12-5DD26FAF3796}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{BD343B0B-29EB-4498-8CD2-D9483B6BDA98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BD343B0B-29EB-4498-8CD2-D9483B6BDA98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BD343B0B-29EB-4498-8CD2-D9483B6BDA98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BD343B0B-29EB-4498-8CD2-D9483B6BDA98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {F2127615-5183-476F-8771-BF9710EB3B2C}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
155
Cuipod/App.cs
Normal file
155
Cuipod/App.cs
Normal 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'); ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Cuipod/CertificateUtils.cs
Normal file
34
Cuipod/CertificateUtils.cs
Normal 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
7
Cuipod/Cuipod.csproj
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
41
Cuipod/Response.cs
Normal file
41
Cuipod/Response.cs
Normal 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
25
Cuipod/StatusCode.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
12
CuipodExample/CuipodExample.csproj
Normal file
12
CuipodExample/CuipodExample.csproj
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Cuipod\Cuipod.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
39
CuipodExample/Server.cs
Normal file
39
CuipodExample/Server.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using Cuipod;
|
||||||
|
|
||||||
|
namespace CuipodExample
|
||||||
|
{
|
||||||
|
class Server
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
App app = new App(
|
||||||
|
"<directory_to_serve>/", // directory to serve
|
||||||
|
"<dir_with_cert>/certificate.crt", // path to certificate
|
||||||
|
"<dir_with_cert>/privatekey.key" // path to private Pkcs8 RSA key
|
||||||
|
);
|
||||||
|
|
||||||
|
// Serve files
|
||||||
|
app.OnRequest("/", response => {
|
||||||
|
response.RenderFileContent("index.gmi");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.OnRequest("/about/", response => {
|
||||||
|
response.RenderFileContent("about_me.gmi");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or dynamically render content
|
||||||
|
app.OnRequest("/dynamic/content/", response => {
|
||||||
|
response.RenderPlainTextLine("# woah much 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
|
||||||
|
// request we will respond with Success status and render result from this lambda
|
||||||
|
app.OnBadRequest(response => {
|
||||||
|
response.RenderPlainTextLine("# Ohh No!!! Request is bad :(");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
README.md
49
README.md
@ -1,2 +1,49 @@
|
|||||||
# cuipod
|
# cuipod
|
||||||
Simple yet flexible framework for Gemini protocol server
|
Simple yet flexible framework for Gemini protocol servers
|
||||||
|
|
||||||
|
Framework is written in C# and based on .NET Core 3.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
|
||||||
|
|
||||||
|
```
|
||||||
|
using Cuipod;
|
||||||
|
|
||||||
|
namespace CuipodExample
|
||||||
|
{
|
||||||
|
class Server
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
App app = new App(
|
||||||
|
"<directory_to_serve>/", // directory to serve
|
||||||
|
"<dir_with_cert>/certificate.crt", // path to certificate
|
||||||
|
"<dir_with_cert>/privatekey.key" // path to private Pkcs8 RSA key
|
||||||
|
);
|
||||||
|
|
||||||
|
// Serve files
|
||||||
|
app.OnRequest("/", response => {
|
||||||
|
response.RenderFileContent("index.gmi");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.OnRequest("/about/", response => {
|
||||||
|
response.RenderFileContent("about_me.gmi");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or dynamically render content
|
||||||
|
app.OnRequest("/dynamic/content/", response => {
|
||||||
|
response.RenderPlainTextLine("# woah much 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
|
||||||
|
// request we will respond with Success status and render result from this lambda
|
||||||
|
app.OnBadRequest(response => {
|
||||||
|
response.RenderPlainTextLine("# Ohh No!!! Request is bad :(");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user